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.kerberos;
21  
22  
23  import org.apache.directory.server.core.DefaultDirectoryService;
24  import org.apache.directory.server.core.DirectoryService;
25  import org.apache.directory.server.core.entry.ServerEntry;
26  import org.apache.directory.server.core.integ.IntegrationUtils;
27  import org.apache.directory.server.core.integ.Level;
28  import org.apache.directory.server.core.integ.annotations.ApplyLdifs;
29  import org.apache.directory.server.core.integ.annotations.CleanupLevel;
30  import org.apache.directory.server.core.integ.annotations.Factory;
31  import org.apache.directory.server.core.interceptor.Interceptor;
32  import org.apache.directory.server.core.kerberos.KeyDerivationInterceptor;
33  import org.apache.directory.server.core.partition.Partition;
34  import org.apache.directory.server.xdbm.Index;
35  import org.apache.directory.server.core.partition.impl.btree.jdbm.JdbmIndex;
36  import org.apache.directory.server.core.partition.impl.btree.jdbm.JdbmPartition;
37  import org.apache.directory.server.integ.LdapServerFactory;
38  import static org.apache.directory.server.integ.ServerIntegrationUtils.getWiredContext;
39  import org.apache.directory.server.integ.SiRunner;
40  import org.apache.directory.server.kerberos.shared.crypto.encryption.EncryptionType;
41  import org.apache.directory.server.kerberos.shared.io.decoder.EncryptionKeyDecoder;
42  import org.apache.directory.server.kerberos.shared.messages.value.EncryptionKey;
43  import org.apache.directory.server.kerberos.shared.store.KerberosAttribute;
44  import org.apache.directory.server.ldap.LdapService;
45  import org.apache.directory.server.ldap.handlers.bind.MechanismHandler;
46  import org.apache.directory.server.ldap.handlers.bind.cramMD5.CramMd5MechanismHandler;
47  import org.apache.directory.server.ldap.handlers.bind.digestMD5.DigestMd5MechanismHandler;
48  import org.apache.directory.server.ldap.handlers.bind.gssapi.GssapiMechanismHandler;
49  import org.apache.directory.server.ldap.handlers.bind.ntlm.NtlmMechanismHandler;
50  import org.apache.directory.server.ldap.handlers.bind.plain.PlainMechanismHandler;
51  import org.apache.directory.server.ldap.handlers.extended.StoredProcedureExtendedOperationHandler;
52  import org.apache.directory.server.protocol.shared.SocketAcceptor;
53  import org.apache.directory.shared.ldap.constants.SupportedSaslMechanisms;
54  import org.apache.mina.util.AvailablePortFinder;
55  import org.junit.Before;
56  import org.junit.Test;
57  import org.junit.runner.RunWith;
58  import static org.junit.Assert.assertTrue;
59  import static org.junit.Assert.assertFalse;
60  import static org.junit.Assert.assertEquals;
61  
62  import javax.crypto.spec.DESKeySpec;
63  import javax.naming.Context;
64  import javax.naming.NamingException;
65  import javax.naming.directory.Attribute;
66  import javax.naming.directory.Attributes;
67  import javax.naming.directory.BasicAttribute;
68  import javax.naming.directory.BasicAttributes;
69  import javax.naming.directory.DirContext;
70  import javax.naming.directory.InitialDirContext;
71  import javax.naming.directory.ModificationItem;
72  
73  import java.io.IOException;
74  import java.security.InvalidKeyException;
75  import java.util.Arrays;
76  import java.util.HashMap;
77  import java.util.HashSet;
78  import java.util.Hashtable;
79  import java.util.List;
80  import java.util.Map;
81  import java.util.Set;
82  
83  
84  /**
85   * An test case for testing the {@link KeyDerivationInterceptor}'s
86   * ability to derive Kerberos symmetric keys based on userPassword and principal
87   * name and to generate random keys when the special keyword "randomKey" is used.
88   * 
89   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
90   * @version $Rev$, $Date$
91   */
92  @RunWith ( SiRunner.class ) 
93  @CleanupLevel ( Level.CLASS )
94  @Factory ( KeyDerivationServiceIT.Factory.class )
95  @ApplyLdifs( {
96      // Entry #0
97      "dn: dc=example,dc=com\n" +
98      "dc: example\n" +
99      "objectClass: top\n" +
100     "objectClass: domain\n\n"
101     }
102 )
103 public class KeyDerivationServiceIT 
104 {
105     private static final String RDN = "uid=hnelson,ou=users,dc=example,dc=com";
106 
107 
108     public static LdapService ldapService;
109 
110      
111      public static class Factory implements LdapServerFactory
112      {
113          public LdapService newInstance() throws Exception
114          {
115              DirectoryService service = new DefaultDirectoryService();
116              IntegrationUtils.doDelete( service.getWorkingDirectory() );
117              service.getChangeLog().setEnabled( true );
118              service.setAllowAnonymousAccess( false );
119              service.setShutdownHookEnabled( false );
120 
121              Set<Partition> partitions = new HashSet<Partition>();
122              JdbmPartition partition = new JdbmPartition();
123              partition.setId( "example" );
124              partition.setSuffix( "dc=example,dc=com" );
125 
126              Set<Index<?,ServerEntry>> indexedAttrs = new HashSet<Index<?,ServerEntry>>();
127              indexedAttrs.add( new JdbmIndex<String,ServerEntry>( "ou" ) );
128              indexedAttrs.add( new JdbmIndex<String,ServerEntry>( "dc" ) );
129              indexedAttrs.add( new JdbmIndex<String,ServerEntry>( "objectClass" ) );
130              partition.setIndexedAttributes( indexedAttrs );
131 
132              partitions.add( partition );
133              service.setPartitions( partitions );
134 
135              List<Interceptor> list = service.getInterceptors();
136              list.add( new KeyDerivationInterceptor() );
137              service.setInterceptors( list );
138              
139              // change the working directory to something that is unique
140              // on the system and somewhere either under target directory
141              // or somewhere in a temp area of the machine.
142 
143              LdapService ldapService = new LdapService();
144              ldapService.setDirectoryService( service );
145              ldapService.setSocketAcceptor( new SocketAcceptor( null ) );
146              ldapService.setIpPort( AvailablePortFinder.getNextAvailable( 1024 ) );
147              ldapService.setAllowAnonymousAccess( false );
148              ldapService.addExtendedOperationHandler( new StoredProcedureExtendedOperationHandler() );
149 
150              // Setup SASL Mechanisms
151              
152              Map<String, MechanismHandler> mechanismHandlerMap = new HashMap<String,MechanismHandler>();
153              mechanismHandlerMap.put( SupportedSaslMechanisms.PLAIN, new PlainMechanismHandler() );
154 
155              CramMd5MechanismHandler cramMd5MechanismHandler = new CramMd5MechanismHandler();
156              mechanismHandlerMap.put( SupportedSaslMechanisms.CRAM_MD5, cramMd5MechanismHandler );
157 
158              DigestMd5MechanismHandler digestMd5MechanismHandler = new DigestMd5MechanismHandler();
159              mechanismHandlerMap.put( SupportedSaslMechanisms.DIGEST_MD5, digestMd5MechanismHandler );
160 
161              GssapiMechanismHandler gssapiMechanismHandler = new GssapiMechanismHandler();
162              mechanismHandlerMap.put( SupportedSaslMechanisms.GSSAPI, gssapiMechanismHandler );
163 
164              NtlmMechanismHandler ntlmMechanismHandler = new NtlmMechanismHandler();
165              mechanismHandlerMap.put( SupportedSaslMechanisms.NTLM, ntlmMechanismHandler );
166              mechanismHandlerMap.put( SupportedSaslMechanisms.GSS_SPNEGO, ntlmMechanismHandler );
167 
168              ldapService.setSaslMechanismHandlers( mechanismHandlerMap );
169              ldapService.setSaslHost( "localhost" );
170              
171              return ldapService;
172          }
173      }
174      
175      
176     /**
177      * Set up a partition for EXAMPLE.COM, add the Key Derivation interceptor, enable
178      * the krb5kdc schema, and add a user principal to test authentication with.
179      */
180      @Before
181     public void setUp() throws Exception
182     {
183         DirContext schemaRoot = ( DirContext ) getWiredContext( ldapService ).lookup( "ou=schema" );
184 
185         // -------------------------------------------------------------------
186         // Enable the krb5kdc schema
187         // -------------------------------------------------------------------
188 
189         // check if krb5kdc is disabled
190         Attributes krb5kdcAttrs = schemaRoot.getAttributes( "cn=Krb5kdc" );
191         boolean isKrb5KdcDisabled = false;
192         
193         if ( krb5kdcAttrs.get( "m-disabled" ) != null )
194         {
195             isKrb5KdcDisabled = ( ( String ) krb5kdcAttrs.get( "m-disabled" ).get() ).equalsIgnoreCase( "TRUE" );
196         }
197 
198         // if krb5kdc is disabled then enable it
199         if ( isKrb5KdcDisabled )
200         {
201             Attribute disabled = new BasicAttribute( "m-disabled" );
202             ModificationItem[] mods = new ModificationItem[]
203                 { new ModificationItem( DirContext.REMOVE_ATTRIBUTE, disabled ) };
204             schemaRoot.modifyAttributes( "cn=Krb5kdc", mods );
205         }
206 
207         DirContext ctx = ( DirContext ) getWiredContext( ldapService ).lookup( "dc=example,dc=com" );
208         Attributes attrs = getOrgUnitAttributes( "users" );
209         DirContext users = ctx.createSubcontext( "ou=users", attrs );
210 
211         attrs = getPersonAttributes( "Nelson", "Horatio Nelson", "hnelson", "secret", "hnelson@EXAMPLE.COM" );
212         users.createSubcontext( "uid=hnelson", attrs );
213     }
214 
215 
216     /**
217      * Tests that the addition of an entry caused keys to be derived and added.
218      * 
219      * @throws NamingException failure to perform LDAP operations
220      * @throws IOException on network errors
221      */
222      @Test
223     public void testAddDerivedKeys() throws NamingException, IOException
224     {
225         Hashtable<String, String> env = new Hashtable<String, String>();
226         env.put( Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory" );
227         env.put( Context.PROVIDER_URL, "ldap://localhost:" + ldapService.getIpPort() );
228 
229         env.put( Context.SECURITY_AUTHENTICATION, "simple" );
230         env.put( Context.SECURITY_PRINCIPAL, "uid=hnelson,ou=users,dc=example,dc=com" );
231         env.put( Context.SECURITY_CREDENTIALS, "secret" );
232         env.put( "java.naming.ldap.attributes.binary", "krb5key" );
233 
234         DirContext ctx = new InitialDirContext( env );
235 
236         String[] attrIDs =
237             { "uid", "userPassword", KerberosAttribute.KRB5_KEY_AT, KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT };
238 
239         Attributes attributes = ctx.getAttributes( RDN, attrIDs );
240 
241         String uid = null;
242 
243         if ( attributes.get( "uid" ) != null )
244         {
245             uid = ( String ) attributes.get( "uid" ).get();
246         }
247 
248         assertEquals( uid, "hnelson" );
249 
250         byte[] userPassword = null;
251 
252         if ( attributes.get( "userPassword" ) != null )
253         {
254             userPassword = ( byte[] ) attributes.get( "userPassword" ).get();
255         }
256 
257         // Could be 4 or 5 depending on whether AES-256 is enabled or not.
258         assertTrue( "Number of keys", attributes.get( "krb5key" ).size() > 3 );
259 
260         byte[] testPasswordBytes =
261             { ( byte ) 0x73, ( byte ) 0x65, ( byte ) 0x63, ( byte ) 0x72, ( byte ) 0x65, ( byte ) 0x74 };
262         assertTrue( Arrays.equals( userPassword, testPasswordBytes ) );
263 
264         Attribute krb5key = attributes.get( KerberosAttribute.KRB5_KEY_AT );
265         Map<EncryptionType, EncryptionKey> map = reconstituteKeyMap( krb5key );
266         EncryptionKey encryptionKey = map.get( EncryptionType.DES_CBC_MD5 );
267 
268         byte[] testKeyBytes =
269             { ( byte ) 0xF4, ( byte ) 0xA7, ( byte ) 0x13, ( byte ) 0x64, ( byte ) 0x8A, ( byte ) 0x61, ( byte ) 0xCE,
270                 ( byte ) 0x5B };
271 
272         assertTrue( Arrays.equals( encryptionKey.getKeyValue(), testKeyBytes ) );
273         assertEquals( EncryptionType.DES_CBC_MD5, encryptionKey.getKeyType() );
274 
275         int keyVersionNumber = -1;
276 
277         if ( attributes.get( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT ) != null )
278         {
279             keyVersionNumber = Integer.valueOf( ( String ) attributes.get( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT ).get() );
280         }
281 
282         assertEquals( "Key version number", 0, keyVersionNumber );
283     }
284 
285 
286     /**
287      * Tests that the modification of an entry caused keys to be derived and modified.  The
288      * modify request contains both the 'userPassword' and the 'krb5PrincipalName'.
289      * 
290      * @throws NamingException failure to perform LDAP operations
291      * @throws IOException on network errors
292      */
293      @Test
294     public void testModifyDerivedKeys() throws NamingException, IOException
295     {
296         Hashtable<String, String> env = new Hashtable<String, String>();
297         env.put( Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory" );
298         env.put( Context.PROVIDER_URL, "ldap://localhost:" + ldapService.getIpPort() );
299 
300         env.put( Context.SECURITY_AUTHENTICATION, "simple" );
301         env.put( Context.SECURITY_PRINCIPAL, "uid=hnelson,ou=users,dc=example,dc=com" );
302         env.put( Context.SECURITY_CREDENTIALS, "secret" );
303         env.put( "java.naming.ldap.attributes.binary", "krb5key" );
304 
305         DirContext ctx = new InitialDirContext( env );
306 
307         String newPrincipalName = "hnelson@EXAMPLE.COM";
308         String newUserPassword = "secretsecret";
309 
310         // Modify password.
311         Attributes attributes = new BasicAttributes( true );
312         Attribute attr = new BasicAttribute( "userPassword", newUserPassword );
313         attributes.put( attr );
314         attr = new BasicAttribute( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT, newPrincipalName );
315         attributes.put( attr );
316 
317         DirContext person = ( DirContext ) ctx.lookup( RDN );
318         person.modifyAttributes( "", DirContext.REPLACE_ATTRIBUTE, attributes );
319 
320         // Read again from directory.
321         person = ( DirContext ) ctx.lookup( RDN );
322 
323         attributes = person.getAttributes( "" );
324 
325         byte[] userPassword = null;
326 
327         if ( attributes.get( "userPassword" ) != null )
328         {
329             userPassword = ( byte[] ) attributes.get( "userPassword" ).get();
330         }
331 
332         // Could be 4 or 5 depending on whether AES-256 is enabled or not.
333         assertTrue( "Number of keys", attributes.get( "krb5key" ).size() > 3 );
334 
335         byte[] testBytes =
336             { 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74 };
337         assertTrue( Arrays.equals( userPassword, testBytes ) );
338 
339         Attribute krb5key = attributes.get( "krb5key" );
340         Map<EncryptionType, EncryptionKey> map = reconstituteKeyMap( krb5key );
341         EncryptionKey encryptionKey = map.get( EncryptionType.DES_CBC_MD5 );
342 
343         byte[] testKeyBytes =
344             { ( byte ) 0x16, ( byte ) 0x4A, ( byte ) 0x6D, ( byte ) 0x89, ( byte ) 0x5D, ( byte ) 0x76, ( byte ) 0x0E,
345                 ( byte ) 0x23 };
346 
347         assertTrue( Arrays.equals( encryptionKey.getKeyValue(), testKeyBytes ) );
348         assertEquals( EncryptionType.DES_CBC_MD5, encryptionKey.getKeyType() );
349 
350         int keyVersionNumber = -1;
351 
352         if ( attributes.get( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT ) != null )
353         {
354             keyVersionNumber = Integer.valueOf( ( String ) attributes.get( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT ).get() );
355         }
356 
357         assertEquals( "Key version number", 1, keyVersionNumber );
358 
359         newUserPassword = "secretsecretsecret";
360 
361         // Modify password.
362         attributes = new BasicAttributes( true );
363         attr = new BasicAttribute( "userPassword", newUserPassword );
364         attributes.put( attr );
365         attr = new BasicAttribute( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT, newPrincipalName );
366         attributes.put( attr );
367 
368         person = ( DirContext ) ctx.lookup( RDN );
369         person.modifyAttributes( "", DirContext.REPLACE_ATTRIBUTE, attributes );
370 
371         // Read again from directory.
372         person = ( DirContext ) ctx.lookup( RDN );
373 
374         attributes = person.getAttributes( "" );
375 
376         if ( attributes.get( "userPassword" ) != null )
377         {
378             userPassword = ( byte[] ) attributes.get( "userPassword" ).get();
379         }
380 
381         assertEquals( "password length", 18, userPassword.length );
382 
383         if ( attributes.get( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT ) != null )
384         {
385             keyVersionNumber = Integer.valueOf( ( String ) attributes.get( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT ).get() );
386         }
387 
388         assertEquals( "Key version number", 2, keyVersionNumber );
389 
390         newUserPassword = "secretsecretsecretsecret";
391 
392         // Modify password.
393         attributes = new BasicAttributes( true );
394         attr = new BasicAttribute( "userPassword", newUserPassword );
395         attributes.put( attr );
396         attr = new BasicAttribute( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT, newPrincipalName );
397         attributes.put( attr );
398 
399         person = ( DirContext ) ctx.lookup( RDN );
400         person.modifyAttributes( "", DirContext.REPLACE_ATTRIBUTE, attributes );
401 
402         // Read again from directory.
403         person = ( DirContext ) ctx.lookup( RDN );
404 
405         attributes = person.getAttributes( "" );
406 
407         if ( attributes.get( "userPassword" ) != null )
408         {
409             userPassword = ( byte[] ) attributes.get( "userPassword" ).get();
410         }
411 
412         assertEquals( "password length", 24, userPassword.length );
413 
414         if ( attributes.get( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT ) != null )
415         {
416             keyVersionNumber = Integer.valueOf( ( String ) attributes.get( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT ).get() );
417         }
418 
419         assertEquals( "Key version number", 3, keyVersionNumber );
420     }
421 
422 
423     /**
424      * Tests that the modification of an entry caused keys to be derived and modified.  The
425      * modify request contains only the 'userPassword'.  The 'krb5PrincipalName' is to be
426      * obtained from the initial add of the user principal entry.
427      * 
428      * @throws NamingException failure to perform LDAP operations
429      * @throws IOException on network errors
430      */
431      @Test
432     public void testModifyDerivedKeysWithoutPrincipalName() throws NamingException, IOException
433     {
434         Hashtable<String, String> env = new Hashtable<String, String>();
435         env.put( Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory" );
436         env.put( Context.PROVIDER_URL, "ldap://localhost:" + ldapService.getIpPort() );
437 
438         env.put( Context.SECURITY_AUTHENTICATION, "simple" );
439         env.put( Context.SECURITY_PRINCIPAL, "uid=hnelson,ou=users,dc=example,dc=com" );
440         env.put( Context.SECURITY_CREDENTIALS, "secret" );
441         env.put( "java.naming.ldap.attributes.binary", "krb5key" );
442 
443         DirContext ctx = new InitialDirContext( env );
444 
445         String newUserPassword = "secretsecret";
446 
447         // Modify password.
448         Attributes attributes = new BasicAttributes( true );
449         Attribute attr = new BasicAttribute( "userPassword", newUserPassword );
450         attributes.put( attr );
451 
452         DirContext person = ( DirContext ) ctx.lookup( RDN );
453         person.modifyAttributes( "", DirContext.REPLACE_ATTRIBUTE, attributes );
454 
455         // Read again from directory.
456         person = ( DirContext ) ctx.lookup( RDN );
457 
458         attributes = person.getAttributes( "" );
459 
460         byte[] userPassword = null;
461 
462         if ( attributes.get( "userPassword" ) != null )
463         {
464             userPassword = ( byte[] ) attributes.get( "userPassword" ).get();
465         }
466 
467         // Could be 4 or 5 depending on whether AES-256 is enabled or not.
468         assertTrue( "Number of keys", attributes.get( "krb5key" ).size() > 3 );
469 
470         byte[] testBytes =
471             { 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74 };
472         assertTrue( Arrays.equals( userPassword, testBytes ) );
473 
474         Attribute krb5key = attributes.get( "krb5key" );
475         Map<EncryptionType, EncryptionKey> map = reconstituteKeyMap( krb5key );
476         EncryptionKey encryptionKey = map.get( EncryptionType.DES_CBC_MD5 );
477 
478         byte[] testKeyBytes =
479             { ( byte ) 0x16, ( byte ) 0x4A, ( byte ) 0x6D, ( byte ) 0x89, ( byte ) 0x5D, ( byte ) 0x76, ( byte ) 0x0E,
480                 ( byte ) 0x23 };
481 
482         assertTrue( Arrays.equals( encryptionKey.getKeyValue(), testKeyBytes ) );
483         assertEquals( EncryptionType.DES_CBC_MD5, encryptionKey.getKeyType() );
484 
485         int keyVersionNumber = -1;
486 
487         if ( attributes.get( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT ) != null )
488         {
489             keyVersionNumber = Integer.valueOf( ( String ) attributes.get( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT ).get() );
490         }
491 
492         assertEquals( "Key version number", 1, keyVersionNumber );
493 
494         newUserPassword = "secretsecretsecret";
495 
496         // Modify password.
497         attributes = new BasicAttributes( true );
498         attr = new BasicAttribute( "userPassword", newUserPassword );
499         attributes.put( attr );
500 
501         person = ( DirContext ) ctx.lookup( RDN );
502         person.modifyAttributes( "", DirContext.REPLACE_ATTRIBUTE, attributes );
503 
504         // Read again from directory.
505         person = ( DirContext ) ctx.lookup( RDN );
506 
507         attributes = person.getAttributes( "" );
508 
509         if ( attributes.get( "userPassword" ) != null )
510         {
511             userPassword = ( byte[] ) attributes.get( "userPassword" ).get();
512         }
513 
514         assertEquals( "password length", 18, userPassword.length );
515 
516         if ( attributes.get( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT ) != null )
517         {
518             keyVersionNumber = Integer.valueOf( ( String ) attributes.get( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT ).get() );
519         }
520 
521         assertEquals( "Key version number", 2, keyVersionNumber );
522 
523         newUserPassword = "secretsecretsecretsecret";
524 
525         // Modify password.
526         attributes = new BasicAttributes( true );
527         attr = new BasicAttribute( "userPassword", newUserPassword );
528         attributes.put( attr );
529 
530         person = ( DirContext ) ctx.lookup( RDN );
531         person.modifyAttributes( "", DirContext.REPLACE_ATTRIBUTE, attributes );
532 
533         // Read again from directory.
534         person = ( DirContext ) ctx.lookup( RDN );
535 
536         attributes = person.getAttributes( "" );
537 
538         if ( attributes.get( "userPassword" ) != null )
539         {
540             userPassword = ( byte[] ) attributes.get( "userPassword" ).get();
541         }
542 
543         assertEquals( "password length", 24, userPassword.length );
544 
545         if ( attributes.get( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT ) != null )
546         {
547             keyVersionNumber = Integer.valueOf( ( String ) attributes.get( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT ).get() );
548         }
549 
550         assertEquals( "Key version number", 3, keyVersionNumber );
551     }
552 
553 
554     /**
555      * Tests that the addition of an entry caused random keys to be derived and added.
556      * 
557      * @throws NamingException failure to perform LDAP operations
558      * @throws IOException on network errors
559      * @throws InvalidKeyException if the incorrect key results
560      */
561      @Test
562     public void testAddRandomKeys() throws NamingException, IOException, InvalidKeyException
563     {
564         Hashtable<String, String> env = new Hashtable<String, String>();
565         env.put( "java.naming.factory.initial", "com.sun.jndi.ldap.LdapCtxFactory" );
566         env.put( "java.naming.provider.url", "ldap://localhost:" + ldapService.getIpPort() + "/ou=users,dc=example,dc=com" );
567         env.put( "java.naming.security.principal", "uid=admin,ou=system" );
568         env.put( "java.naming.security.credentials", "secret" );
569         env.put( "java.naming.security.authentication", "simple" );
570         env.put( "java.naming.ldap.attributes.binary", "krb5key" );
571         DirContext ctx = new InitialDirContext( env );
572 
573         Attributes attrs = getPersonAttributes( "Quist", "Thomas Quist", "tquist", "randomKey", "tquist@EXAMPLE.COM" );
574         ctx.createSubcontext( "uid=tquist", attrs );
575 
576         attrs = getPersonAttributes( "Fryer", "John Fryer", "jfryer", "randomKey", "jfryer@EXAMPLE.COM" );
577         ctx.createSubcontext( "uid=jfryer", attrs );
578 
579         String[] attrIDs =
580             { "uid", "userPassword", "krb5Key" };
581 
582         Attributes tquistAttrs = ctx.getAttributes( "uid=tquist", attrIDs );
583         Attributes jfryerAttrs = ctx.getAttributes( "uid=jfryer", attrIDs );
584 
585         String uid = null;
586         byte[] userPassword = null;
587 
588         if ( tquistAttrs.get( "uid" ) != null )
589         {
590             uid = ( String ) tquistAttrs.get( "uid" ).get();
591         }
592 
593         assertEquals( "tquist", uid );
594 
595         if ( tquistAttrs.get( "userPassword" ) != null )
596         {
597             userPassword = ( byte[] ) tquistAttrs.get( "userPassword" ).get();
598         }
599 
600         // Bytes for "randomKey."
601         byte[] testPasswordBytes =
602             { ( byte ) 0x72, ( byte ) 0x61, ( byte ) 0x6E, ( byte ) 0x64, ( byte ) 0x6F, ( byte ) 0x6D, ( byte ) 0x4B,
603                 ( byte ) 0x65, ( byte ) 0x79 };
604         assertTrue( Arrays.equals( testPasswordBytes, userPassword ) );
605 
606         if ( jfryerAttrs.get( "uid" ) != null )
607         {
608             uid = ( String ) jfryerAttrs.get( "uid" ).get();
609         }
610 
611         assertEquals( "jfryer", uid );
612 
613         if ( jfryerAttrs.get( "userPassword" ) != null )
614         {
615             userPassword = ( byte[] ) jfryerAttrs.get( "userPassword" ).get();
616         }
617 
618         assertTrue( Arrays.equals( testPasswordBytes, userPassword ) );
619 
620         byte[] testKeyBytes =
621             { ( byte ) 0xF4, ( byte ) 0xA7, ( byte ) 0x13, ( byte ) 0x64, ( byte ) 0x8A, ( byte ) 0x61, ( byte ) 0xCE,
622                 ( byte ) 0x5B };
623 
624         Attribute krb5key = tquistAttrs.get( "krb5key" );
625         Map<EncryptionType, EncryptionKey> map = reconstituteKeyMap( krb5key );
626         EncryptionKey encryptionKey = map.get( EncryptionType.DES_CBC_MD5 );
627         byte[] tquistKey = encryptionKey.getKeyValue();
628 
629         assertEquals( EncryptionType.DES_CBC_MD5, encryptionKey.getKeyType() );
630 
631         krb5key = jfryerAttrs.get( "krb5key" );
632         map = reconstituteKeyMap( krb5key );
633         encryptionKey = map.get( EncryptionType.DES_CBC_MD5 );
634         byte[] jfryerKey = encryptionKey.getKeyValue();
635 
636         assertEquals( EncryptionType.DES_CBC_MD5, encryptionKey.getKeyType() );
637 
638         assertEquals( "Key length", 8, tquistKey.length );
639         assertEquals( "Key length", 8, jfryerKey.length );
640 
641         assertFalse( Arrays.equals( testKeyBytes, tquistKey ) );
642         assertFalse( Arrays.equals( testKeyBytes, jfryerKey ) );
643         assertFalse( Arrays.equals( jfryerKey, tquistKey ) );
644 
645         byte[] tquistDerivedKey =
646             { ( byte ) 0xFD, ( byte ) 0x7F, ( byte ) 0x6B, ( byte ) 0x83, ( byte ) 0xA4, ( byte ) 0x76, ( byte ) 0xC1,
647                 ( byte ) 0xEA };
648         byte[] jfryerDerivedKey =
649             { ( byte ) 0xA4, ( byte ) 0x10, ( byte ) 0x3B, ( byte ) 0x49, ( byte ) 0xCE, ( byte ) 0x0B, ( byte ) 0xB5,
650                 ( byte ) 0x07 };
651 
652         assertFalse( Arrays.equals( tquistDerivedKey, tquistKey ) );
653         assertFalse( Arrays.equals( jfryerDerivedKey, jfryerKey ) );
654 
655         assertTrue( DESKeySpec.isParityAdjusted( tquistKey, 0 ) );
656         assertTrue( DESKeySpec.isParityAdjusted( jfryerKey, 0 ) );
657     }
658 
659 
660     /**
661      * Convenience method for creating a person.
662      *
663      * @param cn the commonName of the person
664      * @param sn the surName of the person
665      * @param uid the unique id of the person
666      * @param userPassword the password of the person
667      * @param principal the kerberos principal name for the person
668      * @return the attributes of the person entry
669      */
670     protected Attributes getPersonAttributes( String sn, String cn, String uid, String userPassword, String principal )
671     {
672         Attributes attrs = new BasicAttributes( true );
673         Attribute ocls = new BasicAttribute( "objectClass" );
674         ocls.add( "top" );
675         ocls.add( "person" ); // sn $ cn
676         ocls.add( "inetOrgPerson" ); // uid
677         ocls.add( "krb5principal" );
678         ocls.add( "krb5kdcentry" );
679         attrs.put( ocls );
680         attrs.put( "cn", cn );
681         attrs.put( "sn", sn );
682         attrs.put( "uid", uid );
683         attrs.put( "userPassword", userPassword );
684         attrs.put( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT, principal );
685         attrs.put( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT, "0" );
686 
687         return attrs;
688     }
689 
690 
691     /**
692      * Convenience method for creating an organizational unit.
693      *
694      * @param ou the organizational unit to create
695      * @return the attributes of the organizationalUnit
696      */
697     protected Attributes getOrgUnitAttributes( String ou )
698     {
699         Attributes attrs = new BasicAttributes( true );
700         Attribute ocls = new BasicAttribute( "objectClass" );
701         ocls.add( "top" );
702         ocls.add( "organizationalUnit" );
703         attrs.put( ocls );
704         attrs.put( "ou", ou );
705 
706         return attrs;
707     }
708 
709 
710     private Map<EncryptionType, EncryptionKey> reconstituteKeyMap( Attribute krb5key ) throws NamingException,
711         IOException
712     {
713         Map<EncryptionType, EncryptionKey> map = new HashMap<EncryptionType, EncryptionKey>();
714 
715         for ( int ii = 0; ii < krb5key.size(); ii++ )
716         {
717             byte[] encryptionKeyBytes = ( byte[] ) krb5key.get( ii );
718             EncryptionKey encryptionKey = EncryptionKeyDecoder.decode( encryptionKeyBytes );
719             map.put( encryptionKey.getKeyType(), encryptionKey );
720         }
721 
722         return map;
723     }
724 }