blob: d4739db50e8f4e75c30c5c5896348ad3f37750e9 [file] [log] [blame]
/*
* or more contributor license agreements. See the NOTICE file
* Licensed to the Apache Software Foundation (ASF) under one
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
package org.apache.directory.api.ldap.model.password;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.Date;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import org.apache.commons.codec.digest.Crypt;
import org.apache.directory.api.i18n.I18n;
import org.apache.directory.api.ldap.model.constants.LdapSecurityConstants;
import org.apache.directory.api.util.Base64;
import org.apache.directory.api.util.DateUtils;
import org.apache.directory.api.util.TimeProvider;
import org.apache.directory.api.util.Strings;
/**
* A utility class containing methods related to processing passwords.
*
* @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
*/
public final class PasswordUtil
{
/** The SHA1 hash length */
public static final int SHA1_LENGTH = 20;
/** The SHA256 hash length */
public static final int SHA256_LENGTH = 32;
/** The SHA384 hash length */
public static final int SHA384_LENGTH = 48;
/** The SHA512 hash length */
public static final int SHA512_LENGTH = 64;
/** The MD5 hash length */
public static final int MD5_LENGTH = 16;
/** The PKCS5S2 hash length */
public static final int PKCS5S2_LENGTH = 32;
/** The CRYPT (DES) hash length */
public static final int CRYPT_LENGTH = 11;
/** The CRYPT (MD5) hash length */
public static final int CRYPT_MD5_LENGTH = 22;
/** The CRYPT (SHA-256) hash length */
public static final int CRYPT_SHA256_LENGTH = 43;
/** The CRYPT (SHA-512) hash length */
public static final int CRYPT_SHA512_LENGTH = 86;
/** The CRYPT (BCrypt) hash length */
public static final int CRYPT_BCRYPT_LENGTH = 31;
private static final byte[] CRYPT_SALT_CHARS = Strings
.getBytesUtf8( "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" );
private PasswordUtil()
{
}
/**
* Get the algorithm from the stored password.
* It can be found on the beginning of the stored password, between
* curly brackets.
* @param credentials the credentials of the user
* @return the name of the algorithm to use
*/
public static LdapSecurityConstants findAlgorithm( byte[] credentials )
{
if ( ( credentials == null ) || ( credentials.length == 0 ) )
{
return null;
}
if ( credentials[0] == '{' )
{
// get the algorithm
int pos = 1;
while ( pos < credentials.length )
{
if ( credentials[pos] == '}' )
{
break;
}
pos++;
}
if ( pos < credentials.length )
{
if ( pos == 1 )
{
// We don't have an algorithm : return the credentials as is
return null;
}
String algorithm = Strings.toLowerCaseAscii( Strings.utf8ToString( credentials, 1, pos - 1 ) );
// support for crypt additional encryption algorithms (e.g. {crypt}$1$salt$ez2vlPGdaLYkJam5pWs/Y1)
if ( credentials.length > pos + 3 && credentials[pos + 1] == '$'
&& Character.isDigit( credentials[pos + 2] ) )
{
if ( credentials[pos + 3] == '$' )
{
algorithm += Strings.utf8ToString( credentials, pos + 1, 3 );
}
else if ( credentials.length > pos + 4 && credentials[pos + 4] == '$' )
{
algorithm += Strings.utf8ToString( credentials, pos + 1, 4 );
}
}
return LdapSecurityConstants.getAlgorithm( algorithm );
}
else
{
// We don't have an algorithm
return null;
}
}
else
{
// No '{algo}' part
return null;
}
}
/**
* @see #createStoragePassword(byte[], LdapSecurityConstants)
*
* @param credentials The password
* @param algorithm The algorithm to use
* @return The resulting byte[] containing the paswword
*/
public static byte[] createStoragePassword( String credentials, LdapSecurityConstants algorithm )
{
return createStoragePassword( Strings.getBytesUtf8( credentials ), algorithm );
}
/**
* create a hashed password in a format that can be stored in the server.
* If the specified algorithm requires a salt then a random salt of 8 byte size is used
*
* @param credentials the plain text password
* @param algorithm the hashing algorithm to be applied
* @return the password after hashing with the given algorithm
*/
public static byte[] createStoragePassword( byte[] credentials, LdapSecurityConstants algorithm )
{
// check plain text password
if ( algorithm == null )
{
return credentials;
}
byte[] salt;
switch ( algorithm )
{
case HASH_METHOD_SSHA:
case HASH_METHOD_SSHA256:
case HASH_METHOD_SSHA384:
case HASH_METHOD_SSHA512:
case HASH_METHOD_SMD5:
// we use 8 byte salt always except for "crypt" which needs 2 byte salt
salt = new byte[8];
new SecureRandom().nextBytes( salt );
break;
case HASH_METHOD_PKCS5S2:
// we use 16 byte salt for PKCS5S2
salt = new byte[16];
new SecureRandom().nextBytes( salt );
break;
case HASH_METHOD_CRYPT:
salt = generateCryptSalt( 2 );
break;
case HASH_METHOD_CRYPT_MD5:
case HASH_METHOD_CRYPT_SHA256:
case HASH_METHOD_CRYPT_SHA512:
salt = generateCryptSalt( 8 );
break;
case HASH_METHOD_CRYPT_BCRYPT:
salt = Strings.getBytesUtf8( BCrypt.genSalt() );
break;
default:
salt = null;
}
byte[] hashedPassword = encryptPassword( credentials, algorithm, salt );
StringBuilder sb = new StringBuilder();
sb.append( '{' ).append( Strings.upperCase( algorithm.getPrefix() ) ).append( '}' );
if ( algorithm == LdapSecurityConstants.HASH_METHOD_CRYPT
|| algorithm == LdapSecurityConstants.HASH_METHOD_CRYPT_BCRYPT )
{
sb.append( Strings.utf8ToString( salt ) );
sb.append( Strings.utf8ToString( hashedPassword ) );
}
else if ( algorithm == LdapSecurityConstants.HASH_METHOD_CRYPT_MD5
|| algorithm == LdapSecurityConstants.HASH_METHOD_CRYPT_SHA256
|| algorithm == LdapSecurityConstants.HASH_METHOD_CRYPT_SHA512 )
{
sb.append( algorithm.getSubPrefix() );
sb.append( Strings.utf8ToString( salt ) );
sb.append( '$' );
sb.append( Strings.utf8ToString( hashedPassword ) );
}
else if ( salt != null )
{
byte[] hashedPasswordWithSaltBytes = new byte[hashedPassword.length + salt.length];
if ( algorithm == LdapSecurityConstants.HASH_METHOD_PKCS5S2 )
{
merge( hashedPasswordWithSaltBytes, salt, hashedPassword );
}
else
{
merge( hashedPasswordWithSaltBytes, hashedPassword, salt );
}
sb.append( String.valueOf( Base64.encode( hashedPasswordWithSaltBytes ) ) );
}
else
{
sb.append( String.valueOf( Base64.encode( hashedPassword ) ) );
}
return Strings.getBytesUtf8( sb.toString() );
}
/**
*
* Compare the credentials.
* We have at least 6 algorithms to encrypt the password :
* <ul>
* <li>- SHA</li>
* <li>- SSHA (salted SHA)</li>
* <li>- SHA-2(256, 384 and 512 and their salted versions)</li>
* <li>- MD5</li>
* <li>- SMD5 (slated MD5)</li>
* <li>- PKCS5S2 (PBKDF2)</li>
* <li>- crypt (unix crypt)</li>
* <li>- plain text, ie no encryption.</li>
* </ul>
* <p>
* If we get an encrypted password, it is prefixed by the used algorithm, between
* brackets : {SSHA}password ...
* </p>
* If the password is using SSHA, SMD5 or crypt, some 'salt' is added to the password :
* <ul>
* <li>- length(password) - 20, starting at 21st position for SSHA</li>
* <li>- length(password) - 16, starting at 16th position for SMD5</li>
* <li>- length(password) - 2, starting at 3rd position for crypt</li>
* </ul>
* <p>
* For (S)SHA, SHA-256 and (S)MD5, we have to transform the password from Base64 encoded text
* to a byte[] before comparing the password with the stored one.
* </p>
* <p>
* For PKCS5S2 the salt is stored in the beginning of the password
* </p>
* <p>
* For crypt, we only have to remove the salt.
* </p>
* <p>
* At the end, we use the digest() method for (S)SHA and (S)MD5, the crypt() method for
* the CRYPT algorithm and a straight comparison for PLAIN TEXT passwords.
* </p>
* <p>
* The stored password is always using the unsalted form, and is stored as a bytes array.
* </p>
*
* @param receivedCredentials the credentials provided by user
* @param storedCredentials the credentials stored in the server
* @return true if they are equal, false otherwise
*/
public static boolean compareCredentials( byte[] receivedCredentials, byte[] storedCredentials )
{
LdapSecurityConstants algorithm = findAlgorithm( storedCredentials );
if ( algorithm != null )
{
// Let's get the encrypted part of the stored password
// We should just keep the password, excluding the algorithm
// and the salt, if any.
// But we should also get the algorithm and salt to
// be able to encrypt the submitted user password in the next step
PasswordDetails passwordDetails = PasswordUtil.splitCredentials( storedCredentials );
// Reuse the saltedPassword information to construct the encrypted
// password given by the user.
byte[] userPassword = PasswordUtil.encryptPassword( receivedCredentials, passwordDetails.getAlgorithm(),
passwordDetails.getSalt() );
return compareBytes( userPassword, passwordDetails.getPassword() );
}
else
{
return compareBytes( receivedCredentials, storedCredentials );
}
}
/**
* Compare two byte[] in a constant time. This is necessary because using an Array.equals() is
* not Timing attack safe ([1], [2] and [3]), a breach that can be exploited to break some hashes.
*
* [1] https://en.wikipedia.org/wiki/Timing_attack
* [2] http://rdist.root.org/2009/05/28/timing-attack-in-google-keyczar-library/
* [3] https://cryptocoding.net/index.php/Coding_rules
*
* @param provided The provided password
* @param stored The stored password
* @return <tt>true</tt> if the compared passwords are equal
*/
private static boolean compareBytes( byte[] provided, byte[] stored )
{
if ( stored == null )
{
return provided == null;
}
else if ( provided == null )
{
return false;
}
// Now, compare the two passwords, using a constant time method
if ( stored.length != provided.length )
{
return false;
}
// loop on *every* byte in both passwords, and at the end, if one char at least is different, return false.
int result = 0;
for ( int i = 0; i < stored.length; i++ )
{
// If both bytes are equal, xor will be == 0, otherwise it will be != 0 and so will result.
result |= ( stored[i] ^ provided[i] );
}
return result == 0;
}
/**
* encrypts the given credentials based on the algorithm name and optional salt
*
* @param credentials the credentials to be encrypted
* @param algorithm the algorithm to be used for encrypting the credentials
* @param salt value to be used as salt (optional)
* @return the encrypted credentials
*/
private static byte[] encryptPassword( byte[] credentials, LdapSecurityConstants algorithm, byte[] salt )
{
switch ( algorithm )
{
case HASH_METHOD_SHA:
case HASH_METHOD_SSHA:
return digest( LdapSecurityConstants.HASH_METHOD_SHA, credentials, salt );
case HASH_METHOD_SHA256:
case HASH_METHOD_SSHA256:
return digest( LdapSecurityConstants.HASH_METHOD_SHA256, credentials, salt );
case HASH_METHOD_SHA384:
case HASH_METHOD_SSHA384:
return digest( LdapSecurityConstants.HASH_METHOD_SHA384, credentials, salt );
case HASH_METHOD_SHA512:
case HASH_METHOD_SSHA512:
return digest( LdapSecurityConstants.HASH_METHOD_SHA512, credentials, salt );
case HASH_METHOD_MD5:
case HASH_METHOD_SMD5:
return digest( LdapSecurityConstants.HASH_METHOD_MD5, credentials, salt );
case HASH_METHOD_CRYPT:
String saltWithCrypted = Crypt.crypt( Strings.utf8ToString( credentials ), Strings
.utf8ToString( salt ) );
String crypted = saltWithCrypted.substring( 2 );
return Strings.getBytesUtf8( crypted );
case HASH_METHOD_CRYPT_MD5:
case HASH_METHOD_CRYPT_SHA256:
case HASH_METHOD_CRYPT_SHA512:
String saltWithCrypted2 = Crypt.crypt( Strings.utf8ToString( credentials ),
algorithm.getSubPrefix() + Strings.utf8ToString( salt ) );
String crypted2 = saltWithCrypted2.substring( saltWithCrypted2.lastIndexOf( '$' ) + 1 );
return Strings.getBytesUtf8( crypted2 );
case HASH_METHOD_CRYPT_BCRYPT:
String crypted3 = BCrypt.hashPw( Strings.utf8ToString( credentials ), Strings.utf8ToString( salt ) );
return Strings.getBytesUtf8( crypted3.substring( crypted3.length() - 31 ) );
case HASH_METHOD_PKCS5S2:
return generatePbkdf2Hash( credentials, algorithm, salt );
default:
return credentials;
}
}
/**
* Compute the hashed password given an algorithm, the credentials and
* an optional salt.
*
* @param algorithm the algorithm to use
* @param password the credentials
* @param salt the optional salt
* @return the digested credentials
*/
private static byte[] digest( LdapSecurityConstants algorithm, byte[] password, byte[] salt )
{
MessageDigest digest;
try
{
digest = MessageDigest.getInstance( algorithm.getAlgorithm() );
}
catch ( NoSuchAlgorithmException e1 )
{
return null;
}
if ( salt != null )
{
digest.update( password );
digest.update( salt );
return digest.digest();
}
else
{
return digest.digest( password );
}
}
/**
* Decompose the stored password in an algorithm, an eventual salt
* and the password itself.
*
* If the algorithm is SHA, SSHA, MD5 or SMD5, the part following the algorithm
* is base64 encoded
*
* @param credentials The byte[] containing the credentials to split
* @return The password
*/
public static PasswordDetails splitCredentials( byte[] credentials )
{
LdapSecurityConstants algorithm = findAlgorithm( credentials );
// check plain text password
if ( algorithm == null )
{
return new PasswordDetails( null, null, credentials );
}
int algoLength = algorithm.getPrefix().length() + 2;
byte[] password;
switch ( algorithm )
{
case HASH_METHOD_MD5:
case HASH_METHOD_SMD5:
return getCredentials( credentials, algoLength, MD5_LENGTH, algorithm );
case HASH_METHOD_SHA:
case HASH_METHOD_SSHA:
return getCredentials( credentials, algoLength, SHA1_LENGTH, algorithm );
case HASH_METHOD_SHA256:
case HASH_METHOD_SSHA256:
return getCredentials( credentials, algoLength, SHA256_LENGTH, algorithm );
case HASH_METHOD_SHA384:
case HASH_METHOD_SSHA384:
return getCredentials( credentials, algoLength, SHA384_LENGTH, algorithm );
case HASH_METHOD_SHA512:
case HASH_METHOD_SSHA512:
return getCredentials( credentials, algoLength, SHA512_LENGTH, algorithm );
case HASH_METHOD_PKCS5S2:
return getPbkdf2Credentials( credentials, algoLength, algorithm );
case HASH_METHOD_CRYPT:
// The password is associated with a salt. Decompose it
// in two parts, no decoding required.
// The salt comes first, not like for SSHA and SMD5, and is 2 bytes long
// The algorithm, salt, and password will be stored into the PasswordDetails structure.
byte[] salt = new byte[2];
password = new byte[credentials.length - salt.length - algoLength];
split( credentials, algoLength, salt, password );
return new PasswordDetails( algorithm, salt, password );
case HASH_METHOD_CRYPT_BCRYPT:
salt = Arrays.copyOfRange( credentials, algoLength, credentials.length - 31 );
password = Arrays.copyOfRange( credentials, credentials.length - 31, credentials.length );
return new PasswordDetails( algorithm, salt, password );
case HASH_METHOD_CRYPT_MD5:
case HASH_METHOD_CRYPT_SHA256:
case HASH_METHOD_CRYPT_SHA512:
// skip $x$
algoLength = algoLength + 3;
return getCryptCredentials( credentials, algoLength, algorithm );
default:
// unknown method
throw new IllegalArgumentException( I18n.err( I18n.ERR_13010_UNKNOWN_HASH_ALGO, algorithm ) );
}
}
/**
* Compute the credentials
*
* @param credentials the credentials
* @param algoLength The algorithm length
* @param hashLen The hash length
* @param algorithm the algorithm to use
* @return The split password string, containing the credentials, the salt and the password
*/
private static PasswordDetails getCredentials( byte[] credentials, int algoLength, int hashLen,
LdapSecurityConstants algorithm )
{
// The password is associated with a salt. Decompose it
// in two parts, after having decoded the password.
// The salt is at the end of the credentials.
// The algorithm, salt, and password will be stored into the PasswordDetails structure.
byte[] passwordAndSalt = Base64
.decode( Strings.utf8ToString( credentials, algoLength, credentials.length - algoLength ).toCharArray() );
int saltLength = passwordAndSalt.length - hashLen;
byte[] salt = saltLength == 0 ? null : new byte[saltLength];
byte[] password = new byte[hashLen];
split( passwordAndSalt, 0, password, salt );
return new PasswordDetails( algorithm, salt, password );
}
private static void split( byte[] all, int offset, byte[] left, byte[] right )
{
System.arraycopy( all, offset, left, 0, left.length );
if ( right != null )
{
System.arraycopy( all, offset + left.length, right, 0, right.length );
}
}
private static void merge( byte[] all, byte[] left, byte[] right )
{
System.arraycopy( left, 0, all, 0, left.length );
System.arraycopy( right, 0, all, left.length, right.length );
}
/**
* checks if the given password's change time is older than the max age
*
* @param pwdChangedZtime time when the password was last changed
* @param pwdMaxAgeSec the max age value in seconds
* @param timeProvider The TimeProvider instance to use
* @return true if expired, false otherwise
*/
public static boolean isPwdExpired( String pwdChangedZtime, int pwdMaxAgeSec, TimeProvider timeProvider )
{
Date pwdChangeDate = DateUtils.getDate( pwdChangedZtime );
//DIRSERVER-1735
long time = pwdMaxAgeSec * 1000L;
time += pwdChangeDate.getTime();
Date expiryDate = DateUtils.getDate( DateUtils.getGeneralizedTime( time ) );
Date now = DateUtils.getDate( DateUtils.getGeneralizedTime( timeProvider ) );
boolean expired = false;
if ( expiryDate.equals( now ) || expiryDate.before( now ) )
{
expired = true;
}
return expired;
}
/**
* generates a hash based on the <a href="http://en.wikipedia.org/wiki/PBKDF2">PKCS5S2 spec</a>
*
* Note: this has been implemented to generate hashes compatible with what JIRA generates.
* See the <a href="http://pythonhosted.org/passlib/lib/passlib.hash.atlassian_pbkdf2_sha1.html">JIRA's passlib</a>
*
* @param credentials the credentials
* @param algorithm the algorithm to use
* @param salt the optional salt
* @return the digested credentials
*/
private static byte[] generatePbkdf2Hash( byte[] credentials, LdapSecurityConstants algorithm, byte[] salt )
{
try
{
SecretKeyFactory sk = SecretKeyFactory.getInstance( algorithm.getAlgorithm() );
char[] password = Strings.utf8ToString( credentials ).toCharArray();
KeySpec keySpec = new PBEKeySpec( password, salt, 10000, PKCS5S2_LENGTH * 8 );
Key key = sk.generateSecret( keySpec );
return key.getEncoded();
}
catch ( Exception e )
{
throw new RuntimeException( e );
}
}
/**
* Gets the credentials from a PKCS5S2 hash.
* The salt for PKCS5S2 hash is prepended to the password
*
* @param credentials The password
* @param algoLength The length of the algorithm part
* @param algorithm The algorithm in use
* @return The split credentials, containing the algorithm, the salt and the password
*/
private static PasswordDetails getPbkdf2Credentials( byte[] credentials, int algoLength, LdapSecurityConstants algorithm )
{
// The password is associated with a salt. Decompose it
// in two parts, after having decoded the password.
// The salt is at the *beginning* of the credentials, and is 16 bytes long
// The algorithm, salt, and password will be stored into the PasswordDetails structure.
byte[] passwordAndSalt = Base64
.decode( Strings.utf8ToString( credentials, algoLength, credentials.length - algoLength ).toCharArray() );
int saltLength = passwordAndSalt.length - PKCS5S2_LENGTH;
byte[] salt = new byte[saltLength];
byte[] password = new byte[PKCS5S2_LENGTH];
split( passwordAndSalt, 0, salt, password );
return new PasswordDetails( algorithm, salt, password );
}
private static byte[] generateCryptSalt( int length )
{
byte[] salt = new byte[length];
SecureRandom sr = new SecureRandom();
for ( int i = 0; i < salt.length; i++ )
{
salt[i] = CRYPT_SALT_CHARS[sr.nextInt( CRYPT_SALT_CHARS.length )];
}
return salt;
}
private static PasswordDetails getCryptCredentials( byte[] credentials, int algoLength,
LdapSecurityConstants algorithm )
{
// The password is associated with a salt. Decompose it
// in two parts, no decoding required.
// The salt length is dynamic, between the 2nd and 3rd '$'.
// The algorithm, salt, and password will be stored into the PasswordDetails structure.
// skip {crypt}$x$
int pos = algoLength;
while ( pos < credentials.length )
{
if ( credentials[pos] == '$' )
{
break;
}
pos++;
}
byte[] salt = Arrays.copyOfRange( credentials, algoLength, pos );
byte[] password = Arrays.copyOfRange( credentials, pos + 1, credentials.length );
return new PasswordDetails( algorithm, salt, password );
}
}