View Javadoc
1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one
3    *  or more contributor license agreements.  See the NOTICE file
4    *  distributed with this work for additional information
5    *  regarding copyright ownership.  The ASF licenses this file
6    *  to you under the Apache License, Version 2.0 (the
7    *  "License"); you may not use this file except in compliance
8    *  with the License.  You may obtain a copy of the License at
9    *
10   *    http://www.apache.org/licenses/LICENSE-2.0
11   *
12   *  Unless required by applicable law or agreed to in writing,
13   *  software distributed under the License is distributed on an
14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *  KIND, either express or implied.  See the License for the
16   *  specific language governing permissions and limitations
17   *  under the License.
18   *
19   */
20  package org.apache.directory.api.ldap.codec.api;
21  
22  
23  import javax.security.sasl.Sasl;
24  import javax.security.sasl.SaslClient;
25  import javax.security.sasl.SaslException;
26  import javax.security.sasl.SaslServer;
27  
28  import org.apache.directory.api.ldap.model.constants.SaslQoP;
29  import org.apache.mina.core.buffer.IoBuffer;
30  import org.apache.mina.core.filterchain.IoFilterAdapter;
31  import org.apache.mina.core.session.IoSession;
32  import org.apache.mina.core.write.DefaultWriteRequest;
33  import org.apache.mina.core.write.WriteRequest;
34  import org.slf4j.Logger;
35  import org.slf4j.LoggerFactory;
36  
37  
38  /**
39   * An {@link IoFilterAdapter} that handles integrity and confidentiality protection
40   * for a SASL bound session.  The SaslFilter must be constructed with a SASL
41   * context that has completed SASL negotiation.  Some SASL mechanisms, such as
42   * CRAM-MD5, only support authentication and thus do not need this filter.  DIGEST-MD5
43   * and GSSAPI do support message integrity and confidentiality and, therefore,
44   * do need this filter.
45   * 
46   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
47   */
48  public class SaslFilter extends IoFilterAdapter
49  {
50      private static final Logger LOG = LoggerFactory.getLogger( SaslFilter.class );
51  
52      /**
53       * A session attribute key that makes next one write request bypass
54       * this filter (not adding a security layer).  This is a marker attribute,
55       * which means that you can put whatever as its value. ({@link Boolean#TRUE}
56       * is preferred.)  The attribute is automatically removed from the session
57       * attribute map as soon as {@link IoSession#write(Object)} is invoked,
58       * and therefore should be put again if you want to make more messages
59       * bypass this filter.
60       */
61      public static final String DISABLE_SECURITY_LAYER_ONCE = SaslFilter.class.getName() + ".DisableSecurityLayerOnce";
62  
63      /**
64       * A session attribute key that holds the received bytes of partially received
65       * SASL message.
66       */
67      public static final String BYTES = SaslFilter.class.getName() + ".Buffer";
68  
69      /**
70       * A session attribute key that holds the offset of partially received
71       * SASL message.
72       */
73      public static final String OFFSET = SaslFilter.class.getName() + ".Offset";
74  
75      /** The SASL client, only set if the filter is used at the client side. */
76      private final SaslClient saslClient;
77  
78      /** The SASL server, only set if the filter is used at the server side. */
79      private final SaslServer saslServer;
80  
81      /** True if a security layer has been negotiated */
82      private boolean hasSecurityLayer;
83  
84      /** The negotiated max buffer size */
85      private int maxBufferSize;
86  
87      /**
88       * Creates a new instance of SaslFilter.  The SaslFilter must be constructed
89       * with a SASL client that has completed SASL negotiation.  The SASL client
90       * will be used to provide message integrity and, optionally, message
91       * confidentiality.
92       *
93       * @param saslClient The initialized SASL client.
94       */
95      public SaslFilter( SaslClient saslClient )
96      {
97          if ( saslClient == null )
98          {
99              throw new IllegalArgumentException();
100         }
101 
102         this.saslServer = null;
103         this.saslClient = saslClient;
104         initHasSecurityLayer( ( String ) saslClient.getNegotiatedProperty( Sasl.QOP ) );
105         initMaxBuffer( ( String ) saslClient.getNegotiatedProperty( Sasl.MAX_BUFFER ) );
106     }
107 
108 
109     /**
110      * Creates a new instance of SaslFilter.  The SaslFilter must be constructed
111      * with a SASL server that has completed SASL negotiation.  The SASL server
112      * will be used to provide message integrity and, optionally, message
113      * confidentiality.
114      *
115      * @param saslClient The initialized SASL server.
116      */
117     public SaslFilter( SaslServer saslServer )
118     {
119         if ( saslServer == null )
120         {
121             throw new IllegalArgumentException();
122         }
123 
124         this.saslClient = null;
125         this.saslServer = saslServer;
126         initHasSecurityLayer( ( String ) saslServer.getNegotiatedProperty( Sasl.QOP ) );
127         initMaxBuffer( ( String ) saslServer.getNegotiatedProperty( Sasl.MAX_BUFFER ) );
128     }
129 
130 
131     private void initHasSecurityLayer( String qop )
132     {
133         this.hasSecurityLayer = ( qop != null && ( qop.equals( SaslQoP.AUTH_INT.getValue() ) || qop
134             .equals( SaslQoP.AUTH_CONF.getValue() ) ) );
135     }
136 
137 
138     private void initMaxBuffer( String maxBuffer )
139     {
140         this.maxBufferSize = maxBuffer != null ? Integer.parseInt( maxBuffer ) : 65536;
141     }
142 
143 
144     @Override
145     public synchronized void messageReceived( NextFilter nextFilter, IoSession session, Object message )
146         throws SaslException
147     {
148         LOG.debug( "Message received:  {}", message );
149 
150         if ( !hasSecurityLayer )
151         {
152             LOG.debug( "Will not use SASL on received message." );
153             nextFilter.messageReceived( session, message );
154             return;
155         }
156 
157         /*
158          * Unwrap the data for mechanisms that support QoP (DIGEST-MD5, GSSAPI).
159          */
160         IoBuffer buf = ( IoBuffer ) message;
161         while ( buf.hasRemaining() )
162         {
163             /*
164              * Check for a previously received partial SASL message which is stored in the session.
165              * Otherwise read the first 4 bytes which is the length and allocate the bytes.
166              * Ensure the buffer size doesn't exceed the negotiated max buffer size.
167              */
168             byte[] bytes = ( byte[] ) session.getAttribute( BYTES, null );
169             int offset = ( int ) session.getAttribute( OFFSET, -1 );
170             if ( bytes == null )
171             {
172                 int bufferSize = buf.getInt();
173                 if ( bufferSize > maxBufferSize )
174                 {
175                     throw new IllegalStateException(
176                         bufferSize + " exceeds the negotiated receive buffer size limit: " + maxBufferSize );
177                 }
178                 bytes = new byte[bufferSize];
179                 offset = 0;
180             }
181 
182             /*
183              * Get the buffer as bytes. Handle the case that only a part of the SASL message was received.
184              */
185             int length = Math.min( bytes.length - offset, buf.remaining() );
186             buf.get( bytes, offset, length );
187 
188             /*
189              * Check if the full SASL message was received. If not store the partially received data in
190              * the session so it can be resumed when the next message is received.
191              */
192             offset += length;
193             if ( offset < bytes.length )
194             {
195                 LOG.debug( "Partial SASL message received:  {}/{}", offset, bytes.length );
196                 session.setAttribute( BYTES, bytes );
197                 session.setAttribute( OFFSET, offset );
198                 break;
199             }
200 
201             /*
202              * Unwrap the SASL message and forward it to the next filter.
203              */
204             LOG.debug( "Will use SASL to unwrap received message of length:  {}", bytes.length );
205             byte[] token = unwrap( bytes, 0, bytes.length );
206             nextFilter.messageReceived( session, IoBuffer.wrap( token ) );
207 
208             /*
209              * Finally clear the session attributes.
210              */
211             session.removeAttribute( BYTES );
212             session.removeAttribute( OFFSET );
213         }
214     }
215 
216 
217     @Override
218     public synchronized void filterWrite( NextFilter nextFilter, IoSession session, WriteRequest writeRequest )
219         throws SaslException
220     {
221         LOG.debug( "Filtering write request:  {}", writeRequest );
222 
223         /*
224          * Check if security layer processing should be disabled once.
225          */
226         if ( session.containsAttribute( DISABLE_SECURITY_LAYER_ONCE ) )
227         {
228             // Remove the marker attribute because it is temporary.
229             LOG.debug( "Disabling SaslFilter once; will not use SASL on write request." );
230             session.removeAttribute( DISABLE_SECURITY_LAYER_ONCE );
231             nextFilter.filterWrite( session, writeRequest );
232             return;
233         }
234 
235         if ( !hasSecurityLayer )
236         {
237             LOG.debug( "Will not use SASL on write request." );
238             nextFilter.filterWrite( session, writeRequest );
239             return;
240         }
241 
242         /*
243          * Wrap the data for mechanisms that support QoP (DIGEST-MD5, GSSAPI).
244          */
245 
246         /*
247          * Get the buffer as bytes.
248          */
249         IoBuffer buf = ( IoBuffer ) writeRequest.getMessage();
250         int bufferLength = buf.remaining();
251         byte[] bufferBytes = new byte[bufferLength];
252         buf.get( bufferBytes );
253 
254         LOG.info( "Will use SASL to wrap message of length:  {}", bufferLength );
255 
256         /*
257          * Ensure to not send larger SASL message than negotiated.
258          */
259         int max = maxBufferSize - 200;
260         for ( int offset = 0; offset < bufferLength; offset += max )
261         {
262             int length = Math.min( bufferLength - offset, max );
263             byte[] saslLayer = wrap( bufferBytes, offset, length );
264 
265             /*
266              * Prepend 4 byte length.
267              */
268             IoBuffer saslLayerBuffer = IoBuffer.allocate( 4 + saslLayer.length );
269             saslLayerBuffer.putInt( saslLayer.length );
270             saslLayerBuffer.put( saslLayer );
271             saslLayerBuffer.position( 0 );
272             saslLayerBuffer.limit( 4 + saslLayer.length );
273 
274             LOG.debug( "Sending encrypted token of length {}.", saslLayerBuffer.limit() );
275             nextFilter.filterWrite( session, new DefaultWriteRequest( saslLayerBuffer, writeRequest.getFuture() ) );
276         }
277     }
278 
279 
280     private byte[] wrap( byte[] buffer, int offset, int length ) throws SaslException
281     {
282         if ( saslClient != null )
283         {
284             return saslClient.wrap( buffer, offset, length );
285         }
286         else
287         {
288             return saslServer.wrap( buffer, offset, length );
289         }
290     }
291 
292 
293     private byte[] unwrap( byte[] buffer, int offset, int length ) throws SaslException
294     {
295         if ( saslClient != null )
296         {
297             return saslClient.unwrap( buffer, offset, length );
298         }
299         else
300         {
301             return saslServer.unwrap( buffer, offset, length );
302         }
303     }
304 
305 }