| | |
| | | */ |
| | | package org.opends.server.extensions; |
| | | |
| | | import java.security.NoSuchAlgorithmException; |
| | | import java.security.SecureRandom; |
| | | import java.security.spec.KeySpec; |
| | | import java.util.Arrays; |
| | | |
| | | import javax.crypto.SecretKeyFactory; |
| | | import javax.crypto.spec.PBEKeySpec; |
| | | |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.forgerock.opendj.ldap.ByteSequence; |
| | |
| | | import org.opends.server.types.InitializationException; |
| | | import org.opends.server.util.Base64; |
| | | |
| | | import javax.crypto.SecretKeyFactory; |
| | | import javax.crypto.spec.PBEKeySpec; |
| | | import java.security.NoSuchAlgorithmException; |
| | | import java.security.SecureRandom; |
| | | import java.security.spec.KeySpec; |
| | | import java.util.Arrays; |
| | | |
| | | import static org.opends.messages.ExtensionMessages.*; |
| | | import static org.opends.server.extensions.ExtensionsConstants.*; |
| | | import static org.opends.server.util.StaticUtils.getExceptionMessage; |
| | | import static org.opends.server.util.StaticUtils.*; |
| | | |
| | | /** |
| | | * This class defines a Directory Server password storage scheme based on the |
| | |
| | | { |
| | | private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); |
| | | |
| | | /** |
| | | * The fully-qualified name of this class. |
| | | */ |
| | | /** The fully-qualified name of this class. */ |
| | | private static final String CLASS_NAME = |
| | | "org.opends.server.extensions.PKCS5S2PasswordStorageScheme"; |
| | | |
| | |
| | | */ |
| | | private static final int NUM_SALT_BYTES = 16; |
| | | |
| | | /** |
| | | * The number of bytes the SHA-1 algorithm produces. |
| | | */ |
| | | /** The number of bytes the SHA-1 algorithm produces. */ |
| | | private static final int SHA1_LENGTH = 32; |
| | | |
| | | /** |
| | | * Atlassian hardcoded the number of iterations to 10000. |
| | | */ |
| | | /** Atlassian hardcoded the number of iterations to 10000. */ |
| | | private static final int iterations = 10000; |
| | | |
| | | /** |
| | | * The factory used to generate the PKCS5S2 hashes. |
| | | */ |
| | | /** The factory used to generate the PKCS5S2 hashes. */ |
| | | private SecretKeyFactory factory; |
| | | |
| | | /** |
| | | * The lock used to provide thread-safe access to the message digest. |
| | | */ |
| | | /** The lock used to provide thread-safe access to the message digest. */ |
| | | private final Object factoryLock = new Object(); |
| | | |
| | | /** |
| | | * The secure random number generator to use to generate the salt values. |
| | | */ |
| | | /** The secure random number generator to use to generate the salt values. */ |
| | | private SecureRandom random; |
| | | |
| | | |
| | |
| | | super(); |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * {@inheritDoc} |
| | | */ |
| | | @Override() |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void initializePasswordStorageScheme( |
| | | PKCS5S2PasswordStorageSchemeCfg configuration) |
| | | throws InitializationException |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * {@inheritDoc} |
| | | */ |
| | | @Override() |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public String getStorageSchemeName() |
| | | { |
| | | return STORAGE_SCHEME_NAME_PKCS5S2; |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * {@inheritDoc} |
| | | */ |
| | | @Override() |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public ByteString encodePassword(ByteSequence plaintext) |
| | | throws DirectoryException |
| | | { |
| | |
| | | return ByteString.valueOf(Base64.encode(hashPlusSalt)); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * {@inheritDoc} |
| | | */ |
| | | @Override() |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public ByteString encodePasswordWithScheme(ByteSequence plaintext) |
| | | throws DirectoryException |
| | | { |
| | | return ByteString.valueOf("{" + STORAGE_SCHEME_NAME_PKCS5S2 + '}' |
| | | return ByteString.valueOf('{' + STORAGE_SCHEME_NAME_PKCS5S2 + '}' |
| | | + encodePassword(plaintext)); |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * {@inheritDoc} |
| | | */ |
| | | @Override() |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public boolean passwordMatches(ByteSequence plaintextPassword, |
| | | ByteSequence storedPassword) |
| | | { |
| | | |
| | | // Base64-decode the value and take the first 16 bytes as the salt. |
| | | byte[] saltBytes = new byte[NUM_SALT_BYTES]; |
| | | final int saltLength = NUM_SALT_BYTES; |
| | | byte[] digestBytes = new byte[SHA1_LENGTH]; |
| | | try |
| | | { |
| | | String stored = storedPassword.toString(); |
| | |
| | | storedPassword.toString())); |
| | | return false; |
| | | } |
| | | |
| | | final int saltLength = NUM_SALT_BYTES; |
| | | byte[] saltBytes = new byte[saltLength]; |
| | | byte[] digestBytes = new byte[SHA1_LENGTH]; |
| | | System.arraycopy(decodedBytes, 0, saltBytes, 0, saltLength); |
| | | System.arraycopy(decodedBytes, saltLength, digestBytes, 0, |
| | | SHA1_LENGTH); |
| | | System.arraycopy(decodedBytes, saltLength, digestBytes, 0, SHA1_LENGTH); |
| | | return encodeAndMatch(plaintextPassword, saltBytes, digestBytes, iterations); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | |
| | | storedPassword.toString(), String.valueOf(e))); |
| | | return false; |
| | | } |
| | | |
| | | return encodeAndMatch(plaintextPassword, saltBytes, digestBytes, iterations); |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * {@inheritDoc} |
| | | */ |
| | | @Override() |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public boolean supportsAuthPasswordSyntax() |
| | | { |
| | | return true; |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * {@inheritDoc} |
| | | */ |
| | | @Override() |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public String getAuthPasswordSchemeName() |
| | | { |
| | | return AUTH_PASSWORD_SCHEME_NAME_PKCS5S2; |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * {@inheritDoc} |
| | | */ |
| | | @Override() |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public ByteString encodeAuthPassword(ByteSequence plaintext) |
| | | throws DirectoryException |
| | | { |
| | | byte[] saltBytes = new byte[NUM_SALT_BYTES]; |
| | | byte[] digestBytes = createRandomSaltAndEncode(plaintext, saltBytes); |
| | | // Encode and return the value. |
| | | return ByteString.valueOf(AUTH_PASSWORD_SCHEME_NAME_PKCS5S2 + '$' + |
| | | iterations + ':' + Base64.encode(saltBytes) + |
| | | '$' + Base64.encode(digestBytes)); |
| | | return ByteString.valueOf(AUTH_PASSWORD_SCHEME_NAME_PKCS5S2 + '$' |
| | | + iterations + ':' + Base64.encode(saltBytes) + '$' |
| | | + Base64.encode(digestBytes)); |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * {@inheritDoc} |
| | | */ |
| | | @Override() |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public boolean authPasswordMatches(ByteSequence plaintextPassword, |
| | | String authInfo, String authValue) |
| | | { |
| | | byte[] saltBytes; |
| | | byte[] digestBytes; |
| | | int iterations; |
| | | |
| | | try |
| | | { |
| | | int pos = 0; |
| | | int length = authInfo.length(); |
| | | while (pos < length && authInfo.charAt(pos) != ':') |
| | | int pos = authInfo.indexOf(':'); |
| | | if (pos == -1) |
| | | { |
| | | pos++; |
| | | return false; |
| | | } |
| | | if (pos >= (length - 1) || pos == 0) |
| | | throw new Exception(); |
| | | iterations = Integer.parseInt(authInfo.substring(0, pos)); |
| | | saltBytes = Base64.decode(authInfo.substring(pos + 1)); |
| | | digestBytes = Base64.decode(authValue); |
| | | int iterations = Integer.parseInt(authInfo.substring(0, pos)); |
| | | byte[] saltBytes = Base64.decode(authInfo.substring(pos + 1)); |
| | | byte[] digestBytes = Base64.decode(authValue); |
| | | return encodeAndMatch(plaintextPassword, saltBytes, digestBytes, iterations); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | | return false; |
| | | logger.traceException(e); |
| | | return false; |
| | | } |
| | | |
| | | return encodeAndMatch(plaintextPassword, saltBytes, digestBytes, iterations); |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * {@inheritDoc} |
| | | */ |
| | | @Override() |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public boolean isReversible() |
| | | { |
| | | return false; |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * {@inheritDoc} |
| | | */ |
| | | @Override() |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public ByteString getPlaintextValue(ByteSequence storedPassword) |
| | | throws DirectoryException |
| | | { |
| | |
| | | throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * {@inheritDoc} |
| | | */ |
| | | @Override() |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public ByteString getAuthPasswordPlaintextValue(String authInfo, |
| | | String authValue) |
| | | throws DirectoryException |
| | |
| | | throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message); |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * {@inheritDoc} |
| | | */ |
| | | @Override() |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public boolean isStorageSchemeSecure() |
| | | { |
| | | return true; |
| | |
| | | * @return The encoded password string, including the scheme name in curly |
| | | * braces. |
| | | * |
| | | * @throws org.opends.server.types.DirectoryException If a problem occurs during processing. |
| | | * @throws DirectoryException If a problem occurs during processing. |
| | | */ |
| | | public static String encodeOffline(byte[] passwordBytes) |
| | | throws DirectoryException |
| | |
| | | // Append the hashed value to the salt and base64-the whole thing. |
| | | byte[] hashPlusSalt = concatenateSaltPlusHash(saltBytes, digestBytes); |
| | | |
| | | return "{" + STORAGE_SCHEME_NAME_PKCS5S2 + "}" + |
| | | return '{' + STORAGE_SCHEME_NAME_PKCS5S2 + '}' + |
| | | Base64.encode(hashPlusSalt); |
| | | } |
| | | |
| | |
| | | private boolean encodeAndMatch(ByteSequence plaintext, |
| | | byte[] saltBytes, byte[] digestBytes, int iterations) |
| | | { |
| | | byte[] userDigestBytes; |
| | | |
| | | try |
| | | { |
| | | userDigestBytes = encodeWithSalt(plaintext, saltBytes, iterations); |
| | | byte[] userDigestBytes = encodeWithSalt(plaintext, saltBytes, iterations); |
| | | return Arrays.equals(digestBytes, userDigestBytes); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | return false; |
| | | return false; |
| | | } |
| | | return Arrays.equals(digestBytes, userDigestBytes); |
| | | } |
| | | |
| | | |
| | | private byte[] createRandomSaltAndEncode(ByteSequence plaintext, byte[] saltBytes) throws DirectoryException { |
| | | byte[] digestBytes; |
| | | |
| | | synchronized(factoryLock) |
| | | { |
| | | random.nextBytes(saltBytes); |
| | | digestBytes = encodeWithSalt(plaintext, saltBytes, iterations); |
| | | return encodeWithSalt(plaintext, saltBytes, iterations); |
| | | } |
| | | return digestBytes; |
| | | } |
| | | |
| | | private byte[] encodeWithSalt(ByteSequence plaintext, byte[] saltBytes, int iterations) throws DirectoryException { |
| | | byte[] digestBytes; |
| | | char[] plaintextChars = null; |
| | | try |
| | | { |
| | |
| | | KeySpec spec = new PBEKeySpec( |
| | | plaintextChars, saltBytes, |
| | | iterations, SHA1_LENGTH * 8); |
| | | digestBytes = factory.generateSecret(spec).getEncoded(); |
| | | return factory.generateSecret(spec).getEncoded(); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | |
| | | Arrays.fill(plaintextChars, '0'); |
| | | } |
| | | } |
| | | return digestBytes; |
| | | } |
| | | |
| | | private static byte[] concatenateSaltPlusHash(byte[] saltBytes, byte[] digestBytes) { |