opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/PBKDF2HmacSHA256PasswordStorageSchemeConfiguration.xml
New file @@ -0,0 +1,48 @@ <?xml version="1.0" encoding="UTF-8"?> <!-- The contents of this file are subject to the terms of the Common Development and Distribution License (the License). You may not use this file except in compliance with the License. You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the specific language governing permission and limitations under the License. When distributing Covered Software, include this CDDL Header Notice in each file and include the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL Header, with the fields enclosed by brackets [] replaced by your own identifying information: "Portions Copyright [year] [name of copyright owner]". Copyright 2013 ForgeRock AS. ! --> <adm:managed-object name="pbkdf2-hmac-sha256-password-storage-scheme" plural-name="pbkdf2-hmac-sha256-password-storage-schemes" package="org.forgerock.opendj.server.config" extends="pbkdf2-password-storage-scheme" xmlns:adm="http://opendj.forgerock.org/admin" xmlns:ldap="http://opendj.forgerock.org/admin-ldap"> <adm:synopsis> The <adm:user-friendly-name /> provides a mechanism for encoding user passwords using the PBKDF2-HMAC-SHA256 message digest algorithm. </adm:synopsis> <adm:description> This scheme contains an implementation for the user password syntax, with a storage scheme name of "PBKDF2-HMAC-SHA256". </adm:description> <adm:profile name="ldap"> <ldap:object-class> <ldap:name>ds-cfg-pbkdf2-hmac-sha256-password-storage-scheme</ldap:name> <ldap:superior>ds-cfg-pbkdf2-password-storage-scheme</ldap:superior> </ldap:object-class> </adm:profile> <adm:property-override name="java-class" advanced="true"> <adm:default-behavior> <adm:defined> <adm:value> org.opends.server.extensions.PBKDF2HmacSHA256PasswordStorageScheme </adm:value> </adm:defined> </adm:default-behavior> </adm:property-override> </adm:managed-object> opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/PBKDF2HmacSHA512PasswordStorageSchemeConfiguration.xml
New file @@ -0,0 +1,48 @@ <?xml version="1.0" encoding="UTF-8"?> <!-- The contents of this file are subject to the terms of the Common Development and Distribution License (the License). You may not use this file except in compliance with the License. You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the specific language governing permission and limitations under the License. When distributing Covered Software, include this CDDL Header Notice in each file and include the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL Header, with the fields enclosed by brackets [] replaced by your own identifying information: "Portions Copyright [year] [name of copyright owner]". Copyright 2013 ForgeRock AS. ! --> <adm:managed-object name="pbkdf2-hmac-sha512-password-storage-scheme" plural-name="pbkdf2-hmac-sha512-password-storage-schemes" package="org.forgerock.opendj.server.config" extends="pbkdf2-password-storage-scheme" xmlns:adm="http://opendj.forgerock.org/admin" xmlns:ldap="http://opendj.forgerock.org/admin-ldap"> <adm:synopsis> The <adm:user-friendly-name /> provides a mechanism for encoding user passwords using the PBKDF2-HMAC-SHA512 message digest algorithm. </adm:synopsis> <adm:description> This scheme contains an implementation for the user password syntax, with a storage scheme name of "PBKDF2-HMAC-SHA512". </adm:description> <adm:profile name="ldap"> <ldap:object-class> <ldap:name>ds-cfg-pbkdf2-hmac-sha512-password-storage-scheme</ldap:name> <ldap:superior>ds-cfg-pbkdf2-password-storage-scheme</ldap:superior> </ldap:object-class> </adm:profile> <adm:property-override name="java-class" advanced="true"> <adm:default-behavior> <adm:defined> <adm:value> org.opends.server.extensions.PBKDF2HmacSHA512PasswordStorageScheme </adm:value> </adm:defined> </adm:default-behavior> </adm:property-override> </adm:managed-object> opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/PBKDF2PasswordStorageSchemeConfiguration.xml
@@ -24,7 +24,7 @@ The <adm:user-friendly-name /> provides a mechanism for encoding user passwords using the PBKDF2 message digest algorithm. PBKDF2 message digest algorithm. PBKDF2 is the shortname for PBKDF2-HMAC-SHA1. </adm:synopsis> <adm:description> This scheme contains an implementation for the user password syntax, opendj-server-legacy/resource/config/config.ldif
@@ -1119,6 +1119,24 @@ ds-cfg-java-class: org.opends.server.extensions.PBKDF2PasswordStorageScheme ds-cfg-enabled: true dn: cn=PBKDF2-HMAC-SHA256,cn=Password Storage Schemes,cn=config objectClass: top objectClass: ds-cfg-password-storage-scheme objectClass: ds-cfg-pbkdf2-password-storage-scheme objectClass: ds-cfg-pbkdf2-hmac-sha256-password-storage-scheme cn: PBKDF2-HMAC-SHA256 ds-cfg-java-class: org.opends.server.extensions.PBKDF2HmacSHA256PasswordStorageScheme ds-cfg-enabled: true dn: cn=PBKDF2-HMAC-SHA512,cn=Password Storage Schemes,cn=config objectClass: top objectClass: ds-cfg-password-storage-scheme objectClass: ds-cfg-pbkdf2-password-storage-scheme objectClass: ds-cfg-pbkdf2-hmac-sha512-password-storage-scheme cn: PBKDF2-HMAC-SHA512 ds-cfg-java-class: org.opends.server.extensions.PBKDF2HmacSHA512PasswordStorageScheme ds-cfg-enabled: true dn: cn=PKCS5S2,cn=Password Storage Schemes,cn=config objectClass: top objectClass: ds-cfg-password-storage-scheme opendj-server-legacy/src/admin/messages/PBKDF2HmacSHA256PasswordStorageSchemeCfgDefn.properties
New file @@ -0,0 +1,7 @@ user-friendly-name=PBKDF2-HMAC-SHA256 Password Storage Scheme user-friendly-plural-name=PBKDF2-HMAC-SHA256 Password Storage Schemes synopsis=The PBKDF2-HMAC-SHA256 Password Storage Scheme provides a mechanism for encoding user passwords using the PBKDF2-HMAC-SHA256 message digest algorithm. description=This scheme contains an implementation for the user password syntax, with a storage scheme name of "PBKDF2-HMAC-SHA256". property.enabled.synopsis=Indicates whether the PBKDF2-HMAC-SHA256 Password Storage Scheme is enabled for use. property.java-class.synopsis=Specifies the fully-qualified name of the Java class that provides the PBKDF2-HMAC-SHA256 Password Storage Scheme implementation. property.pbkdf2-iterations.synopsis=The number of algorithm iterations to make. NIST recommends at least 1000. opendj-server-legacy/src/admin/messages/PBKDF2HmacSHA512PasswordStorageSchemeCfgDefn.properties
New file @@ -0,0 +1,7 @@ user-friendly-name=PBKDF2-HMAC-SHA512 Password Storage Scheme user-friendly-plural-name=PBKDF2-HMAC-SHA512 Password Storage Schemes synopsis=The PBKDF2-HMAC-SHA512 Password Storage Scheme provides a mechanism for encoding user passwords using the PBKDF2-HMAC-SHA512 message digest algorithm. description=This scheme contains an implementation for the user password syntax, with a storage scheme name of "PBKDF2-HMAC-SHA512". property.enabled.synopsis=Indicates whether the PBKDF2-HMAC-SHA512 Password Storage Scheme is enabled for use. property.java-class.synopsis=Specifies the fully-qualified name of the Java class that provides the PBKDF2-HMAC-SHA512 Password Storage Scheme implementation. property.pbkdf2-iterations.synopsis=The number of algorithm iterations to make. NIST recommends at least 1000. opendj-server-legacy/src/admin/messages/PBKDF2PasswordStorageSchemeCfgDefn.properties
@@ -1,6 +1,6 @@ user-friendly-name=PBKDF2 Password Storage Scheme user-friendly-plural-name=PBKDF2 Password Storage Schemes synopsis=The PBKDF2 Password Storage Scheme provides a mechanism for encoding user passwords using the PBKDF2 message digest algorithm. synopsis=The PBKDF2 Password Storage Scheme provides a mechanism for encoding user passwords using the PBKDF2 message digest algorithm. PBKDF2 is the shortname for PBKDF2-HMAC-SHA1. description=This scheme contains an implementation for the user password syntax, with a storage scheme name of "PBKDF2". property.enabled.synopsis=Indicates whether the PBKDF2 Password Storage Scheme is enabled for use. property.java-class.synopsis=Specifies the fully-qualified name of the Java class that provides the PBKDF2 Password Storage Scheme implementation. opendj-server-legacy/src/main/java/org/opends/server/extensions/AbstractPBKDF2PasswordStorageScheme.java
New file @@ -0,0 +1,338 @@ package org.opends.server.extensions; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.config.server.ConfigChangeResult; import org.forgerock.opendj.config.server.ConfigException; import org.forgerock.opendj.config.server.ConfigurationChangeListener; import org.forgerock.opendj.ldap.Base64; import org.forgerock.opendj.ldap.ByteSequence; import org.forgerock.opendj.ldap.ByteString; import org.forgerock.opendj.ldap.ResultCode; import org.forgerock.opendj.server.config.server.PBKDF2PasswordStorageSchemeCfg; import org.opends.server.api.PasswordStorageScheme; import org.opends.server.core.DirectoryServer; import org.opends.server.types.DirectoryException; import org.opends.server.types.InitializationException; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; import java.util.Arrays; import java.util.List; import static org.opends.messages.ExtensionMessages.*; import static org.opends.server.util.StaticUtils.getExceptionMessage; abstract class AbstractPBKDF2PasswordStorageScheme extends PasswordStorageScheme<PBKDF2PasswordStorageSchemeCfg> implements ConfigurationChangeListener<PBKDF2PasswordStorageSchemeCfg> { private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); /** The number of bytes of random data to use as the salt when generating the hashes. */ private static final int NUM_SALT_BYTES = 8; /** The chosen digest algorithm. */ private SecretKeyFactory keyFactory; /** The secure random number generator to use to generate the salt values. */ private SecureRandom random; /** The current configuration for this storage scheme. */ private volatile PBKDF2PasswordStorageSchemeCfg config; /** * Creates a new instance of this password storage scheme. Note that no * initialization should be performed here, as all initialization should be * done in the <CODE>initializePasswordStorageScheme</CODE> method. */ public AbstractPBKDF2PasswordStorageScheme() { super(); } @Override public void initializePasswordStorageScheme(PBKDF2PasswordStorageSchemeCfg configuration) throws ConfigException, InitializationException { try { random = new SecureRandom(); // Initialize the digest algorithm keyFactory = SecretKeyFactory.getInstance(getMessageDigestAlgorithm()); } catch (NoSuchAlgorithmException e) { throw new InitializationException(null); } this.config = configuration; config.addPBKDF2ChangeListener(this); } @Override public boolean isConfigurationChangeAcceptable(PBKDF2PasswordStorageSchemeCfg configuration, List<LocalizableMessage> unacceptableReasons) { return true; } @Override public ConfigChangeResult applyConfigurationChange(PBKDF2PasswordStorageSchemeCfg configuration) { this.config = configuration; return new ConfigChangeResult(); } @Override public abstract String getStorageSchemeName(); @Override public ByteString encodePassword(ByteSequence plaintext) throws DirectoryException { byte[] saltBytes = new byte[NUM_SALT_BYTES]; int iterations = config.getPBKDF2Iterations(); SecretKey digest = encodeWithRandomSalt(plaintext, saltBytes, iterations); byte[] hashPlusSalt = concatenateHashPlusSalt(saltBytes, digest.getEncoded()); return ByteString.valueOfUtf8(iterations + ":" + Base64.encode(hashPlusSalt)); } @Override public ByteString encodePasswordWithScheme(ByteSequence plaintext) throws DirectoryException { return ByteString.valueOfUtf8('{' + getStorageSchemeName() + '}' + encodePassword(plaintext)); } @Override public boolean passwordMatches(ByteSequence plaintextPassword, ByteSequence storedPassword) { // Split the iterations from the stored value (separated by a ':') // Base64-decode the remaining value and take the last bytes as the salt. try { final String stored = storedPassword.toString(); final int pos = stored.indexOf(':'); if (pos == -1) { throw new Exception(); } final int iterations = Integer.parseInt(stored.substring(0, pos)); byte[] decodedBytes = Base64.decode(stored.substring(pos + 1)).toByteArray(); int digestLength = getDigestSize(); final int saltLength = decodedBytes.length - digestLength; if (saltLength <= 0) { logger.error(ERR_PWSCHEME_INVALID_BASE64_DECODED_STORED_PASSWORD, storedPassword); return false; } final byte[] digestBytes = new byte[digestLength]; final byte[] saltBytes = new byte[saltLength]; System.arraycopy(decodedBytes, 0, digestBytes, 0, digestLength); System.arraycopy(decodedBytes, digestLength, saltBytes, 0, saltLength); return encodeAndMatch(plaintextPassword, saltBytes, digestBytes, iterations); } catch (Exception e) { logger.traceException(e); logger.error(ERR_PWSCHEME_CANNOT_BASE64_DECODE_STORED_PASSWORD, storedPassword, e); return false; } } @Override public boolean supportsAuthPasswordSyntax() { return true; } @Override public abstract String getAuthPasswordSchemeName(); abstract String getMessageDigestAlgorithm(); abstract int getDigestSize(); @Override public ByteString encodeAuthPassword(ByteSequence plaintext) throws DirectoryException { byte[] saltBytes = new byte[NUM_SALT_BYTES]; int iterations = config.getPBKDF2Iterations(); SecretKey digest = encodeWithRandomSalt(plaintext, saltBytes, iterations); byte[] digestBytes = digest.getEncoded(); // Encode and return the value. return ByteString.valueOfUtf8(getAuthPasswordSchemeName() + '$' + iterations + ':' + Base64.encode(saltBytes) + '$' + Base64.encode(digestBytes)); } @Override public boolean authPasswordMatches(ByteSequence plaintextPassword, String authInfo, String authValue) { try { int pos = authInfo.indexOf(':'); if (pos == -1) { throw new Exception(); } int iterations = Integer.parseInt(authInfo.substring(0, pos)); byte[] saltBytes = Base64.decode(authInfo.substring(pos + 1)).toByteArray(); byte[] digestBytes = Base64.decode(authValue).toByteArray(); return encodeAndMatch(plaintextPassword, saltBytes, digestBytes, iterations); } catch (Exception e) { logger.traceException(e); return false; } } @Override public boolean isReversible() { return false; } @Override public ByteString getPlaintextValue(ByteSequence storedPassword) throws DirectoryException { LocalizableMessage message = ERR_PWSCHEME_NOT_REVERSIBLE.get(getStorageSchemeName()); throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); } @Override public ByteString getAuthPasswordPlaintextValue(String authInfo, String authValue) throws DirectoryException { LocalizableMessage message = ERR_PWSCHEME_NOT_REVERSIBLE.get(getAuthPasswordSchemeName()); throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); } @Override public boolean isStorageSchemeSecure() { return true; } /** * Generates an encoded password string from the given clear-text password. * This method is primarily intended for use when it is necessary to generate a password with the server * offline (e.g., when setting the initial root user password). * * @param passwordBytes The bytes that make up the clear-text password. * @param schemeName The storage scheme name * @param digestAlgo The digest algorithm name * @param digestSize The digest algorithm size in bytes * @return The encoded password string, including the scheme name in curly braces. * @throws DirectoryException If a problem occurs during processing. */ public static String encodeOffline(byte[] passwordBytes, String schemeName, String digestAlgo, int digestSize) throws DirectoryException { byte[] saltBytes = new byte[NUM_SALT_BYTES]; final int iterations = 10000; SecureRandom random = new SecureRandom(); random.nextBytes(saltBytes); final ByteString password = ByteString.wrap(passwordBytes); char[] plaintextChars = password.toString().toCharArray(); PBEKeySpec spec = null; try { SecretKeyFactory factory = SecretKeyFactory.getInstance(digestAlgo); spec = new PBEKeySpec(plaintextChars, saltBytes, iterations, digestSize * 8); SecretKey digest = factory.generateSecret(spec); byte[] digestBytes = digest.getEncoded(); byte[] hashPlusSalt = concatenateHashPlusSalt(saltBytes, digestBytes); return '{' + schemeName + '}' + iterations + ':' + Base64.encode(hashPlusSalt); } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { throw cannotEncodePassword(schemeName, e); } finally { Arrays.fill(plaintextChars, '0'); if (spec != null) spec.clearPassword(); } } private SecretKey encodeWithRandomSalt(ByteString plaintext, byte[] saltBytes, int iterations) throws DirectoryException { random.nextBytes(saltBytes); return encodeWithRandomSalt(plaintext, saltBytes, iterations); } private SecretKey encodeWithSalt(ByteSequence plaintext, byte[] saltBytes, int iterations) throws DirectoryException { final char[] plaintextChars = plaintext.toString().toCharArray(); PBEKeySpec spec = null; try { spec = new PBEKeySpec(plaintextChars, saltBytes, iterations, getDigestSize() * 8); return keyFactory.generateSecret(spec); } catch (Exception e) { throw cannotEncodePassword(getStorageSchemeName(), e); } finally { Arrays.fill(plaintextChars, '0'); if (spec != null) spec.clearPassword(); } } private boolean encodeAndMatch(ByteSequence plaintext, byte[] saltBytes, byte[] digestBytes, int iterations) { try { final SecretKey userDigestBytes = encodeWithSalt(plaintext, saltBytes, iterations); return Arrays.equals(digestBytes, userDigestBytes.getEncoded()); } catch (Exception e) { return false; } } private SecretKey encodeWithRandomSalt(ByteSequence plaintext, byte[] saltBytes, int iterations) throws DirectoryException { random.nextBytes(saltBytes); return encodeWithSalt(plaintext, saltBytes, iterations); } private static DirectoryException cannotEncodePassword(String storageSchemeName, Exception e) { logger.traceException(e); LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( storageSchemeName, getExceptionMessage(e)); return new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), message, e); } private static byte[] concatenateHashPlusSalt(byte[] saltBytes, byte[] digestBytes) { final byte[] hashPlusSalt = new byte[digestBytes.length + saltBytes.length]; System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length); System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length, saltBytes.length); return hashPlusSalt; } } opendj-server-legacy/src/main/java/org/opends/server/extensions/ExtensionsConstants.java
@@ -73,6 +73,17 @@ */ public static final String AUTH_PASSWORD_SCHEME_NAME_PBKDF2 = "PBKDF2"; /** * The authentication password scheme name for use with passwords encoded in a * PBKDF2 representation. */ public static final String AUTH_PASSWORD_SCHEME_NAME_PBKDF2_HMAC_SHA256 = "PBKDF2-HMAC-SHA256"; /** * The authentication password scheme name for use with passwords encoded in a * PBKDF2 representation. */ public static final String AUTH_PASSWORD_SCHEME_NAME_PBKDF2_HMAC_SHA512 = "PBKDF2-HMAC-SHA512"; /** * The authentication password scheme name for use with passwords encoded in a @@ -128,7 +139,19 @@ public static final String MESSAGE_DIGEST_ALGORITHM_PBKDF2 = "PBKDF2WithHmacSHA1"; /** * The name of the message digest algorithm that should be used to generate * PBKDF2 hashes. */ public static final String MESSAGE_DIGEST_ALGORITHM_PBKDF2_HMAC_SHA256 = "PBKDF2WithHmacSHA256"; /** * The name of the message digest algorithm that should be used to generate * PBKDF2 hashes. */ public static final String MESSAGE_DIGEST_ALGORITHM_PBKDF2_HMAC_SHA512 = "PBKDF2WithHmacSHA512"; /** * The name of the pseudo-random number generator using SHA-1. @@ -328,6 +351,17 @@ */ public static final String STORAGE_SCHEME_NAME_PBKDF2 = "PBKDF2"; /** * The password storage scheme name that will be used for passwords stored in * a PBKDF2 representation. */ public static final String STORAGE_SCHEME_NAME_PBKDF2_HMAC_SHA256 = "PBKDF2-HMAC-SHA256"; /** * The password storage scheme name that will be used for passwords stored in * a PBKDF2 representation. */ public static final String STORAGE_SCHEME_NAME_PBKDF2_HMAC_SHA512 = "PBKDF2-HMAC-SHA512"; /** * The password storage scheme name that will be used for passwords stored in opendj-server-legacy/src/main/java/org/opends/server/extensions/PBKDF2HmacSHA256PasswordStorageScheme.java
New file @@ -0,0 +1,53 @@ /* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2013-2016 ForgeRock AS. */ package org.opends.server.extensions; import org.opends.server.types.DirectoryException; import static org.opends.server.extensions.ExtensionsConstants.*; /** * This class defines a Directory Server password storage scheme based on the * PBKDF2 algorithm defined in RFC 2898. This is a one-way digest algorithm * so there is no way to retrieve the original clear-text version of the * password from the hashed value (although this means that it is not suitable * for things that need the clear-text password like DIGEST-MD5). This * implementation uses a configurable number of iterations. */ public class PBKDF2HmacSHA256PasswordStorageScheme extends AbstractPBKDF2PasswordStorageScheme { public String getStorageSchemeName() { return STORAGE_SCHEME_NAME_PBKDF2_HMAC_SHA256; } public String getAuthPasswordSchemeName() { return AUTH_PASSWORD_SCHEME_NAME_PBKDF2_HMAC_SHA256; } String getMessageDigestAlgorithm() { return MESSAGE_DIGEST_ALGORITHM_PBKDF2_HMAC_SHA256; } int getDigestSize() { return 32; } public static String encodeOffline(byte[] passwordBytes) throws DirectoryException { return encodeOffline(passwordBytes, AUTH_PASSWORD_SCHEME_NAME_PBKDF2_HMAC_SHA256, MESSAGE_DIGEST_ALGORITHM_PBKDF2_HMAC_SHA256, 32); } } opendj-server-legacy/src/main/java/org/opends/server/extensions/PBKDF2HmacSHA512PasswordStorageScheme.java
New file @@ -0,0 +1,53 @@ /* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2013-2016 ForgeRock AS. */ package org.opends.server.extensions; import org.opends.server.types.DirectoryException; import static org.opends.server.extensions.ExtensionsConstants.*; /** * This class defines a Directory Server password storage scheme based on the * PBKDF2 algorithm defined in RFC 2898. This is a one-way digest algorithm * so there is no way to retrieve the original clear-text version of the * password from the hashed value (although this means that it is not suitable * for things that need the clear-text password like DIGEST-MD5). This * implementation uses a configurable number of iterations. */ public class PBKDF2HmacSHA512PasswordStorageScheme extends AbstractPBKDF2PasswordStorageScheme { public String getStorageSchemeName() { return STORAGE_SCHEME_NAME_PBKDF2_HMAC_SHA512; } public String getAuthPasswordSchemeName() { return AUTH_PASSWORD_SCHEME_NAME_PBKDF2_HMAC_SHA512; } String getMessageDigestAlgorithm() { return MESSAGE_DIGEST_ALGORITHM_PBKDF2_HMAC_SHA512; } int getDigestSize() { return 64; } public static String encodeOffline(byte[] passwordBytes) throws DirectoryException { return encodeOffline(passwordBytes, AUTH_PASSWORD_SCHEME_NAME_PBKDF2_HMAC_SHA512, MESSAGE_DIGEST_ALGORITHM_PBKDF2_HMAC_SHA512, 64); } } opendj-server-legacy/src/main/java/org/opends/server/extensions/PBKDF2PasswordStorageScheme.java
@@ -15,33 +15,9 @@ */ package org.opends.server.extensions; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.spec.KeySpec; import java.util.Arrays; import java.util.List; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.config.server.ConfigException; import org.forgerock.opendj.ldap.Base64; import org.forgerock.opendj.ldap.ByteSequence; import org.forgerock.opendj.ldap.ByteString; import org.forgerock.opendj.ldap.ResultCode; import org.forgerock.opendj.config.server.ConfigurationChangeListener; import org.forgerock.opendj.server.config.server.PBKDF2PasswordStorageSchemeCfg; import org.opends.server.api.PasswordStorageScheme; import org.opends.server.core.DirectoryServer; import org.forgerock.opendj.config.server.ConfigChangeResult; import org.opends.server.types.DirectoryException; import org.opends.server.types.InitializationException; import static org.opends.messages.ExtensionMessages.*; import static org.opends.server.extensions.ExtensionsConstants.*; import static org.opends.server.util.StaticUtils.*; /** * This class defines a Directory Server password storage scheme based on the @@ -52,300 +28,25 @@ * implementation uses a configurable number of iterations. */ public class PBKDF2PasswordStorageScheme extends PasswordStorageScheme<PBKDF2PasswordStorageSchemeCfg> implements ConfigurationChangeListener<PBKDF2PasswordStorageSchemeCfg> extends AbstractPBKDF2PasswordStorageScheme { private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); /** The fully-qualified name of this class. */ private static final String CLASS_NAME = "org.opends.server.extensions.PBKDF2PasswordStorageScheme"; /** The number of bytes of random data to use as the salt when generating the hashes. */ private static final int NUM_SALT_BYTES = 8; /** The number of bytes the SHA-1 algorithm produces. */ private static final int SHA1_LENGTH = 20; /** The secure random number generator to use to generate the salt values. */ private SecureRandom random; /** The current configuration for this storage scheme. */ private volatile PBKDF2PasswordStorageSchemeCfg config; /** * Creates a new instance of this password storage scheme. Note that no * initialization should be performed here, as all initialization should be * done in the <CODE>initializePasswordStorageScheme</CODE> method. */ public PBKDF2PasswordStorageScheme() { super(); } @Override public void initializePasswordStorageScheme(PBKDF2PasswordStorageSchemeCfg configuration) throws ConfigException, InitializationException { try { random = SecureRandom.getInstance(SECURE_PRNG_SHA1); // Just try to verify if the algorithm is supported. SecretKeyFactory.getInstance(MESSAGE_DIGEST_ALGORITHM_PBKDF2); } catch (NoSuchAlgorithmException e) { throw new InitializationException(null); } this.config = configuration; config.addPBKDF2ChangeListener(this); } @Override public boolean isConfigurationChangeAcceptable(PBKDF2PasswordStorageSchemeCfg configuration, List<LocalizableMessage> unacceptableReasons) { return true; } @Override public ConfigChangeResult applyConfigurationChange(PBKDF2PasswordStorageSchemeCfg configuration) { this.config = configuration; return new ConfigChangeResult(); } @Override public String getStorageSchemeName() { public String getStorageSchemeName() { return STORAGE_SCHEME_NAME_PBKDF2; } @Override public ByteString encodePassword(ByteSequence plaintext) throws DirectoryException { byte[] saltBytes = new byte[NUM_SALT_BYTES]; int iterations = config.getPBKDF2Iterations(); byte[] digestBytes = encodeWithRandomSalt(plaintext, saltBytes, iterations,random); byte[] hashPlusSalt = concatenateHashPlusSalt(saltBytes, digestBytes); return ByteString.valueOfUtf8(iterations + ":" + Base64.encode(hashPlusSalt)); } @Override public ByteString encodePasswordWithScheme(ByteSequence plaintext) throws DirectoryException { return ByteString.valueOfUtf8('{' + STORAGE_SCHEME_NAME_PBKDF2 + '}' + encodePassword(plaintext)); } @Override public boolean passwordMatches(ByteSequence plaintextPassword, ByteSequence storedPassword) { // Split the iterations from the stored value (separated by a ':') // Base64-decode the remaining value and take the last 8 bytes as the salt. try { final String stored = storedPassword.toString(); final int pos = stored.indexOf(':'); if (pos == -1) { throw new Exception(); } final int iterations = Integer.parseInt(stored.substring(0, pos)); byte[] decodedBytes = Base64.decode(stored.substring(pos + 1)).toByteArray(); final int saltLength = decodedBytes.length - SHA1_LENGTH; if (saltLength <= 0) { logger.error(ERR_PWSCHEME_INVALID_BASE64_DECODED_STORED_PASSWORD, storedPassword); return false; } final byte[] digestBytes = new byte[SHA1_LENGTH]; final byte[] saltBytes = new byte[saltLength]; System.arraycopy(decodedBytes, 0, digestBytes, 0, SHA1_LENGTH); System.arraycopy(decodedBytes, SHA1_LENGTH, saltBytes, 0, saltLength); return encodeAndMatch(plaintextPassword, saltBytes, digestBytes, iterations); } catch (Exception e) { logger.traceException(e); logger.error(ERR_PWSCHEME_CANNOT_BASE64_DECODE_STORED_PASSWORD, storedPassword, e); return false; } } @Override public boolean supportsAuthPasswordSyntax() { return true; } @Override public String getAuthPasswordSchemeName() { public String getAuthPasswordSchemeName() { return AUTH_PASSWORD_SCHEME_NAME_PBKDF2; } @Override public ByteString encodeAuthPassword(ByteSequence plaintext) throws DirectoryException { byte[] saltBytes = new byte[NUM_SALT_BYTES]; int iterations = config.getPBKDF2Iterations(); byte[] digestBytes = encodeWithRandomSalt(plaintext, saltBytes, iterations,random); // Encode and return the value. return ByteString.valueOfUtf8(AUTH_PASSWORD_SCHEME_NAME_PBKDF2 + '$' + iterations + ':' + Base64.encode(saltBytes) + '$' + Base64.encode(digestBytes)); String getMessageDigestAlgorithm() { return MESSAGE_DIGEST_ALGORITHM_PBKDF2; } @Override public boolean authPasswordMatches(ByteSequence plaintextPassword, String authInfo, String authValue) { try { int pos = authInfo.indexOf(':'); if (pos == -1) { throw new Exception(); } int iterations = Integer.parseInt(authInfo.substring(0, pos)); byte[] saltBytes = Base64.decode(authInfo.substring(pos + 1)).toByteArray(); byte[] digestBytes = Base64.decode(authValue).toByteArray(); return encodeAndMatch(plaintextPassword, saltBytes, digestBytes, iterations); } catch (Exception e) { logger.traceException(e); return false; } int getDigestSize() { return 20; } @Override public boolean isReversible() { return false; } @Override public ByteString getPlaintextValue(ByteSequence storedPassword) throws DirectoryException { LocalizableMessage message = ERR_PWSCHEME_NOT_REVERSIBLE.get(STORAGE_SCHEME_NAME_PBKDF2); throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); } @Override public ByteString getAuthPasswordPlaintextValue(String authInfo, String authValue) throws DirectoryException { LocalizableMessage message = ERR_PWSCHEME_NOT_REVERSIBLE.get(AUTH_PASSWORD_SCHEME_NAME_PBKDF2); throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); } @Override public boolean isStorageSchemeSecure() { return true; } /** * Generates an encoded password string from the given clear-text password. * This method is primarily intended for use when it is necessary to generate a password with the server * offline (e.g., when setting the initial root user password). * * @param passwordBytes The bytes that make up the clear-text password. * @return The encoded password string, including the scheme name in curly braces. * @throws DirectoryException If a problem occurs during processing. */ public static String encodeOffline(byte[] passwordBytes) throws DirectoryException { byte[] saltBytes = new byte[NUM_SALT_BYTES]; int iterations = 10000; final ByteString password = ByteString.wrap(passwordBytes); byte[] digestBytes = encodeWithRandomSalt(password, saltBytes, iterations); byte[] hashPlusSalt = concatenateHashPlusSalt(saltBytes, digestBytes); return '{' + STORAGE_SCHEME_NAME_PBKDF2 + '}' + iterations + ':' + Base64.encode(hashPlusSalt); } private static byte[] encodeWithRandomSalt(ByteString plaintext, byte[] saltBytes, int iterations) throws DirectoryException { try { final SecureRandom random = SecureRandom.getInstance(SECURE_PRNG_SHA1); return encodeWithRandomSalt(plaintext, saltBytes, iterations, random); } catch (DirectoryException e) { throw e; } catch (Exception e) { throw cannotEncodePassword(e); } } private static byte[] encodeWithSalt(ByteSequence plaintext, byte[] saltBytes, int iterations) throws DirectoryException { final char[] plaintextChars = plaintext.toString().toCharArray(); try { final SecretKeyFactory factory = SecretKeyFactory.getInstance(MESSAGE_DIGEST_ALGORITHM_PBKDF2); KeySpec spec = new PBEKeySpec(plaintextChars, saltBytes, iterations, SHA1_LENGTH * 8); return factory.generateSecret(spec).getEncoded(); } catch (Exception e) { throw cannotEncodePassword(e); } finally { Arrays.fill(plaintextChars, '0'); } } private boolean encodeAndMatch(ByteSequence plaintext, byte[] saltBytes, byte[] digestBytes, int iterations) { try { final byte[] userDigestBytes = encodeWithSalt(plaintext, saltBytes, iterations); return Arrays.equals(digestBytes, userDigestBytes); } catch (Exception e) { return false; } } private static byte[] encodeWithRandomSalt(ByteSequence plaintext, byte[] saltBytes, int iterations, SecureRandom random) throws DirectoryException { random.nextBytes(saltBytes); return encodeWithSalt(plaintext, saltBytes, iterations); } private static DirectoryException cannotEncodePassword(Exception e) { logger.traceException(e); LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(CLASS_NAME, getExceptionMessage(e)); return new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), message, e); } private static byte[] concatenateHashPlusSalt(byte[] saltBytes, byte[] digestBytes) { final byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES]; System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length); System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length, NUM_SALT_BYTES); return hashPlusSalt; public static String encodeOffline(byte[] passwordBytes) throws DirectoryException { return encodeOffline(passwordBytes, AUTH_PASSWORD_SCHEME_NAME_PBKDF2, MESSAGE_DIGEST_ALGORITHM_PBKDF2, 20); } } opendj-server-legacy/src/test/java/org/opends/server/extensions/PBKDF2HmacSHA256PasswordStorageSchemeTestCase.java
New file @@ -0,0 +1,73 @@ /* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2014-2016 ForgeRock AS. */ package org.opends.server.extensions; import org.forgerock.opendj.server.config.meta.PBKDF2PasswordStorageSchemeCfgDefn; import org.opends.server.api.PasswordStorageScheme; import org.opends.server.types.DirectoryException; import org.testng.annotations.DataProvider; /** * A set of test cases for the PBKDF2-HMAC-SHA256 password storage scheme. */ @SuppressWarnings("javadoc") public class PBKDF2HmacSHA256PasswordStorageSchemeTestCase extends PasswordStorageSchemeTestCase { /** Creates a new instance of this storage scheme test case. */ public PBKDF2HmacSHA256PasswordStorageSchemeTestCase() { super("cn=PBKDF2-HMAC-SHA256,cn=Password Storage Schemes,cn=config"); } /** * Retrieves a set of passwords that may be used to test the password storage scheme. * * @return A set of passwords that may be used to test the password storage scheme. */ @Override @DataProvider(name = "testPasswords") public Object[][] getTestPasswords() { final Object[][] testPasswords = super.getTestPasswords(); // JDK Bug 6879540. Empty passwords are not accepted when generating PBESpecKey. // The bug is present in Java 6 and some version of Java 7. final int newLength = testPasswords.length - 2; final Object[][] results = new Object[newLength][]; System.arraycopy(testPasswords, 2, results, 0, newLength); return results; } /** * Retrieves an initialized instance of this password storage scheme. * * @return An initialized instance of this password storage scheme. */ @Override protected PasswordStorageScheme<?> getScheme() throws Exception { return InitializationUtils.initializePasswordStorageScheme( new PBKDF2HmacSHA256PasswordStorageScheme(), configEntry, PBKDF2PasswordStorageSchemeCfgDefn.getInstance()); } @Override protected String encodeOffline(final byte[] plaintextBytes) throws DirectoryException { return PBKDF2HmacSHA256PasswordStorageScheme.encodeOffline(plaintextBytes); } } opendj-server-legacy/src/test/java/org/opends/server/extensions/PBKDF2HmacSHA512PasswordStorageSchemeTestCase.java
New file @@ -0,0 +1,73 @@ /* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2014-2016 ForgeRock AS. */ package org.opends.server.extensions; import org.forgerock.opendj.server.config.meta.PBKDF2PasswordStorageSchemeCfgDefn; import org.opends.server.api.PasswordStorageScheme; import org.opends.server.types.DirectoryException; import org.testng.annotations.DataProvider; /** * A set of test cases for the PBKDF2-HMAC-SHA512 password storage scheme. */ @SuppressWarnings("javadoc") public class PBKDF2HmacSHA512PasswordStorageSchemeTestCase extends PasswordStorageSchemeTestCase { /** Creates a new instance of this storage scheme test case. */ public PBKDF2HmacSHA512PasswordStorageSchemeTestCase() { super("cn=PBKDF2-HMAC-SHA512,cn=Password Storage Schemes,cn=config"); } /** * Retrieves a set of passwords that may be used to test the password storage scheme. * * @return A set of passwords that may be used to test the password storage scheme. */ @Override @DataProvider(name = "testPasswords") public Object[][] getTestPasswords() { final Object[][] testPasswords = super.getTestPasswords(); // JDK Bug 6879540. Empty passwords are not accepted when generating PBESpecKey. // The bug is present in Java 6 and some version of Java 7. final int newLength = testPasswords.length - 2; final Object[][] results = new Object[newLength][]; System.arraycopy(testPasswords, 2, results, 0, newLength); return results; } /** * Retrieves an initialized instance of this password storage scheme. * * @return An initialized instance of this password storage scheme. */ @Override protected PasswordStorageScheme<?> getScheme() throws Exception { return InitializationUtils.initializePasswordStorageScheme( new PBKDF2HmacSHA512PasswordStorageScheme(), configEntry, PBKDF2PasswordStorageSchemeCfgDefn.getInstance()); } @Override protected String encodeOffline(final byte[] plaintextBytes) throws DirectoryException { return PBKDF2HmacSHA512PasswordStorageScheme.encodeOffline(plaintextBytes); } }