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