001/*
002 *   or more contributor license agreements.  See the NOTICE file
003 *   Licensed to the Apache Software Foundation (ASF) under one
004 *   distributed with this work for additional information
005 *   regarding copyright ownership.  The ASF licenses this file
006 *   to you under the Apache License, Version 2.0 (the
007 *   "License"); you may not use this file except in compliance
008 *   with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *   Unless required by applicable law or agreed to in writing,
013 *   software distributed under the License is distributed on an
014 *   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *   KIND, either express or implied.  See the License for the
016 *   specific language governing permissions and limitations
017 *   under the License.
018 *
019 */
020
021package org.apache.directory.api.ldap.model.password;
022
023
024import java.security.Key;
025import java.security.MessageDigest;
026import java.security.NoSuchAlgorithmException;
027import java.security.SecureRandom;
028import java.security.spec.KeySpec;
029import java.util.Arrays;
030import java.util.Date;
031
032import javax.crypto.SecretKeyFactory;
033import javax.crypto.spec.PBEKeySpec;
034
035import org.apache.commons.codec.digest.Crypt;
036import org.apache.directory.api.i18n.I18n;
037import org.apache.directory.api.ldap.model.constants.LdapSecurityConstants;
038import org.apache.directory.api.util.Base64;
039import org.apache.directory.api.util.DateUtils;
040import org.apache.directory.api.util.Strings;
041
042/**
043 * A utility class containing methods related to processing passwords.
044 *
045 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
046 */
047public final class PasswordUtil
048{
049
050    /** The SHA1 hash length */
051    public static final int SHA1_LENGTH = 20;
052
053    /** The SHA256 hash length */
054    public static final int SHA256_LENGTH = 32;
055
056    /** The SHA384 hash length */
057    public static final int SHA384_LENGTH = 48;
058
059    /** The SHA512 hash length */
060    public static final int SHA512_LENGTH = 64;
061
062    /** The MD5 hash length */
063    public static final int MD5_LENGTH = 16;
064
065    /** The PKCS5S2 hash length */
066    public static final int PKCS5S2_LENGTH = 32;
067
068    /** The CRYPT (DES) hash length */
069    public static final int CRYPT_LENGTH = 11;
070
071    /** The CRYPT (MD5) hash length */
072    public static final int CRYPT_MD5_LENGTH = 22;
073
074    /** The CRYPT (SHA-256) hash length */
075    public static final int CRYPT_SHA256_LENGTH = 43;
076
077    /** The CRYPT (SHA-512) hash length */
078    public static final int CRYPT_SHA512_LENGTH = 86;
079
080    /** The CRYPT (BCrypt) hash length */
081    public static final int CRYPT_BCRYPT_LENGTH = 31;
082
083    private static final byte[] CRYPT_SALT_CHARS = Strings
084        .getBytesUtf8( "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" );
085
086    private PasswordUtil()
087    {
088    }
089
090
091    /**
092     * Get the algorithm from the stored password. 
093     * It can be found on the beginning of the stored password, between 
094     * curly brackets.
095     * @param credentials the credentials of the user
096     * @return the name of the algorithm to use
097     */
098    public static LdapSecurityConstants findAlgorithm( byte[] credentials )
099    {
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     * @param provided The provided password
353     * @param stored The stored password
354     * @return <tt>true</tt> if the compared passwords are equal
355     */
356    private static boolean compareBytes( byte[] provided, byte[] stored )
357    {
358        if ( stored == null )
359        {
360            return provided == null;
361        }
362        else if ( provided == null )
363        {
364            return false;
365        }
366        
367        // Now, compare the two passwords, using a constant time method
368        if ( stored.length != provided.length )
369        {
370            return false;
371        }
372        
373        // loop on *every* byte in both passwords, and at the end, if one char at least is different, return false.
374        int result = 0;
375        
376        for ( int i = 0; i < stored.length; i++ )
377        {
378            // If both bytes are equal, xor will be == 0, otherwise it will be != 0 and so will result.
379            result |= ( stored[i] ^ provided[i] );
380        }
381        
382        return result == 0;
383    }
384
385
386    /**
387     * encrypts the given credentials based on the algorithm name and optional salt
388     *
389     * @param credentials the credentials to be encrypted
390     * @param algorithm the algorithm to be used for encrypting the credentials
391     * @param salt value to be used as salt (optional)
392     * @return the encrypted credentials
393     */
394    private static byte[] encryptPassword( byte[] credentials, LdapSecurityConstants algorithm, byte[] salt )
395    {
396        switch ( algorithm )
397        {
398            case HASH_METHOD_SHA:
399            case HASH_METHOD_SSHA:
400                return digest( LdapSecurityConstants.HASH_METHOD_SHA, credentials, salt );
401
402            case HASH_METHOD_SHA256:
403            case HASH_METHOD_SSHA256:
404                return digest( LdapSecurityConstants.HASH_METHOD_SHA256, credentials, salt );
405
406            case HASH_METHOD_SHA384:
407            case HASH_METHOD_SSHA384:
408                return digest( LdapSecurityConstants.HASH_METHOD_SHA384, credentials, salt );
409
410            case HASH_METHOD_SHA512:
411            case HASH_METHOD_SSHA512:
412                return digest( LdapSecurityConstants.HASH_METHOD_SHA512, credentials, salt );
413
414            case HASH_METHOD_MD5:
415            case HASH_METHOD_SMD5:
416                return digest( LdapSecurityConstants.HASH_METHOD_MD5, credentials, salt );
417
418            case HASH_METHOD_CRYPT:
419                String saltWithCrypted = Crypt.crypt( Strings.utf8ToString( credentials ), Strings
420                    .utf8ToString( salt ) );
421                String crypted = saltWithCrypted.substring( 2 );
422                return Strings.getBytesUtf8( crypted );
423
424            case HASH_METHOD_CRYPT_MD5:
425            case HASH_METHOD_CRYPT_SHA256:
426            case HASH_METHOD_CRYPT_SHA512:
427                String saltWithCrypted2 = Crypt.crypt( Strings.utf8ToString( credentials ),
428                    algorithm.getSubPrefix() + Strings.utf8ToString( salt ) );
429                String crypted2 = saltWithCrypted2.substring( saltWithCrypted2.lastIndexOf( '$' ) + 1 );
430                return Strings.getBytesUtf8( crypted2 );
431
432            case HASH_METHOD_CRYPT_BCRYPT:
433                String crypted3 = BCrypt.hashPw( Strings.utf8ToString( credentials ), Strings.utf8ToString( salt ) );
434                return Strings.getBytesUtf8( crypted3.substring( crypted3.length() - 31 ) );
435                
436            case HASH_METHOD_PKCS5S2:
437                return generatePbkdf2Hash( credentials, algorithm, salt );
438
439            default:
440                return credentials;
441        }
442    }
443
444
445    /**
446     * Compute the hashed password given an algorithm, the credentials and 
447     * an optional salt.
448     *
449     * @param algorithm the algorithm to use
450     * @param password the credentials
451     * @param salt the optional salt
452     * @return the digested credentials
453     */
454    private static byte[] digest( LdapSecurityConstants algorithm, byte[] password, byte[] salt )
455    {
456        MessageDigest digest;
457
458        try
459        {
460            digest = MessageDigest.getInstance( algorithm.getAlgorithm() );
461        }
462        catch ( NoSuchAlgorithmException e1 )
463        {
464            return null;
465        }
466
467        if ( salt != null )
468        {
469            digest.update( password );
470            digest.update( salt );
471            return digest.digest();
472        }
473        else
474        {
475            return digest.digest( password );
476        }
477    }
478
479
480    /**
481     * Decompose the stored password in an algorithm, an eventual salt
482     * and the password itself.
483     *
484     * If the algorithm is SHA, SSHA, MD5 or SMD5, the part following the algorithm
485     * is base64 encoded
486     *
487     * @param credentials The byte[] containing the credentials to split
488     * @return The password
489     */
490    public static PasswordDetails splitCredentials( byte[] credentials )
491    {
492        LdapSecurityConstants algorithm = findAlgorithm( credentials );
493
494        // check plain text password
495        if ( algorithm == null )
496        {
497            return new PasswordDetails( null, null, credentials );
498        }
499
500        int algoLength = algorithm.getPrefix().length() + 2;
501        byte[] password;
502
503        switch ( algorithm )
504        {
505            case HASH_METHOD_MD5:
506            case HASH_METHOD_SMD5:
507                return getCredentials( credentials, algoLength, MD5_LENGTH, algorithm );
508
509            case HASH_METHOD_SHA:
510            case HASH_METHOD_SSHA:
511                return getCredentials( credentials, algoLength, SHA1_LENGTH, algorithm );
512
513            case HASH_METHOD_SHA256:
514            case HASH_METHOD_SSHA256:
515                return getCredentials( credentials, algoLength, SHA256_LENGTH, algorithm );
516
517            case HASH_METHOD_SHA384:
518            case HASH_METHOD_SSHA384:
519                return getCredentials( credentials, algoLength, SHA384_LENGTH, algorithm );
520
521            case HASH_METHOD_SHA512:
522            case HASH_METHOD_SSHA512:
523                return getCredentials( credentials, algoLength, SHA512_LENGTH, algorithm );
524
525            case HASH_METHOD_PKCS5S2:
526                return getPbkdf2Credentials( credentials, algoLength, algorithm );
527
528            case HASH_METHOD_CRYPT:
529                // The password is associated with a salt. Decompose it
530                // in two parts, no decoding required.
531                // The salt comes first, not like for SSHA and SMD5, and is 2 bytes long
532                // The algorithm, salt, and password will be stored into the PasswordDetails structure.
533                byte[] salt = new byte[2];
534                password = new byte[credentials.length - salt.length - algoLength];
535                split( credentials, algoLength, salt, password );
536                return new PasswordDetails( algorithm, salt, password );
537
538            case HASH_METHOD_CRYPT_BCRYPT:
539                    salt = Arrays.copyOfRange( credentials, algoLength, credentials.length - 31 );
540                    password = Arrays.copyOfRange( credentials, credentials.length - 31, credentials.length );
541                    
542                    return new PasswordDetails( algorithm, salt, password );
543
544            case HASH_METHOD_CRYPT_MD5:
545            case HASH_METHOD_CRYPT_SHA256:
546            case HASH_METHOD_CRYPT_SHA512:
547                // skip $x$
548                algoLength = algoLength + 3;
549                return getCryptCredentials( credentials, algoLength, algorithm );
550
551            default:
552                // unknown method
553                throw new IllegalArgumentException( I18n.err( I18n.ERR_13010_UNKNOWN_HASH_ALGO, algorithm ) );
554        }
555    }
556
557
558    /**
559     * Compute the credentials
560     * 
561     * @param credentials the credentials
562     * @param algoLength The algorithm length
563     * @param hashLen The hash length
564     * @param algorithm the algorithm to use
565     * @return The split password string, containing the credentials, the salt and the password
566     */
567    private static PasswordDetails getCredentials( byte[] credentials, int algoLength, int hashLen,
568        LdapSecurityConstants algorithm )
569    {
570        // The password is associated with a salt. Decompose it
571        // in two parts, after having decoded the password.
572        // The salt is at the end of the credentials.
573        // The algorithm, salt, and password will be stored into the PasswordDetails structure.
574        byte[] passwordAndSalt = Base64
575            .decode( Strings.utf8ToString( credentials, algoLength, credentials.length - algoLength ).toCharArray() );
576
577        int saltLength = passwordAndSalt.length - hashLen;
578        byte[] salt = saltLength == 0 ? null : new byte[saltLength];
579        byte[] password = new byte[hashLen];
580        split( passwordAndSalt, 0, password, salt );
581
582        return new PasswordDetails( algorithm, salt, password );
583    }
584
585
586    private static void split( byte[] all, int offset, byte[] left, byte[] right )
587    {
588        System.arraycopy( all, offset, left, 0, left.length );
589        if ( right != null )
590        {
591            System.arraycopy( all, offset + left.length, right, 0, right.length );
592        }
593    }
594
595
596    private static void merge( byte[] all, byte[] left, byte[] right )
597    {
598        System.arraycopy( left, 0, all, 0, left.length );
599        System.arraycopy( right, 0, all, left.length, right.length );
600    }
601
602
603    /**
604     * checks if the given password's change time is older than the max age 
605     *
606     * @param pwdChangedZtime time when the password was last changed
607     * @param pwdMaxAgeSec the max age value in seconds
608     * @return true if expired, false otherwise
609     */
610    public static boolean isPwdExpired( String pwdChangedZtime, int pwdMaxAgeSec )
611    {
612        Date pwdChangeDate = DateUtils.getDate( pwdChangedZtime );
613
614        //DIRSERVER-1735
615        long time = pwdMaxAgeSec * 1000L;
616        time += pwdChangeDate.getTime();
617
618        Date expiryDate = DateUtils.getDate( DateUtils.getGeneralizedTime( time ) );
619        Date now = DateUtils.getDate( DateUtils.getGeneralizedTime() );
620
621        boolean expired = false;
622
623        if ( expiryDate.equals( now ) || expiryDate.before( now ) )
624        {
625            expired = true;
626        }
627
628        return expired;
629    }
630
631
632    /**
633     * generates a hash based on the <a href="http://en.wikipedia.org/wiki/PBKDF2">PKCS5S2 spec</a>
634     * 
635     * Note: this has been implemented to generate hashes compatible with what JIRA generates.
636     *       See the <a href="http://pythonhosted.org/passlib/lib/passlib.hash.atlassian_pbkdf2_sha1.html">JIRA's passlib</a>
637     *       
638     * @param credentials the credentials
639     * @param algorithm the algorithm to use
640     * @param salt the optional salt
641     * @return the digested credentials
642     */
643    private static byte[] generatePbkdf2Hash( byte[] credentials, LdapSecurityConstants algorithm, byte[] salt )
644    {
645        try
646        {
647            SecretKeyFactory sk = SecretKeyFactory.getInstance( algorithm.getAlgorithm() );
648            char[] password = Strings.utf8ToString( credentials ).toCharArray();
649            KeySpec keySpec = new PBEKeySpec( password, salt, 10000, PKCS5S2_LENGTH * 8 );
650            Key key = sk.generateSecret( keySpec );
651            return key.getEncoded();
652        }
653        catch ( Exception e )
654        {
655            throw new RuntimeException( e );
656        }
657    }
658
659
660    /**
661     * Gets the credentials from a PKCS5S2 hash.
662     * The salt for PKCS5S2 hash is prepended to the password
663     * 
664     * @param credentials The password
665     * @param algoLength The length of the algorithm part
666     * @param algorithm The algorithm in use
667     * @return The split credentials, containing the algorithm, the salt and the password 
668     */
669    private static PasswordDetails getPbkdf2Credentials( byte[] credentials, int algoLength, LdapSecurityConstants algorithm )
670    {
671        // The password is associated with a salt. Decompose it
672        // in two parts, after having decoded the password.
673        // The salt is at the *beginning* of the credentials, and is 16 bytes long
674        // The algorithm, salt, and password will be stored into the PasswordDetails structure.
675        byte[] passwordAndSalt = Base64
676            .decode( Strings.utf8ToString( credentials, algoLength, credentials.length - algoLength ).toCharArray() );
677
678        int saltLength = passwordAndSalt.length - PKCS5S2_LENGTH;
679        byte[] salt = new byte[saltLength];
680        byte[] password = new byte[PKCS5S2_LENGTH];
681
682        split( passwordAndSalt, 0, salt, password );
683
684        return new PasswordDetails( algorithm, salt, password );
685    }
686
687
688    private static byte[] generateCryptSalt( int length )
689    {
690        byte[] salt = new byte[length];
691        SecureRandom sr = new SecureRandom();
692        for ( int i = 0; i < salt.length; i++ )
693        {
694            salt[i] = CRYPT_SALT_CHARS[sr.nextInt( CRYPT_SALT_CHARS.length )];
695        }
696        
697        return salt;
698    }
699
700
701    private static PasswordDetails getCryptCredentials( byte[] credentials, int algoLength,
702        LdapSecurityConstants algorithm )
703    {
704        // The password is associated with a salt. Decompose it
705        // in two parts, no decoding required.
706        // The salt length is dynamic, between the 2nd and 3rd '$'.
707        // The algorithm, salt, and password will be stored into the PasswordDetails structure.
708
709        // skip {crypt}$x$
710        int pos = algoLength;
711        while ( pos < credentials.length )
712        {
713            if ( credentials[pos] == '$' )
714            {
715                break;
716            }
717
718            pos++;
719        }
720
721        byte[] salt = Arrays.copyOfRange( credentials, algoLength, pos );
722        byte[] password = Arrays.copyOfRange( credentials, pos + 1, credentials.length );
723
724        return new PasswordDetails( algorithm, salt, password );
725    }
726
727}