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.ldap.model.constants.LdapSecurityConstants;
037import org.apache.directory.api.util.Base64;
038import org.apache.directory.api.util.DateUtils;
039import org.apache.directory.api.util.Strings;
040
041/**
042 * A utility class containing methods related to processing passwords.
043 *
044 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
045 */
046public final class PasswordUtil
047{
048
049    /** The SHA1 hash length */
050    public static final int SHA1_LENGTH = 20;
051
052    /** The SHA256 hash length */
053    public static final int SHA256_LENGTH = 32;
054
055    /** The SHA384 hash length */
056    public static final int SHA384_LENGTH = 48;
057
058    /** The SHA512 hash length */
059    public static final int SHA512_LENGTH = 64;
060
061    /** The MD5 hash length */
062    public static final int MD5_LENGTH = 16;
063
064    /** The PKCS5S2 hash length */
065    public static final int PKCS5S2_LENGTH = 32;
066
067    /** The CRYPT (DES) hash length */
068    public static final int CRYPT_LENGTH = 11;
069
070    /** The CRYPT (MD5) hash length */
071    public static final int CRYPT_MD5_LENGTH = 22;
072
073    /** The CRYPT (SHA-256) hash length */
074    public static final int CRYPT_SHA256_LENGTH = 43;
075
076    /** The CRYPT (SHA-512) hash length */
077    public static final int CRYPT_SHA512_LENGTH = 86;
078
079    /** The CRYPT (BCrypt) hash length */
080    public static final int CRYPT_BCRYPT_LENGTH = 31;
081
082    private static final byte[] CRYPT_SALT_CHARS = Strings
083        .getBytesUtf8( "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" );
084
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    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}