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 *     https://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.Base64;
031import java.util.Date;
032
033import javax.crypto.SecretKeyFactory;
034import javax.crypto.spec.PBEKeySpec;
035
036import org.apache.commons.codec.digest.Crypt;
037import org.apache.directory.api.i18n.I18n;
038import org.apache.directory.api.ldap.model.constants.LdapSecurityConstants;
039import org.apache.directory.api.util.DateUtils;
040import org.apache.directory.api.util.TimeProvider;
041import org.apache.directory.api.util.Strings;
042
043/**
044 * A utility class containing methods related to processing passwords.
045 *
046 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
047 */
048public final class PasswordUtil
049{
050
051    /** The SHA1 hash length */
052    public static final int SHA1_LENGTH = 20;
053
054    /** The SHA256 hash length */
055    public static final int SHA256_LENGTH = 32;
056
057    /** The SHA384 hash length */
058    public static final int SHA384_LENGTH = 48;
059
060    /** The SHA512 hash length */
061    public static final int SHA512_LENGTH = 64;
062
063    /** The MD5 hash length */
064    public static final int MD5_LENGTH = 16;
065
066    /** The PKCS5S2 hash length */
067    public static final int PKCS5S2_LENGTH = 32;
068
069    /** The CRYPT (DES) hash length */
070    public static final int CRYPT_LENGTH = 11;
071
072    /** The CRYPT (MD5) hash length */
073    public static final int CRYPT_MD5_LENGTH = 22;
074
075    /** The CRYPT (SHA-256) hash length */
076    public static final int CRYPT_SHA256_LENGTH = 43;
077
078    /** The CRYPT (SHA-512) hash length */
079    public static final int CRYPT_SHA512_LENGTH = 86;
080
081    /** The CRYPT (BCrypt) hash length */
082    public static final int CRYPT_BCRYPT_LENGTH = 31;
083
084    private static final byte[] CRYPT_SALT_CHARS = Strings
085        .getBytesUtf8( "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" );
086
087    private PasswordUtil()
088    {
089    }
090
091
092    /**
093     * Get the algorithm from the stored password. 
094     * It can be found on the beginning of the stored password, between 
095     * curly brackets.
096     * @param credentials the credentials of the user
097     * @return the name of the algorithm to use
098     */
099    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}