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     */
020    package org.apache.directory.server.core.kerberos;
021    
022    
023    import java.io.IOException;
024    import java.util.ArrayList;
025    import java.util.Collection;
026    import java.util.Collections;
027    import java.util.HashSet;
028    import java.util.Iterator;
029    import java.util.List;
030    import java.util.Map;
031    import java.util.Set;
032    
033    import javax.naming.NamingException;
034    
035    import org.apache.directory.server.core.authn.AuthenticationInterceptor;
036    import org.apache.directory.server.core.authz.AciAuthorizationInterceptor;
037    import org.apache.directory.server.core.authz.DefaultAuthorizationInterceptor;
038    import org.apache.directory.server.core.collective.CollectiveAttributeInterceptor;
039    import org.apache.directory.server.core.entry.ClonedServerEntry;
040    import org.apache.directory.server.core.event.EventInterceptor;
041    import org.apache.directory.server.core.exception.ExceptionInterceptor;
042    import org.apache.directory.server.core.interceptor.BaseInterceptor;
043    import org.apache.directory.server.core.interceptor.Interceptor;
044    import org.apache.directory.server.core.interceptor.NextInterceptor;
045    import org.apache.directory.server.core.interceptor.context.AddOperationContext;
046    import org.apache.directory.server.core.interceptor.context.LookupOperationContext;
047    import org.apache.directory.server.core.interceptor.context.ModifyOperationContext;
048    import org.apache.directory.server.core.normalization.NormalizationInterceptor;
049    import org.apache.directory.server.core.operational.OperationalAttributeInterceptor;
050    import org.apache.directory.server.core.referral.ReferralInterceptor;
051    import org.apache.directory.server.core.schema.SchemaInterceptor;
052    import org.apache.directory.server.core.subtree.SubentryInterceptor;
053    import org.apache.directory.server.core.trigger.TriggerInterceptor;
054    import org.apache.directory.server.i18n.I18n;
055    import org.apache.directory.server.kerberos.shared.crypto.encryption.EncryptionType;
056    import org.apache.directory.server.kerberos.shared.crypto.encryption.KerberosKeyFactory;
057    import org.apache.directory.server.kerberos.shared.crypto.encryption.RandomKeyFactory;
058    import org.apache.directory.server.kerberos.shared.exceptions.KerberosException;
059    import org.apache.directory.server.kerberos.shared.io.encoder.EncryptionKeyEncoder;
060    import org.apache.directory.server.kerberos.shared.messages.value.EncryptionKey;
061    import org.apache.directory.server.kerberos.shared.store.KerberosAttribute;
062    import org.apache.directory.shared.ldap.constants.SchemaConstants;
063    import org.apache.directory.shared.ldap.entry.BinaryValue;
064    import org.apache.directory.shared.ldap.entry.StringValue;
065    import org.apache.directory.shared.ldap.entry.DefaultServerAttribute;
066    import org.apache.directory.shared.ldap.entry.EntryAttribute;
067    import org.apache.directory.shared.ldap.entry.Modification;
068    import org.apache.directory.shared.ldap.entry.ModificationOperation;
069    import org.apache.directory.shared.ldap.entry.ServerEntry;
070    import org.apache.directory.shared.ldap.entry.ServerModification;
071    import org.apache.directory.shared.ldap.entry.Value;
072    import org.apache.directory.shared.ldap.exception.LdapAuthenticationException;
073    import org.apache.directory.shared.ldap.name.DN;
074    import org.apache.directory.shared.ldap.schema.SchemaManager;
075    import org.apache.directory.shared.ldap.util.StringTools;
076    import org.slf4j.Logger;
077    import org.slf4j.LoggerFactory;
078    
079    
080    /**
081     * An {@link Interceptor} that creates symmetric Kerberos keys for users.  When a
082     * 'userPassword' is added or modified, the 'userPassword' and 'krb5PrincipalName'
083     * are used to derive Kerberos keys.  If the 'userPassword' is the special keyword
084     * 'randomKey', a random key is generated and used as the Kerberos key.
085     * 
086     * @org.apache.xbean.XBean
087     *
088     * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
089     * @version $Rev$, $Date$
090     */
091    public class KeyDerivationInterceptor extends BaseInterceptor
092    {
093        /** The log for this class. */
094        private static final Logger log = LoggerFactory.getLogger( KeyDerivationInterceptor.class );
095    
096        /** The service name. */
097        public static final String NAME = "keyDerivationService";
098    
099        /**
100         * Define the interceptors to bypass upon user lookup.
101         */
102        private static final Collection<String> USERLOOKUP_BYPASS;
103        static
104        {
105            Set<String> c = new HashSet<String>();
106            c.add( NormalizationInterceptor.class.getName() );
107            c.add( AuthenticationInterceptor.class.getName() );
108            c.add( ReferralInterceptor.class.getName() );
109            c.add( AciAuthorizationInterceptor.class.getName() );
110            c.add( DefaultAuthorizationInterceptor.class.getName() );
111            c.add( ExceptionInterceptor.class.getName() );
112            c.add( OperationalAttributeInterceptor.class.getName() );
113            c.add( SchemaInterceptor.class.getName() );
114            c.add( SubentryInterceptor.class.getName() );
115            c.add( CollectiveAttributeInterceptor.class.getName() );
116            c.add( EventInterceptor.class.getName() );
117            c.add( TriggerInterceptor.class.getName() );
118            USERLOOKUP_BYPASS = Collections.unmodifiableCollection( c );
119        }
120    
121    
122        /**
123         * Intercept the addition of the 'userPassword' and 'krb5PrincipalName' attributes.  Use the 'userPassword'
124         * and 'krb5PrincipalName' attributes to derive Kerberos keys for the principal.  If the 'userPassword' is
125         * the special keyword 'randomKey', set random keys for the principal.  Set the key version number (kvno)
126         * to '0'.
127         */
128        public void add( NextInterceptor next, AddOperationContext addContext ) throws Exception
129        {
130            DN normName = addContext.getDn();
131    
132            ServerEntry entry = addContext.getEntry();
133    
134            if ( ( entry.get( SchemaConstants.USER_PASSWORD_AT ) != null ) && 
135                ( entry.get( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT ) != null ) )
136            {
137                log.debug( "Adding the entry '{}' for DN '{}'.", entry, normName.getName() );
138    
139                BinaryValue userPassword = (BinaryValue)entry.get( SchemaConstants.USER_PASSWORD_AT ).get();
140                String strUserPassword = userPassword.getString();
141    
142                if ( log.isDebugEnabled() )
143                {
144                    StringBuffer sb = new StringBuffer();
145                    sb.append( "'" + strUserPassword + "' ( " );
146                    sb.append( userPassword );
147                    sb.append( " )" );
148                    log.debug( "Adding Attribute id : 'userPassword',  Values : [ {} ]", sb.toString() );
149                }
150    
151                Value<?> principalNameValue = entry.get( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT ).get();
152                
153                String principalName = principalNameValue.getString();
154    
155                log.debug( "Got principal '{}' with userPassword '{}'.", principalName, strUserPassword );
156    
157                Map<EncryptionType, EncryptionKey> keys = generateKeys( principalName, strUserPassword );
158    
159                entry.put( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT, principalName );
160                entry.put( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT, "0" );
161    
162                entry.put( getKeyAttribute( addContext.getSession().getDirectoryService().getSchemaManager(), keys ) );
163    
164                log.debug( "Adding modified entry '{}' for DN '{}'.", entry, normName
165                    .getName() );
166            }
167    
168            next.add( addContext );
169        }
170    
171    
172        /**
173         * Intercept the modification of the 'userPassword' attribute.  Perform a lookup to check for an
174         * existing principal name and key version number (kvno).  If a 'krb5PrincipalName' is not in
175         * the modify request, attempt to use an existing 'krb5PrincipalName' attribute.  If a kvno
176         * exists, increment the kvno; otherwise, set the kvno to '0'.
177         * 
178         * If both a 'userPassword' and 'krb5PrincipalName' can be found, use the 'userPassword' and
179         * 'krb5PrincipalName' attributes to derive Kerberos keys for the principal.
180         * 
181         * If the 'userPassword' is the special keyword 'randomKey', set random keys for the principal.
182         */
183        public void modify( NextInterceptor next, ModifyOperationContext modContext ) throws Exception
184        {
185            ModifySubContext subContext = new ModifySubContext();
186    
187            detectPasswordModification( modContext, subContext );
188    
189            if ( subContext.getUserPassword() != null )
190            {
191                lookupPrincipalAttributes( modContext, subContext );
192            }
193    
194            if ( subContext.isPrincipal() && subContext.hasValues() )
195            {
196                deriveKeys( modContext, subContext );
197            }
198    
199            next.modify( modContext );
200        }
201    
202    
203        /**
204         * Detect password modification by checking the modify request for the 'userPassword'.  Additionally,
205         * check to see if a 'krb5PrincipalName' was provided.
206         *
207         * @param modContext
208         * @param subContext
209         * @throws NamingException
210         */
211        void detectPasswordModification( ModifyOperationContext modContext, ModifySubContext subContext )
212            throws Exception
213        {
214            List<Modification> mods = modContext.getModItems();
215    
216            String operation = null;
217    
218            // Loop over attributes being modified to pick out 'userPassword' and 'krb5PrincipalName'.
219            for ( Modification mod:mods )
220            {
221                if ( log.isDebugEnabled() )
222                {
223                    switch ( mod.getOperation() )
224                    {
225                        case ADD_ATTRIBUTE:
226                            operation = "Adding";
227                            break;
228                            
229                        case REMOVE_ATTRIBUTE:
230                            operation = "Removing";
231                            break;
232                            
233                        case REPLACE_ATTRIBUTE:
234                            operation = "Replacing";
235                            break;
236                    }
237                }
238    
239                EntryAttribute attr = mod.getAttribute();
240    
241                if ( attr.instanceOf( SchemaConstants.USER_PASSWORD_AT ) )
242                {
243                    Object firstValue = attr.get();
244                    String password = null;
245    
246                    if ( firstValue instanceof StringValue )
247                    {
248                        password = ((StringValue)firstValue).getString();
249                        log.debug( "{} Attribute id : 'userPassword',  Values : [ '{}' ]", operation, password );
250                    }
251                    else if ( firstValue instanceof BinaryValue )
252                    {
253                        password = ((BinaryValue)firstValue).getString();
254    
255                        if ( log.isDebugEnabled() )
256                        {
257                            StringBuffer sb = new StringBuffer();
258                            sb.append( "'" + password + "' ( " );
259                            sb.append( StringTools.dumpBytes( ((BinaryValue)firstValue).getBytes() ).trim() );
260                            sb.append( " )" );
261                            log.debug( "{} Attribute id : 'userPassword',  Values : [ {} ]", operation, sb.toString() );
262                        }
263                    }
264    
265                    subContext.setUserPassword( password );
266                    log.debug( "Got userPassword '{}'.", subContext.getUserPassword() );
267                }
268    
269                if ( attr.instanceOf( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT ) )
270                {
271                    subContext.setPrincipalName( attr.getString() );
272                    log.debug( "Got principal '{}'.", subContext.getPrincipalName() );
273                }
274            }
275        }
276    
277    
278        /**
279         * Lookup the principal's attributes that are relevant to executing key derivation.
280         *
281         * @param modContext
282         * @param subContext
283         * @throws NamingException
284         */
285        void lookupPrincipalAttributes( ModifyOperationContext modContext, ModifySubContext subContext )
286            throws Exception
287        {
288            DN principalDn = modContext.getDn();
289    
290            LookupOperationContext lookupContext = modContext.newLookupContext( principalDn );
291            lookupContext.setByPassed( USERLOOKUP_BYPASS );
292            lookupContext.setAttrsId( new String[] 
293            { 
294                SchemaConstants.OBJECT_CLASS_AT, 
295                KerberosAttribute.KRB5_PRINCIPAL_NAME_AT, 
296                KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT 
297            } );
298            
299            ClonedServerEntry userEntry = modContext.lookup( lookupContext );
300    
301            if ( userEntry == null )
302            {
303                throw new LdapAuthenticationException( I18n.err( I18n.ERR_512, principalDn ) );
304            }
305    
306            EntryAttribute objectClass = userEntry.getOriginalEntry().get( SchemaConstants.OBJECT_CLASS_AT );
307            
308            if ( !objectClass.contains( SchemaConstants.KRB5_PRINCIPAL_OC ) )
309            {
310                return;
311            }
312            else
313            {
314                subContext.isPrincipal( true );
315                log.debug( "DN {} is a Kerberos principal.  Will attempt key derivation.", principalDn.getName() );
316            }
317    
318            if ( subContext.getPrincipalName() == null )
319            {
320                EntryAttribute principalAttribute = userEntry.getOriginalEntry().get( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT );
321                String principalName = principalAttribute.getString();
322                subContext.setPrincipalName( principalName );
323                log.debug( "Found principal '{}' from lookup.", principalName );
324            }
325    
326            EntryAttribute keyVersionNumberAttr = userEntry.getOriginalEntry().get( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT );
327    
328            if ( keyVersionNumberAttr == null )
329            {
330                subContext.setNewKeyVersionNumber( 0 );
331                log.debug( "Key version number was null, setting to 0." );
332            }
333            else
334            {
335                int oldKeyVersionNumber = Integer.valueOf( keyVersionNumberAttr.getString() );
336                int newKeyVersionNumber = oldKeyVersionNumber + 1;
337                subContext.setNewKeyVersionNumber( newKeyVersionNumber );
338                log.debug( "Found key version number '{}', setting to '{}'.", oldKeyVersionNumber, newKeyVersionNumber );
339            }
340        }
341    
342    
343        /**
344         * Use the 'userPassword' and 'krb5PrincipalName' attributes to derive Kerberos keys for the principal.
345         * 
346         * If the 'userPassword' is the special keyword 'randomKey', set random keys for the principal.
347         *
348         * @param modContext
349         * @param subContext
350         */
351        void deriveKeys( ModifyOperationContext modContext, ModifySubContext subContext ) throws Exception
352        {
353            List<Modification> mods = modContext.getModItems();
354    
355            String principalName = subContext.getPrincipalName();
356            String userPassword = subContext.getUserPassword();
357            int kvno = subContext.getNewKeyVersionNumber();
358    
359            log.debug( "Got principal '{}' with userPassword '{}'.", principalName, userPassword );
360    
361            Map<EncryptionType, EncryptionKey> keys = generateKeys( principalName, userPassword );
362    
363            List<Modification> newModsList = new ArrayList<Modification>();
364    
365            // Make sure we preserve any other modification items.
366            for ( Modification mod:mods )
367            {
368                newModsList.add( mod );
369            }
370            
371            SchemaManager schemaManager = modContext.getSession()
372                .getDirectoryService().getSchemaManager();
373    
374            // Add our modification items.
375            newModsList.add( 
376                new ServerModification( 
377                    ModificationOperation.REPLACE_ATTRIBUTE, 
378                    new DefaultServerAttribute(
379                        KerberosAttribute.KRB5_PRINCIPAL_NAME_AT, 
380                        schemaManager.lookupAttributeTypeRegistry( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT ),
381                        principalName ) ) );
382            newModsList.add( 
383                new ServerModification( 
384                    ModificationOperation.REPLACE_ATTRIBUTE, 
385                    new DefaultServerAttribute(
386                        KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT, 
387                        schemaManager.lookupAttributeTypeRegistry( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT ),
388                        Integer.toString( kvno ) ) ) );
389            
390            EntryAttribute attribute = getKeyAttribute( modContext.getSession()
391                .getDirectoryService().getSchemaManager(), keys );
392            newModsList.add( new ServerModification( ModificationOperation.REPLACE_ATTRIBUTE, attribute ) );
393    
394            modContext.setModItems( newModsList );
395        }
396    
397    
398        private EntryAttribute getKeyAttribute( SchemaManager schemaManager, Map<EncryptionType, EncryptionKey> keys ) throws Exception
399        {
400            EntryAttribute keyAttribute = 
401                new DefaultServerAttribute( KerberosAttribute.KRB5_KEY_AT, 
402                    schemaManager.lookupAttributeTypeRegistry( KerberosAttribute.KRB5_KEY_AT ) );
403    
404            Iterator<EncryptionKey> it = keys.values().iterator();
405    
406            while ( it.hasNext() )
407            {
408                try
409                {
410                    keyAttribute.add( EncryptionKeyEncoder.encode( it.next() ) );
411                }
412                catch ( IOException ioe )
413                {
414                    log.error( I18n.err( I18n.ERR_122 ), ioe );
415                }
416            }
417    
418            return keyAttribute;
419        }
420    
421    
422        private Map<EncryptionType, EncryptionKey> generateKeys( String principalName, String userPassword )
423        {
424            if ( userPassword.equalsIgnoreCase( "randomKey" ) )
425            {
426                // Generate random key.
427                try
428                {
429                    return RandomKeyFactory.getRandomKeys();
430                }
431                catch ( KerberosException ke )
432                {
433                    log.debug( ke.getLocalizedMessage(), ke );
434                    return null;
435                }
436            }
437            else
438            {
439                // Derive key based on password and principal name.
440                return KerberosKeyFactory.getKerberosKeys( principalName, userPassword );
441            }
442        }
443    
444        class ModifySubContext
445        {
446            private boolean isPrincipal = false;
447            private String principalName;
448            private String userPassword;
449            private int newKeyVersionNumber = -1;
450    
451    
452            boolean isPrincipal()
453            {
454                return isPrincipal;
455            }
456    
457    
458            void isPrincipal( boolean isPrincipal )
459            {
460                this.isPrincipal = isPrincipal;
461            }
462    
463    
464            String getPrincipalName()
465            {
466                return principalName;
467            }
468    
469    
470            void setPrincipalName( String principalName )
471            {
472                this.principalName = principalName;
473            }
474    
475    
476            String getUserPassword()
477            {
478                return userPassword;
479            }
480    
481    
482            void setUserPassword( String userPassword )
483            {
484                this.userPassword = userPassword;
485            }
486    
487    
488            int getNewKeyVersionNumber()
489            {
490                return newKeyVersionNumber;
491            }
492    
493    
494            void setNewKeyVersionNumber( int newKeyVersionNumber )
495            {
496                this.newKeyVersionNumber = newKeyVersionNumber;
497            }
498    
499    
500            boolean hasValues()
501            {
502                return userPassword != null && principalName != null && newKeyVersionNumber > -1;
503            }
504        }
505    }