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.server.core.kerberos;
21  
22  
23  import java.io.IOException;
24  import java.util.ArrayList;
25  import java.util.Collection;
26  import java.util.Collections;
27  import java.util.HashSet;
28  import java.util.Iterator;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.Set;
32  
33  import org.apache.directory.server.core.interceptor.BaseInterceptor;
34  import org.apache.directory.server.core.interceptor.Interceptor;
35  import org.apache.directory.server.core.interceptor.NextInterceptor;
36  import org.apache.directory.server.core.interceptor.context.AddOperationContext;
37  import org.apache.directory.server.core.interceptor.context.LookupOperationContext;
38  import org.apache.directory.server.core.interceptor.context.ModifyOperationContext;
39  import org.apache.directory.server.core.normalization.NormalizationInterceptor;
40  import org.apache.directory.server.core.authn.AuthenticationInterceptor;
41  import org.apache.directory.server.core.authz.AciAuthorizationInterceptor;
42  import org.apache.directory.server.core.authz.DefaultAuthorizationInterceptor;
43  import org.apache.directory.server.core.exception.ExceptionInterceptor;
44  import org.apache.directory.server.core.operational.OperationalAttributeInterceptor;
45  import org.apache.directory.server.core.schema.SchemaInterceptor;
46  import org.apache.directory.server.core.subtree.SubentryInterceptor;
47  import org.apache.directory.server.core.collective.CollectiveAttributeInterceptor;
48  import org.apache.directory.server.core.entry.ClonedServerEntry;
49  import org.apache.directory.server.core.entry.DefaultServerAttribute;
50  import org.apache.directory.server.core.entry.ServerAttribute;
51  import org.apache.directory.server.core.entry.ServerBinaryValue;
52  import org.apache.directory.server.core.entry.ServerEntry;
53  import org.apache.directory.server.core.entry.ServerModification;
54  import org.apache.directory.server.core.entry.ServerStringValue;
55  import org.apache.directory.server.core.event.EventInterceptor;
56  import org.apache.directory.server.core.trigger.TriggerInterceptor;
57  import org.apache.directory.server.kerberos.shared.crypto.encryption.EncryptionType;
58  import org.apache.directory.server.kerberos.shared.crypto.encryption.KerberosKeyFactory;
59  import org.apache.directory.server.kerberos.shared.crypto.encryption.RandomKeyFactory;
60  import org.apache.directory.server.kerberos.shared.exceptions.KerberosException;
61  import org.apache.directory.server.kerberos.shared.io.encoder.EncryptionKeyEncoder;
62  import org.apache.directory.server.kerberos.shared.messages.value.EncryptionKey;
63  import org.apache.directory.server.kerberos.shared.store.KerberosAttribute;
64  import org.apache.directory.server.schema.registries.AttributeTypeRegistry;
65  import org.apache.directory.server.schema.registries.Registries;
66  import org.apache.directory.shared.ldap.constants.SchemaConstants;
67  import org.apache.directory.shared.ldap.entry.EntryAttribute;
68  import org.apache.directory.shared.ldap.entry.Modification;
69  import org.apache.directory.shared.ldap.entry.ModificationOperation;
70  import org.apache.directory.shared.ldap.entry.Value;
71  import org.apache.directory.shared.ldap.exception.LdapAuthenticationException;
72  import org.apache.directory.shared.ldap.name.LdapDN;
73  import org.apache.directory.shared.ldap.util.StringTools;
74  import org.slf4j.Logger;
75  import org.slf4j.LoggerFactory;
76  
77  
78  /**
79   * An {@link Interceptor} that creates symmetric Kerberos keys for users.  When a
80   * 'userPassword' is added or modified, the 'userPassword' and 'krb5PrincipalName'
81   * are used to derive Kerberos keys.  If the 'userPassword' is the special keyword
82   * 'randomKey', a random key is generated and used as the Kerberos key.
83   * 
84   * @org.apache.xbean.XBean
85   *
86   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
87   * @version $Rev$, $Date$
88   */
89  public class KeyDerivationInterceptor extends BaseInterceptor
90  {
91      /** The log for this class. */
92      private static final Logger log = LoggerFactory.getLogger( KeyDerivationInterceptor.class );
93  
94      /** The service name. */
95      public static final String NAME = "keyDerivationService";
96  
97      /**
98       * Define the interceptors to bypass upon user lookup.
99       */
100     private static final Collection<String> USERLOOKUP_BYPASS;
101     static
102     {
103         Set<String> c = new HashSet<String>();
104         c.add( NormalizationInterceptor.class.getName() );
105         c.add( AuthenticationInterceptor.class.getName() );
106         c.add( AciAuthorizationInterceptor.class.getName() );
107         c.add( DefaultAuthorizationInterceptor.class.getName() );
108         c.add( ExceptionInterceptor.class.getName() );
109         c.add( OperationalAttributeInterceptor.class.getName() );
110         c.add( SchemaInterceptor.class.getName() );
111         c.add( SubentryInterceptor.class.getName() );
112         c.add( CollectiveAttributeInterceptor.class.getName() );
113         c.add( EventInterceptor.class.getName() );
114         c.add( TriggerInterceptor.class.getName() );
115         USERLOOKUP_BYPASS = Collections.unmodifiableCollection( c );
116     }
117 
118 
119     /**
120      * Intercept the addition of the 'userPassword' and 'krb5PrincipalName' attributes.  Use the 'userPassword'
121      * and 'krb5PrincipalName' attributes to derive Kerberos keys for the principal.  If the 'userPassword' is
122      * the special keyword 'randomKey', set random keys for the principal.  Set the key version number (kvno)
123      * to '0'.
124      */
125     public void add( NextInterceptor next, AddOperationContext addContext ) throws Exception
126     {
127         LdapDN normName = addContext.getDn();
128 
129         ServerEntry entry = addContext.getEntry();
130 
131         if ( ( entry.get( SchemaConstants.USER_PASSWORD_AT ) != null ) && 
132             ( entry.get( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT ) != null ) )
133         {
134             log.debug( "Adding the entry '{}' for DN '{}'.", entry, normName.getUpName() );
135 
136             ServerBinaryValue userPassword = (ServerBinaryValue)entry.get( SchemaConstants.USER_PASSWORD_AT ).get();
137             String strUserPassword = StringTools.utf8ToString( userPassword.get() );
138 
139             if ( log.isDebugEnabled() )
140             {
141                 StringBuffer sb = new StringBuffer();
142                 sb.append( "'" + strUserPassword + "' ( " );
143                 sb.append( userPassword );
144                 sb.append( " )" );
145                 log.debug( "Adding Attribute id : 'userPassword',  Values : [ {} ]", sb.toString() );
146             }
147 
148             Value<?> principalNameValue = entry.get( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT ).get();
149             
150             String principalName = (String)principalNameValue.get();
151 
152             log.debug( "Got principal '{}' with userPassword '{}'.", principalName, strUserPassword );
153 
154             Map<EncryptionType, EncryptionKey> keys = generateKeys( principalName, strUserPassword );
155 
156             entry.put( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT, principalName );
157             entry.put( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT, "0" );
158 
159             entry.put( getKeyAttribute( addContext.getSession().getDirectoryService().getRegistries(), keys ) );
160 
161             log.debug( "Adding modified entry '{}' for DN '{}'.", entry, normName
162                 .getUpName() );
163         }
164 
165         next.add( addContext );
166     }
167 
168 
169     /**
170      * Intercept the modification of the 'userPassword' attribute.  Perform a lookup to check for an
171      * existing principal name and key version number (kvno).  If a 'krb5PrincipalName' is not in
172      * the modify request, attempt to use an existing 'krb5PrincipalName' attribute.  If a kvno
173      * exists, increment the kvno; otherwise, set the kvno to '0'.
174      * 
175      * If both a 'userPassword' and 'krb5PrincipalName' can be found, use the 'userPassword' and
176      * 'krb5PrincipalName' attributes to derive Kerberos keys for the principal.
177      * 
178      * If the 'userPassword' is the special keyword 'randomKey', set random keys for the principal.
179      */
180     public void modify( NextInterceptor next, ModifyOperationContext modContext ) throws Exception
181     {
182         ModifySubContext subContext = new ModifySubContext();
183 
184         detectPasswordModification( modContext, subContext );
185 
186         if ( subContext.getUserPassword() != null )
187         {
188             lookupPrincipalAttributes( modContext, subContext );
189         }
190 
191         if ( subContext.isPrincipal() && subContext.hasValues() )
192         {
193             deriveKeys( modContext, subContext );
194         }
195 
196         next.modify( modContext );
197     }
198 
199 
200     /**
201      * Detect password modification by checking the modify request for the 'userPassword'.  Additionally,
202      * check to see if a 'krb5PrincipalName' was provided.
203      *
204      * @param modContext
205      * @param subContext
206      * @throws NamingException
207      */
208     void detectPasswordModification( ModifyOperationContext modContext, ModifySubContext subContext )
209         throws Exception
210     {
211         List<Modification> mods = modContext.getModItems();
212 
213         String operation = null;
214 
215         // Loop over attributes being modified to pick out 'userPassword' and 'krb5PrincipalName'.
216         for ( Modification mod:mods )
217         {
218             if ( log.isDebugEnabled() )
219             {
220                 switch ( mod.getOperation() )
221                 {
222                     case ADD_ATTRIBUTE:
223                         operation = "Adding";
224                         break;
225                         
226                     case REMOVE_ATTRIBUTE:
227                         operation = "Removing";
228                         break;
229                         
230                     case REPLACE_ATTRIBUTE:
231                         operation = "Replacing";
232                         break;
233                 }
234             }
235 
236             ServerAttribute attr = (ServerAttribute)mod.getAttribute();
237 
238             if ( attr.instanceOf( SchemaConstants.USER_PASSWORD_AT ) )
239             {
240                 Object firstValue = attr.get();
241                 String password = null;
242 
243                 if ( firstValue instanceof ServerStringValue )
244                 {
245                     password = ((ServerStringValue)firstValue).get();
246                     log.debug( "{} Attribute id : 'userPassword',  Values : [ '{}' ]", operation, password );
247                 }
248                 else if ( firstValue instanceof ServerBinaryValue )
249                 {
250                     password = StringTools.utf8ToString( ((ServerBinaryValue)firstValue).get() );
251 
252                     if ( log.isDebugEnabled() )
253                     {
254                         StringBuffer sb = new StringBuffer();
255                         sb.append( "'" + password + "' ( " );
256                         sb.append( StringTools.dumpBytes( ((ServerBinaryValue)firstValue).get() ).trim() );
257                         sb.append( " )" );
258                         log.debug( "{} Attribute id : 'userPassword',  Values : [ {} ]", operation, sb.toString() );
259                     }
260                 }
261 
262                 subContext.setUserPassword( password );
263                 log.debug( "Got userPassword '{}'.", subContext.getUserPassword() );
264             }
265 
266             if ( attr.instanceOf( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT ) )
267             {
268                 subContext.setPrincipalName( attr.getString() );
269                 log.debug( "Got principal '{}'.", subContext.getPrincipalName() );
270             }
271         }
272     }
273 
274 
275     /**
276      * Lookup the principal's attributes that are relevant to executing key derivation.
277      *
278      * @param modContext
279      * @param subContext
280      * @throws NamingException
281      */
282     void lookupPrincipalAttributes( ModifyOperationContext modContext, ModifySubContext subContext )
283         throws Exception
284     {
285         LdapDN principalDn = modContext.getDn();
286 
287         LookupOperationContext lookupContext = modContext.newLookupContext( principalDn );
288         lookupContext.setByPassed( USERLOOKUP_BYPASS );
289         lookupContext.setAttrsId( new String[] 
290         { 
291             SchemaConstants.OBJECT_CLASS_AT, 
292             KerberosAttribute.KRB5_PRINCIPAL_NAME_AT, 
293             KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT 
294         } );
295         
296         ClonedServerEntry userEntry = modContext.lookup( lookupContext );
297 
298         if ( userEntry == null )
299         {
300             throw new LdapAuthenticationException( "Failed to authenticate user '" + principalDn + "'." );
301         }
302 
303         EntryAttribute objectClass = userEntry.getOriginalEntry().get( SchemaConstants.OBJECT_CLASS_AT );
304         
305         if ( !objectClass.contains( SchemaConstants.KRB5_PRINCIPAL_OC ) )
306         {
307             return;
308         }
309         else
310         {
311             subContext.isPrincipal( true );
312             log.debug( "DN {} is a Kerberos principal.  Will attempt key derivation.", principalDn.getUpName() );
313         }
314 
315         if ( subContext.getPrincipalName() == null )
316         {
317             EntryAttribute principalAttribute = userEntry.getOriginalEntry().get( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT );
318             String principalName = principalAttribute.getString();
319             subContext.setPrincipalName( principalName );
320             log.debug( "Found principal '{}' from lookup.", principalName );
321         }
322 
323         EntryAttribute keyVersionNumberAttr = userEntry.getOriginalEntry().get( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT );
324 
325         if ( keyVersionNumberAttr == null )
326         {
327             subContext.setNewKeyVersionNumber( 0 );
328             log.debug( "Key version number was null, setting to 0." );
329         }
330         else
331         {
332             int oldKeyVersionNumber = Integer.valueOf( keyVersionNumberAttr.getString() );
333             int newKeyVersionNumber = oldKeyVersionNumber + 1;
334             subContext.setNewKeyVersionNumber( newKeyVersionNumber );
335             log.debug( "Found key version number '{}', setting to '{}'.", oldKeyVersionNumber, newKeyVersionNumber );
336         }
337     }
338 
339 
340     /**
341      * Use the 'userPassword' and 'krb5PrincipalName' attributes to derive Kerberos keys for the principal.
342      * 
343      * If the 'userPassword' is the special keyword 'randomKey', set random keys for the principal.
344      *
345      * @param modContext
346      * @param subContext
347      */
348     void deriveKeys( ModifyOperationContext modContext, ModifySubContext subContext ) throws Exception
349     {
350         List<Modification> mods = modContext.getModItems();
351 
352         String principalName = subContext.getPrincipalName();
353         String userPassword = subContext.getUserPassword();
354         int kvno = subContext.getNewKeyVersionNumber();
355 
356         log.debug( "Got principal '{}' with userPassword '{}'.", principalName, userPassword );
357 
358         Map<EncryptionType, EncryptionKey> keys = generateKeys( principalName, userPassword );
359 
360         List<Modification> newModsList = new ArrayList<Modification>();
361 
362         // Make sure we preserve any other modification items.
363         for ( Modification mod:mods )
364         {
365             newModsList.add( mod );
366         }
367         
368         AttributeTypeRegistry atRegistry = modContext.getSession()
369             .getDirectoryService().getRegistries().getAttributeTypeRegistry();
370 
371         // Add our modification items.
372         newModsList.add( 
373             new ServerModification( 
374                 ModificationOperation.REPLACE_ATTRIBUTE, 
375                 new DefaultServerAttribute(
376                     KerberosAttribute.KRB5_PRINCIPAL_NAME_AT, 
377                     atRegistry.lookup( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT ),
378                     principalName ) ) );
379         newModsList.add( 
380             new ServerModification( 
381                 ModificationOperation.REPLACE_ATTRIBUTE, 
382                 new DefaultServerAttribute(
383                     KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT, 
384                     atRegistry.lookup( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT ),
385                     Integer.toString( kvno ) ) ) );
386         
387         ServerAttribute attribute = getKeyAttribute( modContext.getSession()
388             .getDirectoryService().getRegistries(), keys );
389         newModsList.add( new ServerModification( ModificationOperation.REPLACE_ATTRIBUTE, attribute ) );
390 
391         modContext.setModItems( newModsList );
392     }
393 
394 
395     private ServerAttribute getKeyAttribute( Registries registries, Map<EncryptionType, EncryptionKey> keys ) throws Exception
396     {
397         ServerAttribute keyAttribute = 
398             new DefaultServerAttribute( KerberosAttribute.KRB5_KEY_AT, 
399                 registries.getAttributeTypeRegistry().lookup( KerberosAttribute.KRB5_KEY_AT ) );
400 
401         Iterator<EncryptionKey> it = keys.values().iterator();
402 
403         while ( it.hasNext() )
404         {
405             try
406             {
407                 keyAttribute.add( EncryptionKeyEncoder.encode( it.next() ) );
408             }
409             catch ( IOException ioe )
410             {
411                 log.error( "Error encoding EncryptionKey.", ioe );
412             }
413         }
414 
415         return keyAttribute;
416     }
417 
418 
419     private Map<EncryptionType, EncryptionKey> generateKeys( String principalName, String userPassword )
420     {
421         if ( userPassword.equalsIgnoreCase( "randomKey" ) )
422         {
423             // Generate random key.
424             try
425             {
426                 return RandomKeyFactory.getRandomKeys();
427             }
428             catch ( KerberosException ke )
429             {
430                 log.debug( ke.getMessage(), ke );
431                 return null;
432             }
433         }
434         else
435         {
436             // Derive key based on password and principal name.
437             return KerberosKeyFactory.getKerberosKeys( principalName, userPassword );
438         }
439     }
440 
441     class ModifySubContext
442     {
443         private boolean isPrincipal = false;
444         private String principalName;
445         private String userPassword;
446         private int newKeyVersionNumber = -1;
447 
448 
449         boolean isPrincipal()
450         {
451             return isPrincipal;
452         }
453 
454 
455         void isPrincipal( boolean isPrincipal )
456         {
457             this.isPrincipal = isPrincipal;
458         }
459 
460 
461         String getPrincipalName()
462         {
463             return principalName;
464         }
465 
466 
467         void setPrincipalName( String principalName )
468         {
469             this.principalName = principalName;
470         }
471 
472 
473         String getUserPassword()
474         {
475             return userPassword;
476         }
477 
478 
479         void setUserPassword( String userPassword )
480         {
481             this.userPassword = userPassword;
482         }
483 
484 
485         int getNewKeyVersionNumber()
486         {
487             return newKeyVersionNumber;
488         }
489 
490 
491         void setNewKeyVersionNumber( int newKeyVersionNumber )
492         {
493             this.newKeyVersionNumber = newKeyVersionNumber;
494         }
495 
496 
497         boolean hasValues()
498         {
499             return userPassword != null && principalName != null && newKeyVersionNumber > -1;
500         }
501     }
502 }