001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 *
019 */
020package org.apache.directory.api.ldap.codec.api;
021
022
023import javax.security.sasl.Sasl;
024import javax.security.sasl.SaslClient;
025import javax.security.sasl.SaslException;
026import javax.security.sasl.SaslServer;
027
028import org.apache.directory.api.ldap.model.constants.SaslQoP;
029import org.apache.mina.core.buffer.IoBuffer;
030import org.apache.mina.core.filterchain.IoFilterAdapter;
031import org.apache.mina.core.session.IoSession;
032import org.apache.mina.core.write.DefaultWriteRequest;
033import org.apache.mina.core.write.WriteRequest;
034import org.slf4j.Logger;
035import org.slf4j.LoggerFactory;
036
037
038/**
039 * An {@link IoFilterAdapter} that handles integrity and confidentiality protection
040 * for a SASL bound session.  The SaslFilter must be constructed with a SASL
041 * context that has completed SASL negotiation.  Some SASL mechanisms, such as
042 * CRAM-MD5, only support authentication and thus do not need this filter.  DIGEST-MD5
043 * and GSSAPI do support message integrity and confidentiality and, therefore,
044 * do need this filter.
045 * 
046 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
047 */
048public class SaslFilter extends IoFilterAdapter
049{
050    private static final Logger LOG = LoggerFactory.getLogger( SaslFilter.class );
051
052    /**
053     * A session attribute key that makes next one write request bypass
054     * this filter (not adding a security layer).  This is a marker attribute,
055     * which means that you can put whatever as its value. ({@link Boolean#TRUE}
056     * is preferred.)  The attribute is automatically removed from the session
057     * attribute map as soon as {@link IoSession#write(Object)} is invoked,
058     * and therefore should be put again if you want to make more messages
059     * bypass this filter.
060     */
061    public static final String DISABLE_SECURITY_LAYER_ONCE = SaslFilter.class.getName() + ".DisableSecurityLayerOnce";
062
063    /**
064     * A session attribute key that holds the received bytes of partially received
065     * SASL message.
066     */
067    public static final String BYTES = SaslFilter.class.getName() + ".Buffer";
068
069    /**
070     * A session attribute key that holds the offset of partially received
071     * SASL message.
072     */
073    public static final String OFFSET = SaslFilter.class.getName() + ".Offset";
074
075    /** The SASL client, only set if the filter is used at the client side. */
076    private final SaslClient saslClient;
077
078    /** The SASL server, only set if the filter is used at the server side. */
079    private final SaslServer saslServer;
080
081    /** True if a security layer has been negotiated */
082    private boolean hasSecurityLayer;
083
084    /** The negotiated max buffer size */
085    private int maxBufferSize;
086
087    /**
088     * Creates a new instance of SaslFilter.  The SaslFilter must be constructed
089     * with a SASL client that has completed SASL negotiation.  The SASL client
090     * will be used to provide message integrity and, optionally, message
091     * confidentiality.
092     *
093     * @param saslClient The initialized SASL client.
094     */
095    public SaslFilter( SaslClient saslClient )
096    {
097        if ( saslClient == null )
098        {
099            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}