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.io.UnsupportedEncodingException;
025import java.security.Key;
026import java.security.MessageDigest;
027import java.security.NoSuchAlgorithmException;
028import java.security.SecureRandom;
029import java.security.spec.KeySpec;
030import java.util.Arrays;
031import java.util.Date;
032
033import javax.crypto.SecretKeyFactory;
034import javax.crypto.spec.PBEKeySpec;
035
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;
040import org.apache.directory.api.util.UnixCrypt;
041
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 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    /**
070     * Get the algorithm from the stored password. 
071     * It can be found on the beginning of the stored password, between 
072     * curly brackets.
073     * @param credentials the credentials of the user
074     * @return the name of the algorithm to use
075     */
076    public static LdapSecurityConstants findAlgorithm( byte[] credentials )
077    {
078        if ( ( credentials == null ) || ( credentials.length == 0 ) )
079        {
080            return null;
081        }
082
083        if ( credentials[0] == '{' )
084        {
085            // get the algorithm
086            int pos = 1;
087
088            while ( pos < credentials.length )
089            {
090                if ( credentials[pos] == '}' )
091                {
092                    break;
093                }
094
095                pos++;
096            }
097
098            if ( pos < credentials.length )
099            {
100                if ( pos == 1 )
101                {
102                    // We don't have an algorithm : return the credentials as is
103                    return null;
104                }
105
106                String algorithm = Strings.toLowerCase( new String( credentials, 1, pos - 1 ) );
107
108                return LdapSecurityConstants.getAlgorithm( algorithm );
109            }
110            else
111            {
112                // We don't have an algorithm
113                return null;
114            }
115        }
116        else
117        {
118            // No '{algo}' part
119            return null;
120        }
121    }
122
123
124    /**
125     * @see #createStoragePassword(byte[], LdapSecurityConstants)
126     */
127    public static byte[] createStoragePassword( String credentials, LdapSecurityConstants algorithm )
128    {
129        return createStoragePassword( Strings.getBytesUtf8( credentials ), algorithm );
130    }
131
132
133    /**
134     * create a hashed password in a format that can be stored in the server.
135     * If the specified algorithm requires a salt then a random salt of 8 byte size is used
136     *  
137     * @param credentials the plain text password
138     * @param algorithm the hashing algorithm to be applied
139     * @return the password after hashing with the given algorithm 
140     */
141    public static byte[] createStoragePassword( byte[] credentials, LdapSecurityConstants algorithm )
142    {
143        byte[] salt;
144
145        switch ( algorithm )
146        {
147            case HASH_METHOD_SSHA:
148            case HASH_METHOD_SSHA256:
149            case HASH_METHOD_SSHA384:
150            case HASH_METHOD_SSHA512:
151            case HASH_METHOD_SMD5:
152                salt = new byte[8]; // we use 8 byte salt always except for "crypt" which needs 2 byte salt
153                new SecureRandom().nextBytes( salt );
154                break;
155
156            case HASH_METHOD_PKCS5S2:
157                salt = new byte[16]; // we use 16 byte salt for PKCS5S2
158                new SecureRandom().nextBytes( salt );
159                break;
160                
161            case HASH_METHOD_CRYPT:
162                salt = new byte[2];
163                SecureRandom sr = new SecureRandom();
164                int i1 = sr.nextInt( 64 );
165                int i2 = sr.nextInt( 64 );
166
167                salt[0] = ( byte ) ( i1 < 12 ? ( i1 + '.' ) : i1 < 38 ? ( i1 + 'A' - 12 ) : ( i1 + 'a' - 38 ) );
168                salt[1] = ( byte ) ( i2 < 12 ? ( i2 + '.' ) : i2 < 38 ? ( i2 + 'A' - 12 ) : ( i2 + 'a' - 38 ) );
169                break;
170
171            default:
172                salt = null;
173        }
174
175        byte[] hashedPassword = encryptPassword( credentials, algorithm, salt );
176        StringBuffer sb = new StringBuffer();
177
178        if ( algorithm != null )
179        {
180            sb.append( '{' ).append( algorithm.getPrefix().toUpperCase() ).append( '}' );
181
182            if ( algorithm == LdapSecurityConstants.HASH_METHOD_CRYPT )
183            {
184                sb.append( Strings.utf8ToString( salt ) );
185                sb.append( Strings.utf8ToString( hashedPassword ) );
186            }
187            else if ( salt != null )
188            {
189                byte[] hashedPasswordWithSaltBytes = new byte[hashedPassword.length + salt.length];
190
191                if ( algorithm == LdapSecurityConstants.HASH_METHOD_PKCS5S2 )
192                {
193                    merge( hashedPasswordWithSaltBytes, salt, hashedPassword );
194                }
195                else
196                {
197                    merge( hashedPasswordWithSaltBytes, hashedPassword, salt );
198                }
199                
200                sb.append( String.valueOf( Base64.encode( hashedPasswordWithSaltBytes ) ) );
201            }
202            else
203            {
204                sb.append( String.valueOf( Base64.encode( hashedPassword ) ) );
205            }
206        }
207        else
208        {
209            sb.append( Strings.utf8ToString( hashedPassword ) );
210        }
211
212        return Strings.getBytesUtf8( sb.toString() );
213    }
214
215
216    /**
217     * 
218     * Compare the credentials.
219     * We have at least 6 algorithms to encrypt the password :
220     * <ul>
221     * <li>- SHA</li>
222     * <li>- SSHA (salted SHA)</li>
223     * <li>- SHA-2(256, 384 and 512 and their salted versions)</li>
224     * <li>- MD5</li>
225     * <li>- SMD5 (slated MD5)</li>
226     * <li>- PKCS5S2 (PBKDF2)</li>
227     * <li>- crypt (unix crypt)</li>
228     * <li>- plain text, ie no encryption.</li>
229     * </ul>
230     * <p>
231     *  If we get an encrypted password, it is prefixed by the used algorithm, between
232     *  brackets : {SSHA}password ...
233     *  </p>
234     *  If the password is using SSHA, SMD5 or crypt, some 'salt' is added to the password :
235     *  <ul>
236     *  <li>- length(password) - 20, starting at 21st position for SSHA</li>
237     *  <li>- length(password) - 16, starting at 16th position for SMD5</li>
238     *  <li>- length(password) - 2, starting at 3rd position for crypt</li>
239     *  </ul>
240     *  <p>
241     *  For (S)SHA, SHA-256 and (S)MD5, we have to transform the password from Base64 encoded text
242     *  to a byte[] before comparing the password with the stored one.
243     *  </p>
244     *  <p>
245     *  For PKCS5S2 the salt is stored in the beginning of the password
246     *  </p>
247     *  <p>
248     *  For crypt, we only have to remove the salt.
249     *  </p>
250     *  <p>
251     *  At the end, we use the digest() method for (S)SHA and (S)MD5, the crypt() method for
252     *  the CRYPT algorithm and a straight comparison for PLAIN TEXT passwords.
253     *  </p>
254     *  <p>
255     *  The stored password is always using the unsalted form, and is stored as a bytes array.
256     *  </p>
257     *
258     * @param receivedCredentials the credentials provided by user
259     * @param storedCredentials the credentials stored in the server
260     * @return true if they are equal, false otherwise
261     */
262    public static boolean compareCredentials( byte[] receivedCredentials, byte[] storedCredentials )
263    {
264        LdapSecurityConstants algorithm = findAlgorithm( storedCredentials );
265
266        if ( algorithm != null )
267        {
268            EncryptionMethod encryptionMethod = new EncryptionMethod( algorithm, null );
269
270            // Let's get the encrypted part of the stored password
271            // We should just keep the password, excluding the algorithm
272            // and the salt, if any.
273            // But we should also get the algorithm and salt to
274            // be able to encrypt the submitted user password in the next step
275            byte[] encryptedStored = PasswordUtil.splitCredentials( storedCredentials, encryptionMethod );
276
277            // Reuse the saltedPassword informations to construct the encrypted
278            // password given by the user.
279            byte[] userPassword = PasswordUtil.encryptPassword( receivedCredentials, encryptionMethod.getAlgorithm(),
280                encryptionMethod.getSalt() );
281
282            // Now, compare the two passwords.
283            return Arrays.equals( userPassword, encryptedStored );
284        }
285        else
286        {
287            return Arrays.equals( storedCredentials, receivedCredentials );
288        }
289    }
290
291
292    /**
293     * encrypts the given credentials based on the algorithm name and optional salt
294     *
295     * @param credentials the credentials to be encrypted
296     * @param algorithm the algorithm to be used for encrypting the credentials
297     * @param salt value to be used as salt (optional)
298     * @return the encrypted credentials
299     */
300    public static byte[] encryptPassword( byte[] credentials, LdapSecurityConstants algorithm, byte[] salt )
301    {
302        switch ( algorithm )
303        {
304            case HASH_METHOD_SHA:
305            case HASH_METHOD_SSHA:
306                return digest( LdapSecurityConstants.HASH_METHOD_SHA, credentials, salt );
307
308            case HASH_METHOD_SHA256:
309            case HASH_METHOD_SSHA256:
310                return digest( LdapSecurityConstants.HASH_METHOD_SHA256, credentials, salt );
311
312            case HASH_METHOD_SHA384:
313            case HASH_METHOD_SSHA384:
314                return digest( LdapSecurityConstants.HASH_METHOD_SHA384, credentials, salt );
315
316            case HASH_METHOD_SHA512:
317            case HASH_METHOD_SSHA512:
318                return digest( LdapSecurityConstants.HASH_METHOD_SHA512, credentials, salt );
319
320            case HASH_METHOD_MD5:
321            case HASH_METHOD_SMD5:
322                return digest( LdapSecurityConstants.HASH_METHOD_MD5, credentials, salt );
323
324            case HASH_METHOD_CRYPT:
325                String saltWithCrypted = UnixCrypt.crypt( Strings.utf8ToString( credentials ), Strings
326                    .utf8ToString( salt ) );
327                String crypted = saltWithCrypted.substring( 2 );
328
329                return Strings.getBytesUtf8( crypted );
330
331            case HASH_METHOD_PKCS5S2:
332                return generatePbkdf2Hash( credentials, algorithm, salt );
333                
334            default:
335                return credentials;
336        }
337    }
338
339
340    /**
341     * Compute the hashed password given an algorithm, the credentials and 
342     * an optional salt.
343     *
344     * @param algorithm the algorithm to use
345     * @param password the credentials
346     * @param salt the optional salt
347     * @return the digested credentials
348     */
349    private static byte[] digest( LdapSecurityConstants algorithm, byte[] password, byte[] salt )
350    {
351        MessageDigest digest;
352
353        try
354        {
355            digest = MessageDigest.getInstance( algorithm.getAlgorithm() );
356        }
357        catch ( NoSuchAlgorithmException e1 )
358        {
359            return null;
360        }
361
362        if ( salt != null )
363        {
364            digest.update( password );
365            digest.update( salt );
366            return digest.digest();
367        }
368        else
369        {
370            return digest.digest( password );
371        }
372    }
373
374
375    /**
376     * Decompose the stored password in an algorithm, an eventual salt
377     * and the password itself.
378     *
379     * If the algorithm is SHA, SSHA, MD5 or SMD5, the part following the algorithm
380     * is base64 encoded
381     *
382     * @param encryptionMethod The structure to feed
383     * @return The password
384     * @param credentials the credentials to split
385     */
386    public static byte[] splitCredentials( byte[] credentials, EncryptionMethod encryptionMethod )
387    {
388        int algoLength = encryptionMethod.getAlgorithm().getPrefix().length() + 2;
389
390        switch ( encryptionMethod.getAlgorithm() )
391        {
392            case HASH_METHOD_MD5:
393            case HASH_METHOD_SHA:
394                try
395                {
396                    // We just have the password just after the algorithm, base64 encoded.
397                    // Just decode the password and return it.
398                    return Base64
399                        .decode( new String( credentials, algoLength, credentials.length - algoLength, "UTF-8" )
400                            .toCharArray() );
401                }
402                catch ( UnsupportedEncodingException uee )
403                {
404                    // do nothing
405                    return credentials;
406                }
407
408            case HASH_METHOD_SMD5:
409                try
410                {
411                    // The password is associated with a salt. Decompose it
412                    // in two parts, after having decoded the password.
413                    // The salt will be stored into the EncryptionMethod structure
414                    // The salt is at the end of the credentials, and is 8 bytes long
415                    byte[] passwordAndSalt = Base64.decode( new String( credentials, algoLength, credentials.length
416                        - algoLength, "UTF-8" ).toCharArray() );
417
418                    int saltLength = passwordAndSalt.length - MD5_LENGTH;
419                    encryptionMethod.setSalt( new byte[saltLength] );
420                    byte[] password = new byte[MD5_LENGTH];
421                    split( passwordAndSalt, 0, password, encryptionMethod.getSalt() );
422
423                    return password;
424                }
425                catch ( UnsupportedEncodingException uee )
426                {
427                    // do nothing
428                    return credentials;
429                }
430
431            case HASH_METHOD_SSHA:
432                return getCredentials( credentials, algoLength, SHA1_LENGTH, encryptionMethod );
433
434            case HASH_METHOD_SHA256:
435            case HASH_METHOD_SSHA256:
436                return getCredentials( credentials, algoLength, SHA256_LENGTH, encryptionMethod );
437
438            case HASH_METHOD_SHA384:
439            case HASH_METHOD_SSHA384:
440                return getCredentials( credentials, algoLength, SHA384_LENGTH, encryptionMethod );
441
442            case HASH_METHOD_SHA512:
443            case HASH_METHOD_SSHA512:
444                return getCredentials( credentials, algoLength, SHA512_LENGTH, encryptionMethod );
445
446            case HASH_METHOD_PKCS5S2:
447                return getPbkdf2Credentials( credentials, algoLength, encryptionMethod );
448                
449            case HASH_METHOD_CRYPT:
450                // The password is associated with a salt. Decompose it
451                // in two parts, storing the salt into the EncryptionMethod structure.
452                // The salt comes first, not like for SSHA and SMD5, and is 2 bytes long
453                encryptionMethod.setSalt( new byte[2] );
454                byte[] password = new byte[credentials.length - encryptionMethod.getSalt().length - algoLength];
455                split( credentials, algoLength, encryptionMethod.getSalt(), password );
456
457                return password;
458
459            default:
460                // unknown method
461                return credentials;
462
463        }
464    }
465
466
467    /**
468     * Compute the credentials
469     */
470    private static byte[] getCredentials( byte[] credentials, int algoLength, int hashLen,
471        EncryptionMethod encryptionMethod )
472    {
473        try
474        {
475            // The password is associated with a salt. Decompose it
476            // in two parts, after having decoded the password.
477            // The salt will be stored into the EncryptionMethod structure
478            // The salt is at the end of the credentials, and is 8 bytes long
479            byte[] passwordAndSalt = Base64.decode( new String( credentials, algoLength, credentials.length
480                - algoLength, "UTF-8" ).toCharArray() );
481
482            int saltLength = passwordAndSalt.length - hashLen;
483            encryptionMethod.setSalt( new byte[saltLength] );
484            byte[] password = new byte[hashLen];
485            split( passwordAndSalt, 0, password, encryptionMethod.getSalt() );
486
487            return password;
488        }
489        catch ( UnsupportedEncodingException uee )
490        {
491            // do nothing
492            return credentials;
493        }
494    }
495
496
497    private static void split( byte[] all, int offset, byte[] left, byte[] right )
498    {
499        System.arraycopy( all, offset, left, 0, left.length );
500        System.arraycopy( all, offset + left.length, right, 0, right.length );
501    }
502
503
504    private static void merge( byte[] all, byte[] left, byte[] right )
505    {
506        System.arraycopy( left, 0, all, 0, left.length );
507        System.arraycopy( right, 0, all, left.length, right.length );
508    }
509
510
511    /**
512     * checks if the given password's change time is older than the max age 
513     *
514     * @param pwdChangedZtime time when the password was last changed
515     * @param pwdMaxAgeSec the max age value in seconds
516     * @return true if expired, false otherwise
517     */
518    public static boolean isPwdExpired( String pwdChangedZtime, int pwdMaxAgeSec )
519    {
520        Date pwdChangeDate = DateUtils.getDate( pwdChangedZtime );
521
522        long time = pwdMaxAgeSec * 1000L;//DIRSERVER-1735
523        time += pwdChangeDate.getTime();
524
525        Date expiryDate = DateUtils.getDate( DateUtils.getGeneralizedTime( time ) );
526        Date now = DateUtils.getDate( DateUtils.getGeneralizedTime() );
527
528        boolean expired = false;
529
530        if ( expiryDate.equals( now ) || expiryDate.before( now ) )
531        {
532            expired = true;
533        }
534
535        return expired;
536    }
537    
538    
539    /**
540     * generates a hash based on the <a href="http://en.wikipedia.org/wiki/PBKDF2">PKCS5S2 spec</a>
541     * 
542     * Note: this has been implemented to generate hashes compatible with what JIRA generates.
543     *       See the <a href="http://pythonhosted.org/passlib/lib/passlib.hash.atlassian_pbkdf2_sha1.html">JIRA's passlib</a>
544     * @param algorithm the algorithm to use
545     * @param password the credentials
546     * @param salt the optional salt
547     * @return the digested credentials
548     */
549    private static byte[] generatePbkdf2Hash( byte[] credentials, LdapSecurityConstants algorithm, byte[] salt )
550    {
551        try
552        {
553            SecretKeyFactory sk = SecretKeyFactory.getInstance( algorithm.getAlgorithm() );
554            char[] password = Strings.utf8ToString( credentials ).toCharArray();
555            KeySpec keySpec = new PBEKeySpec( password, salt, 10000, PKCS5S2_LENGTH * 8 );
556            Key key = sk.generateSecret( keySpec );
557            return key.getEncoded();
558        }
559        catch( Exception e )
560        {
561            throw new RuntimeException( e );
562        }
563    }
564
565    
566    /**
567     * Gets the credentials from a PKCS5S2 hash.
568     * The salt for PKCS5S2 hash is prepended to the password
569     */
570    private static byte[] getPbkdf2Credentials( byte[] credentials, int algoLength, EncryptionMethod encryptionMethod )
571    {
572        try
573        {
574            // The password is associated with a salt. Decompose it
575            // in two parts, after having decoded the password.
576            // The salt will be stored into the EncryptionMethod structure
577            // The salt is at the *beginning* of the credentials, and is 16 bytes long
578            byte[] passwordAndSalt = Base64.decode( new String( credentials, algoLength, credentials.length
579                - algoLength, "UTF-8" ).toCharArray() );
580
581            int saltLength = passwordAndSalt.length - PKCS5S2_LENGTH;
582            encryptionMethod.setSalt( new byte[saltLength] );
583            byte[] password = new byte[PKCS5S2_LENGTH];
584            
585            split( passwordAndSalt, 0, encryptionMethod.getSalt(), password );
586
587            return password;
588        }
589        catch ( UnsupportedEncodingException uee )
590        {
591            // do nothing
592            return credentials;
593        }
594    }
595    
596}