View Javadoc
1   /*
2    *   or more contributor license agreements.  See the NOTICE file
3    *   Licensed to the Apache Software Foundation (ASF) under one
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  
21  package org.apache.directory.api.ldap.model.password;
22  
23  
24  import java.security.Key;
25  import java.security.MessageDigest;
26  import java.security.NoSuchAlgorithmException;
27  import java.security.SecureRandom;
28  import java.security.spec.KeySpec;
29  import java.util.Arrays;
30  import java.util.Date;
31  
32  import javax.crypto.SecretKeyFactory;
33  import javax.crypto.spec.PBEKeySpec;
34  
35  import org.apache.commons.codec.digest.Crypt;
36  import org.apache.directory.api.ldap.model.constants.LdapSecurityConstants;
37  import org.apache.directory.api.util.Base64;
38  import org.apache.directory.api.util.DateUtils;
39  import org.apache.directory.api.util.Strings;
40  
41  /**
42   * A utility class containing methods related to processing passwords.
43   *
44   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
45   */
46  public final class PasswordUtil
47  {
48  
49      /** The SHA1 hash length */
50      public static final int SHA1_LENGTH = 20;
51  
52      /** The SHA256 hash length */
53      public static final int SHA256_LENGTH = 32;
54  
55      /** The SHA384 hash length */
56      public static final int SHA384_LENGTH = 48;
57  
58      /** The SHA512 hash length */
59      public static final int SHA512_LENGTH = 64;
60  
61      /** The MD5 hash length */
62      public static final int MD5_LENGTH = 16;
63  
64      /** The PKCS5S2 hash length */
65      public static final int PKCS5S2_LENGTH = 32;
66  
67      /** The CRYPT (DES) hash length */
68      public static final int CRYPT_LENGTH = 11;
69  
70      /** The CRYPT (MD5) hash length */
71      public static final int CRYPT_MD5_LENGTH = 22;
72  
73      /** The CRYPT (SHA-256) hash length */
74      public static final int CRYPT_SHA256_LENGTH = 43;
75  
76      /** The CRYPT (SHA-512) hash length */
77      public static final int CRYPT_SHA512_LENGTH = 86;
78  
79      /** The CRYPT (BCrypt) hash length */
80      public static final int CRYPT_BCRYPT_LENGTH = 31;
81  
82      private static final byte[] CRYPT_SALT_CHARS = Strings
83          .getBytesUtf8( "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" );
84  
85  
86      private PasswordUtil()
87      {
88      }
89  
90  
91      /**
92       * Get the algorithm from the stored password. 
93       * It can be found on the beginning of the stored password, between 
94       * curly brackets.
95       * @param credentials the credentials of the user
96       * @return the name of the algorithm to use
97       */
98      public static LdapSecurityConstants findAlgorithm( byte[] credentials )
99      {
100         if ( ( credentials == null ) || ( credentials.length == 0 ) )
101         {
102             return null;
103         }
104 
105         if ( credentials[0] == '{' )
106         {
107             // get the algorithm
108             int pos = 1;
109 
110             while ( pos < credentials.length )
111             {
112                 if ( credentials[pos] == '}' )
113                 {
114                     break;
115                 }
116 
117                 pos++;
118             }
119 
120             if ( pos < credentials.length )
121             {
122                 if ( pos == 1 )
123                 {
124                     // We don't have an algorithm : return the credentials as is
125                     return null;
126                 }
127 
128                 String algorithm = Strings.toLowerCaseAscii( Strings.utf8ToString( credentials, 1, pos - 1 ) );
129 
130                 // support for crypt additional encryption algorithms (e.g. {crypt}$1$salt$ez2vlPGdaLYkJam5pWs/Y1)
131                 if ( credentials.length > pos + 3 && credentials[pos + 1] == '$'
132                     && Character.isDigit( credentials[pos + 2] ) )
133                 {
134                     if ( credentials[pos + 3] == '$' )
135                     {
136                         algorithm += Strings.utf8ToString( credentials, pos + 1, 3 );
137                     }
138                     else if ( credentials.length > pos + 4 && credentials[pos + 4] == '$' )
139                     {
140                         algorithm += Strings.utf8ToString( credentials, pos + 1, 4 );
141                     }
142                 }
143 
144                 return LdapSecurityConstants.getAlgorithm( algorithm );
145             }
146             else
147             {
148                 // We don't have an algorithm
149                 return null;
150             }
151         }
152         else
153         {
154             // No '{algo}' part
155             return null;
156         }
157     }
158 
159 
160     /**
161      * @see #createStoragePassword(byte[], LdapSecurityConstants)
162      * 
163      * @param credentials The password
164      * @param algorithm The algorithm to use
165      * @return The resulting byte[] containing the paswword
166      */
167     public static byte[] createStoragePassword( String credentials, LdapSecurityConstants algorithm )
168     {
169         return createStoragePassword( Strings.getBytesUtf8( credentials ), algorithm );
170     }
171 
172 
173     /**
174      * create a hashed password in a format that can be stored in the server.
175      * If the specified algorithm requires a salt then a random salt of 8 byte size is used
176      *  
177      * @param credentials the plain text password
178      * @param algorithm the hashing algorithm to be applied
179      * @return the password after hashing with the given algorithm 
180      */
181     public static byte[] createStoragePassword( byte[] credentials, LdapSecurityConstants algorithm )
182     {
183         // check plain text password
184         if ( algorithm == null )
185         {
186             return credentials;
187         }
188 
189         byte[] salt;
190 
191         switch ( algorithm )
192         {
193             case HASH_METHOD_SSHA:
194             case HASH_METHOD_SSHA256:
195             case HASH_METHOD_SSHA384:
196             case HASH_METHOD_SSHA512:
197             case HASH_METHOD_SMD5:
198                 // we use 8 byte salt always except for "crypt" which needs 2 byte salt
199                 salt = new byte[8];
200                 new SecureRandom().nextBytes( salt );
201                 break;
202 
203             case HASH_METHOD_PKCS5S2:
204                 // we use 16 byte salt for PKCS5S2
205                 salt = new byte[16];
206                 new SecureRandom().nextBytes( salt );
207                 break;
208 
209             case HASH_METHOD_CRYPT:
210                 salt = generateCryptSalt( 2 );
211                 break;
212 
213             case HASH_METHOD_CRYPT_MD5:
214             case HASH_METHOD_CRYPT_SHA256:
215             case HASH_METHOD_CRYPT_SHA512:
216                 salt = generateCryptSalt( 8 );
217                 break;
218                 
219             case HASH_METHOD_CRYPT_BCRYPT:
220                 salt = Strings.getBytesUtf8( BCrypt.genSalt() );
221                 break;
222     
223             default:
224                 salt = null;
225         }
226 
227         byte[] hashedPassword = encryptPassword( credentials, algorithm, salt );
228         StringBuilder sb = new StringBuilder();
229 
230         sb.append( '{' ).append( Strings.upperCase( algorithm.getPrefix() ) ).append( '}' );
231 
232         if ( algorithm == LdapSecurityConstants.HASH_METHOD_CRYPT
233             || algorithm == LdapSecurityConstants.HASH_METHOD_CRYPT_BCRYPT )
234         {
235             sb.append( Strings.utf8ToString( salt ) );
236             sb.append( Strings.utf8ToString( hashedPassword ) );
237         }
238         else if ( algorithm == LdapSecurityConstants.HASH_METHOD_CRYPT_MD5
239             || algorithm == LdapSecurityConstants.HASH_METHOD_CRYPT_SHA256
240             || algorithm == LdapSecurityConstants.HASH_METHOD_CRYPT_SHA512 )
241         {
242             sb.append( algorithm.getSubPrefix() );
243             sb.append( Strings.utf8ToString( salt ) );
244             sb.append( '$' );
245             sb.append( Strings.utf8ToString( hashedPassword ) );
246         }
247         else if ( salt != null )
248         {
249             byte[] hashedPasswordWithSaltBytes = new byte[hashedPassword.length + salt.length];
250 
251             if ( algorithm == LdapSecurityConstants.HASH_METHOD_PKCS5S2 )
252             {
253                 merge( hashedPasswordWithSaltBytes, salt, hashedPassword );
254             }
255             else
256             {
257                 merge( hashedPasswordWithSaltBytes, hashedPassword, salt );
258             }
259 
260             sb.append( String.valueOf( Base64.encode( hashedPasswordWithSaltBytes ) ) );
261         }
262         else
263         {
264             sb.append( String.valueOf( Base64.encode( hashedPassword ) ) );
265         }
266 
267         return Strings.getBytesUtf8( sb.toString() );
268     }
269 
270 
271     /**
272      * 
273      * Compare the credentials.
274      * We have at least 6 algorithms to encrypt the password :
275      * <ul>
276      * <li>- SHA</li>
277      * <li>- SSHA (salted SHA)</li>
278      * <li>- SHA-2(256, 384 and 512 and their salted versions)</li>
279      * <li>- MD5</li>
280      * <li>- SMD5 (slated MD5)</li>
281      * <li>- PKCS5S2 (PBKDF2)</li>
282      * <li>- crypt (unix crypt)</li>
283      * <li>- plain text, ie no encryption.</li>
284      * </ul>
285      * <p>
286      *  If we get an encrypted password, it is prefixed by the used algorithm, between
287      *  brackets : {SSHA}password ...
288      *  </p>
289      *  If the password is using SSHA, SMD5 or crypt, some 'salt' is added to the password :
290      *  <ul>
291      *  <li>- length(password) - 20, starting at 21st position for SSHA</li>
292      *  <li>- length(password) - 16, starting at 16th position for SMD5</li>
293      *  <li>- length(password) - 2, starting at 3rd position for crypt</li>
294      *  </ul>
295      *  <p>
296      *  For (S)SHA, SHA-256 and (S)MD5, we have to transform the password from Base64 encoded text
297      *  to a byte[] before comparing the password with the stored one.
298      *  </p>
299      *  <p>
300      *  For PKCS5S2 the salt is stored in the beginning of the password
301      *  </p>
302      *  <p>
303      *  For crypt, we only have to remove the salt.
304      *  </p>
305      *  <p>
306      *  At the end, we use the digest() method for (S)SHA and (S)MD5, the crypt() method for
307      *  the CRYPT algorithm and a straight comparison for PLAIN TEXT passwords.
308      *  </p>
309      *  <p>
310      *  The stored password is always using the unsalted form, and is stored as a bytes array.
311      *  </p>
312      *
313      * @param receivedCredentials the credentials provided by user
314      * @param storedCredentials the credentials stored in the server
315      * @return true if they are equal, false otherwise
316      */
317     public static boolean compareCredentials( byte[] receivedCredentials, byte[] storedCredentials )
318     {
319         LdapSecurityConstants algorithm = findAlgorithm( storedCredentials );
320 
321         if ( algorithm != null )
322         {
323             // Let's get the encrypted part of the stored password
324             // We should just keep the password, excluding the algorithm
325             // and the salt, if any.
326             // But we should also get the algorithm and salt to
327             // be able to encrypt the submitted user password in the next step
328             PasswordDetails passwordDetails = PasswordUtil.splitCredentials( storedCredentials );
329 
330             // Reuse the saltedPassword information to construct the encrypted
331             // password given by the user.
332             byte[] userPassword = PasswordUtil.encryptPassword( receivedCredentials, passwordDetails.getAlgorithm(),
333                 passwordDetails.getSalt() );
334 
335             return compareBytes( userPassword, passwordDetails.getPassword() );
336         }
337         else
338         {
339             return compareBytes( receivedCredentials, storedCredentials );
340         }
341     }
342     
343     
344     /**
345      * Compare two byte[] in a constant time. This is necessary because using an Array.equals() is
346      * not Timing attack safe ([1], [2] and [3]), a breach that can be exploited to break some hashes.
347      * 
348      *  [1] https://en.wikipedia.org/wiki/Timing_attack
349      *  [2] http://rdist.root.org/2009/05/28/timing-attack-in-google-keyczar-library/
350      *  [3] https://cryptocoding.net/index.php/Coding_rules
351      */
352     private static boolean compareBytes( byte[] provided, byte[] stored )
353     {
354         if ( stored == null )
355         {
356             return provided == null;
357         }
358         else if ( provided == null )
359         {
360             return false;
361         }
362         
363         // Now, compare the two passwords, using a constant time method
364         if ( stored.length != provided.length )
365         {
366             return false;
367         }
368         
369         // loop on *every* byte in both passwords, and at the end, if one char at least is different, return false.
370         int result = 0;
371         
372         for ( int i = 0; i < stored.length; i++ )
373         {
374             // If both bytes are equal, xor will be == 0, otherwise it will be != 0 and so will result.
375             result |= ( stored[i] ^ provided[i] );
376         }
377         
378         return result == 0;
379     }
380 
381 
382     /**
383      * encrypts the given credentials based on the algorithm name and optional salt
384      *
385      * @param credentials the credentials to be encrypted
386      * @param algorithm the algorithm to be used for encrypting the credentials
387      * @param salt value to be used as salt (optional)
388      * @return the encrypted credentials
389      */
390     private static byte[] encryptPassword( byte[] credentials, LdapSecurityConstants algorithm, byte[] salt )
391     {
392         switch ( algorithm )
393         {
394             case HASH_METHOD_SHA:
395             case HASH_METHOD_SSHA:
396                 return digest( LdapSecurityConstants.HASH_METHOD_SHA, credentials, salt );
397 
398             case HASH_METHOD_SHA256:
399             case HASH_METHOD_SSHA256:
400                 return digest( LdapSecurityConstants.HASH_METHOD_SHA256, credentials, salt );
401 
402             case HASH_METHOD_SHA384:
403             case HASH_METHOD_SSHA384:
404                 return digest( LdapSecurityConstants.HASH_METHOD_SHA384, credentials, salt );
405 
406             case HASH_METHOD_SHA512:
407             case HASH_METHOD_SSHA512:
408                 return digest( LdapSecurityConstants.HASH_METHOD_SHA512, credentials, salt );
409 
410             case HASH_METHOD_MD5:
411             case HASH_METHOD_SMD5:
412                 return digest( LdapSecurityConstants.HASH_METHOD_MD5, credentials, salt );
413 
414             case HASH_METHOD_CRYPT:
415                 String saltWithCrypted = Crypt.crypt( Strings.utf8ToString( credentials ), Strings
416                     .utf8ToString( salt ) );
417                 String crypted = saltWithCrypted.substring( 2 );
418                 return Strings.getBytesUtf8( crypted );
419 
420             case HASH_METHOD_CRYPT_MD5:
421             case HASH_METHOD_CRYPT_SHA256:
422             case HASH_METHOD_CRYPT_SHA512:
423                 String saltWithCrypted2 = Crypt.crypt( Strings.utf8ToString( credentials ),
424                     algorithm.getSubPrefix() + Strings.utf8ToString( salt ) );
425                 String crypted2 = saltWithCrypted2.substring( saltWithCrypted2.lastIndexOf( '$' ) + 1 );
426                 return Strings.getBytesUtf8( crypted2 );
427 
428             case HASH_METHOD_CRYPT_BCRYPT:
429                 String crypted3 = BCrypt.hashPw( Strings.utf8ToString( credentials ), Strings.utf8ToString( salt ) );
430                 return Strings.getBytesUtf8( crypted3.substring( crypted3.length() - 31 ) );
431                 
432             case HASH_METHOD_PKCS5S2:
433                 return generatePbkdf2Hash( credentials, algorithm, salt );
434 
435             default:
436                 return credentials;
437         }
438     }
439 
440 
441     /**
442      * Compute the hashed password given an algorithm, the credentials and 
443      * an optional salt.
444      *
445      * @param algorithm the algorithm to use
446      * @param password the credentials
447      * @param salt the optional salt
448      * @return the digested credentials
449      */
450     private static byte[] digest( LdapSecurityConstants algorithm, byte[] password, byte[] salt )
451     {
452         MessageDigest digest;
453 
454         try
455         {
456             digest = MessageDigest.getInstance( algorithm.getAlgorithm() );
457         }
458         catch ( NoSuchAlgorithmException e1 )
459         {
460             return null;
461         }
462 
463         if ( salt != null )
464         {
465             digest.update( password );
466             digest.update( salt );
467             return digest.digest();
468         }
469         else
470         {
471             return digest.digest( password );
472         }
473     }
474 
475 
476     /**
477      * Decompose the stored password in an algorithm, an eventual salt
478      * and the password itself.
479      *
480      * If the algorithm is SHA, SSHA, MD5 or SMD5, the part following the algorithm
481      * is base64 encoded
482      *
483      * @param credentials The byte[] containing the credentials to split
484      * @return The password
485      */
486     public static PasswordDetails splitCredentials( byte[] credentials )
487     {
488         LdapSecurityConstants algorithm = findAlgorithm( credentials );
489 
490         // check plain text password
491         if ( algorithm == null )
492         {
493             return new PasswordDetails( null, null, credentials );
494         }
495 
496         int algoLength = algorithm.getPrefix().length() + 2;
497         byte[] password;
498 
499         switch ( algorithm )
500         {
501             case HASH_METHOD_MD5:
502             case HASH_METHOD_SMD5:
503                 return getCredentials( credentials, algoLength, MD5_LENGTH, algorithm );
504 
505             case HASH_METHOD_SHA:
506             case HASH_METHOD_SSHA:
507                 return getCredentials( credentials, algoLength, SHA1_LENGTH, algorithm );
508 
509             case HASH_METHOD_SHA256:
510             case HASH_METHOD_SSHA256:
511                 return getCredentials( credentials, algoLength, SHA256_LENGTH, algorithm );
512 
513             case HASH_METHOD_SHA384:
514             case HASH_METHOD_SSHA384:
515                 return getCredentials( credentials, algoLength, SHA384_LENGTH, algorithm );
516 
517             case HASH_METHOD_SHA512:
518             case HASH_METHOD_SSHA512:
519                 return getCredentials( credentials, algoLength, SHA512_LENGTH, algorithm );
520 
521             case HASH_METHOD_PKCS5S2:
522                 return getPbkdf2Credentials( credentials, algoLength, algorithm );
523 
524             case HASH_METHOD_CRYPT:
525                 // The password is associated with a salt. Decompose it
526                 // in two parts, no decoding required.
527                 // The salt comes first, not like for SSHA and SMD5, and is 2 bytes long
528                 // The algorithm, salt, and password will be stored into the PasswordDetails structure.
529                 byte[] salt = new byte[2];
530                 password = new byte[credentials.length - salt.length - algoLength];
531                 split( credentials, algoLength, salt, password );
532                 return new PasswordDetails( algorithm, salt, password );
533 
534             case HASH_METHOD_CRYPT_BCRYPT:
535                     salt = Arrays.copyOfRange( credentials, algoLength, credentials.length - 31 );
536                     password = Arrays.copyOfRange( credentials, credentials.length - 31, credentials.length );
537                     
538                     return new PasswordDetails( algorithm, salt, password );
539             case HASH_METHOD_CRYPT_MD5:
540             case HASH_METHOD_CRYPT_SHA256:
541             case HASH_METHOD_CRYPT_SHA512:
542                 // skip $x$
543                 algoLength = algoLength + 3;
544                 return getCryptCredentials( credentials, algoLength, algorithm );
545 
546             default:
547                 // unknown method
548                 throw new IllegalArgumentException( "Unknown hash algorithm " + algorithm );
549         }
550     }
551 
552 
553     /**
554      * Compute the credentials
555      */
556     private static PasswordDetails getCredentials( byte[] credentials, int algoLength, int hashLen,
557         LdapSecurityConstants algorithm )
558     {
559         // The password is associated with a salt. Decompose it
560         // in two parts, after having decoded the password.
561         // The salt is at the end of the credentials.
562         // The algorithm, salt, and password will be stored into the PasswordDetails structure.
563         byte[] passwordAndSalt = Base64
564             .decode( Strings.utf8ToString( credentials, algoLength, credentials.length - algoLength ).toCharArray() );
565 
566         int saltLength = passwordAndSalt.length - hashLen;
567         byte[] salt = saltLength == 0 ? null : new byte[saltLength];
568         byte[] password = new byte[hashLen];
569         split( passwordAndSalt, 0, password, salt );
570 
571         return new PasswordDetails( algorithm, salt, password );
572     }
573 
574 
575     private static void split( byte[] all, int offset, byte[] left, byte[] right )
576     {
577         System.arraycopy( all, offset, left, 0, left.length );
578         if ( right != null )
579         {
580             System.arraycopy( all, offset + left.length, right, 0, right.length );
581         }
582     }
583 
584 
585     private static void merge( byte[] all, byte[] left, byte[] right )
586     {
587         System.arraycopy( left, 0, all, 0, left.length );
588         System.arraycopy( right, 0, all, left.length, right.length );
589     }
590 
591 
592     /**
593      * checks if the given password's change time is older than the max age 
594      *
595      * @param pwdChangedZtime time when the password was last changed
596      * @param pwdMaxAgeSec the max age value in seconds
597      * @return true if expired, false otherwise
598      */
599     public static boolean isPwdExpired( String pwdChangedZtime, int pwdMaxAgeSec )
600     {
601         Date pwdChangeDate = DateUtils.getDate( pwdChangedZtime );
602 
603         //DIRSERVER-1735
604         long time = pwdMaxAgeSec * 1000L;
605         time += pwdChangeDate.getTime();
606 
607         Date expiryDate = DateUtils.getDate( DateUtils.getGeneralizedTime( time ) );
608         Date now = DateUtils.getDate( DateUtils.getGeneralizedTime() );
609 
610         boolean expired = false;
611 
612         if ( expiryDate.equals( now ) || expiryDate.before( now ) )
613         {
614             expired = true;
615         }
616 
617         return expired;
618     }
619 
620 
621     /**
622      * generates a hash based on the <a href="http://en.wikipedia.org/wiki/PBKDF2">PKCS5S2 spec</a>
623      * 
624      * Note: this has been implemented to generate hashes compatible with what JIRA generates.
625      *       See the <a href="http://pythonhosted.org/passlib/lib/passlib.hash.atlassian_pbkdf2_sha1.html">JIRA's passlib</a>
626      * @param credentials the credentials
627      * @param algorithm the algorithm to use
628      * @param salt the optional salt
629      * @return the digested credentials
630      */
631     private static byte[] generatePbkdf2Hash( byte[] credentials, LdapSecurityConstants algorithm, byte[] salt )
632     {
633         try
634         {
635             SecretKeyFactory sk = SecretKeyFactory.getInstance( algorithm.getAlgorithm() );
636             char[] password = Strings.utf8ToString( credentials ).toCharArray();
637             KeySpec keySpec = new PBEKeySpec( password, salt, 10000, PKCS5S2_LENGTH * 8 );
638             Key key = sk.generateSecret( keySpec );
639             return key.getEncoded();
640         }
641         catch ( Exception e )
642         {
643             throw new RuntimeException( e );
644         }
645     }
646 
647 
648     /**
649      * Gets the credentials from a PKCS5S2 hash.
650      * The salt for PKCS5S2 hash is prepended to the password
651      */
652     private static PasswordDetails getPbkdf2Credentials( byte[] credentials, int algoLength, LdapSecurityConstants algorithm )
653     {
654         // The password is associated with a salt. Decompose it
655         // in two parts, after having decoded the password.
656         // The salt is at the *beginning* of the credentials, and is 16 bytes long
657         // The algorithm, salt, and password will be stored into the PasswordDetails structure.
658         byte[] passwordAndSalt = Base64
659             .decode( Strings.utf8ToString( credentials, algoLength, credentials.length - algoLength ).toCharArray() );
660 
661         int saltLength = passwordAndSalt.length - PKCS5S2_LENGTH;
662         byte[] salt = new byte[saltLength];
663         byte[] password = new byte[PKCS5S2_LENGTH];
664 
665         split( passwordAndSalt, 0, salt, password );
666 
667         return new PasswordDetails( algorithm, salt, password );
668     }
669 
670 
671     private static byte[] generateCryptSalt( int length )
672     {
673         byte[] salt = new byte[length];
674         SecureRandom sr = new SecureRandom();
675         for ( int i = 0; i < salt.length; i++ )
676         {
677             salt[i] = CRYPT_SALT_CHARS[sr.nextInt( CRYPT_SALT_CHARS.length )];
678         }
679         
680         return salt;
681     }
682 
683 
684     private static PasswordDetails getCryptCredentials( byte[] credentials, int algoLength,
685         LdapSecurityConstants algorithm )
686     {
687         // The password is associated with a salt. Decompose it
688         // in two parts, no decoding required.
689         // The salt length is dynamic, between the 2nd and 3rd '$'.
690         // The algorithm, salt, and password will be stored into the PasswordDetails structure.
691 
692         // skip {crypt}$x$
693         int pos = algoLength;
694         while ( pos < credentials.length )
695         {
696             if ( credentials[pos] == '$' )
697             {
698                 break;
699             }
700 
701             pos++;
702         }
703 
704         byte[] salt = Arrays.copyOfRange( credentials, algoLength, pos );
705         byte[] password = Arrays.copyOfRange( credentials, pos + 1, credentials.length );
706 
707         return new PasswordDetails( algorithm, salt, password );
708     }
709 
710 }