6045N/A/*
6045N/A * CDDL HEADER START
6045N/A *
6045N/A * The contents of this file are subject to the terms of the
6045N/A * Common Development and Distribution License, Version 1.0 only
6045N/A * (the "License"). You may not use this file except in compliance
6045N/A * with the License.
6045N/A *
6983N/A * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
6983N/A * or http://forgerock.org/license/CDDLv1.0.html.
6045N/A * See the License for the specific language governing permissions
6045N/A * and limitations under the License.
6045N/A *
6045N/A * When distributing Covered Code, include this CDDL HEADER in each
6983N/A * file and include the License file at legal-notices/CDDLv1_0.txt.
6983N/A * If applicable, add the following below this CDDL HEADER, with the
6983N/A * fields enclosed by brackets "[]" replaced with your own identifying
6983N/A * information:
6045N/A * Portions Copyright [yyyy] [name of copyright owner]
6045N/A *
6045N/A * CDDL HEADER END
6045N/A *
6045N/A *
7222N/A * Copyright 2013-2014 ForgeRock AS.
6045N/A */
6045N/Apackage org.opends.server.extensions;
6045N/A
6045N/Aimport java.security.NoSuchAlgorithmException;
6045N/Aimport java.security.SecureRandom;
7222N/Aimport java.security.spec.KeySpec;
6045N/Aimport java.util.Arrays;
6045N/Aimport java.util.List;
6045N/A
6045N/Aimport javax.crypto.SecretKeyFactory;
6045N/Aimport javax.crypto.spec.PBEKeySpec;
6045N/A
6045N/Aimport org.opends.messages.Message;
6045N/Aimport org.opends.server.admin.server.ConfigurationChangeListener;
6045N/Aimport org.opends.server.admin.std.server.PBKDF2PasswordStorageSchemeCfg;
6045N/Aimport org.opends.server.api.PasswordStorageScheme;
6045N/Aimport org.opends.server.config.ConfigException;
6045N/Aimport org.opends.server.core.DirectoryServer;
6045N/Aimport org.opends.server.loggers.ErrorLogger;
6045N/Aimport org.opends.server.loggers.debug.DebugTracer;
6045N/Aimport org.opends.server.types.*;
6045N/Aimport org.opends.server.util.Base64;
6045N/A
6045N/Aimport static org.opends.messages.ExtensionMessages.*;
6045N/Aimport static org.opends.server.extensions.ExtensionsConstants.*;
6045N/Aimport static org.opends.server.loggers.debug.DebugLogger.*;
7222N/Aimport static org.opends.server.util.StaticUtils.*;
6045N/A
6045N/A/**
6045N/A * This class defines a Directory Server password storage scheme based on the
6045N/A * PBKDF2 algorithm defined in RFC 2898. This is a one-way digest algorithm
6045N/A * so there is no way to retrieve the original clear-text version of the
6045N/A * password from the hashed value (although this means that it is not suitable
6045N/A * for things that need the clear-text password like DIGEST-MD5). This
6045N/A * implementation uses a configurable number of iterations.
6045N/A */
6045N/Apublic class PBKDF2PasswordStorageScheme
7227N/A extends PasswordStorageScheme<PBKDF2PasswordStorageSchemeCfg>
7227N/A implements ConfigurationChangeListener<PBKDF2PasswordStorageSchemeCfg>
6045N/A{
7222N/A /** The tracer object for the debug logger. */
6045N/A private static final DebugTracer TRACER = getTracer();
6045N/A
7222N/A /** The fully-qualified name of this class. */
6045N/A private static final String CLASS_NAME =
7227N/A "org.opends.server.extensions.PBKDF2PasswordStorageScheme";
6045N/A
6045N/A
6045N/A /**
6045N/A * The number of bytes of random data to use as the salt when generating the
6045N/A * hashes.
6045N/A */
6045N/A private static final int NUM_SALT_BYTES = 8;
6045N/A
7222N/A /** The number of bytes the SHA-1 algorithm produces. */
6045N/A private static final int SHA1_LENGTH = 20;
6045N/A
7222N/A /** The factory used to generate the PBKDF2 hashes. */
6045N/A private SecretKeyFactory factory;
6045N/A
7222N/A /** The lock used to provide thread-safe access to the message digest. */
7222N/A private final Object factoryLock = new Object();
6045N/A
7222N/A /** The secure random number generator to use to generate the salt values. */
6045N/A private SecureRandom random;
6045N/A
7222N/A /** The current configuration for this storage scheme. */
6045N/A private volatile PBKDF2PasswordStorageSchemeCfg config;
6045N/A
6045N/A /**
6045N/A * Creates a new instance of this password storage scheme. Note that no
6045N/A * initialization should be performed here, as all initialization should be
6045N/A * done in the <CODE>initializePasswordStorageScheme</CODE> method.
6045N/A */
6045N/A public PBKDF2PasswordStorageScheme()
6045N/A {
6045N/A super();
6045N/A }
6045N/A
7222N/A /** {@inheritDoc} */
7222N/A @Override
6045N/A public void initializePasswordStorageScheme(
7227N/A PBKDF2PasswordStorageSchemeCfg configuration)
7227N/A throws ConfigException, InitializationException
6045N/A {
6045N/A try
6045N/A {
6045N/A random = SecureRandom.getInstance(SECURE_PRNG_SHA1);
6045N/A factory = SecretKeyFactory.getInstance(MESSAGE_DIGEST_ALGORITHM_PBKDF2);
6045N/A }
6045N/A catch (NoSuchAlgorithmException e)
6045N/A {
6045N/A throw new InitializationException(null);
6045N/A }
6045N/A
6045N/A this.config = configuration;
6045N/A config.addPBKDF2ChangeListener(this);
6045N/A }
6045N/A
7222N/A /** {@inheritDoc} */
7222N/A @Override
6045N/A public boolean isConfigurationChangeAcceptable(
6045N/A PBKDF2PasswordStorageSchemeCfg configuration,
6045N/A List<Message> unacceptableReasons)
6045N/A {
6045N/A return true;
6045N/A }
6045N/A
7222N/A /** {@inheritDoc} */
7222N/A @Override
6045N/A public ConfigChangeResult applyConfigurationChange(
6045N/A PBKDF2PasswordStorageSchemeCfg configuration)
6045N/A {
6045N/A this.config = configuration;
6045N/A return new ConfigChangeResult(ResultCode.SUCCESS, false);
6045N/A }
6045N/A
7222N/A /** {@inheritDoc} */
7222N/A @Override
6045N/A public String getStorageSchemeName()
6045N/A {
6045N/A return STORAGE_SCHEME_NAME_PBKDF2;
6045N/A }
6045N/A
7222N/A /** {@inheritDoc} */
7222N/A @Override
6045N/A public ByteString encodePassword(ByteSequence plaintext)
7227N/A throws DirectoryException
6045N/A {
6469N/A byte[] saltBytes = new byte[NUM_SALT_BYTES];
6469N/A int iterations = config.getPBKDF2Iterations();
6045N/A
7227N/A byte[] digestBytes = encodeWithRandomSalt(plaintext, saltBytes, iterations);
7227N/A byte[] hashPlusSalt = concatenateHashPlusSalt(saltBytes, digestBytes);
7222N/A
7224N/A return ByteString.valueOf(iterations + ":" + Base64.encode(hashPlusSalt));
7222N/A }
7222N/A
7222N/A /** {@inheritDoc} */
7222N/A @Override
7222N/A public ByteString encodePasswordWithScheme(ByteSequence plaintext)
7227N/A throws DirectoryException
7222N/A {
7222N/A return ByteString.valueOf('{' + STORAGE_SCHEME_NAME_PBKDF2 + '}'
7222N/A + encodePassword(plaintext));
7222N/A }
7222N/A
7222N/A /** {@inheritDoc} */
7222N/A @Override
7222N/A public boolean passwordMatches(ByteSequence plaintextPassword,
7222N/A ByteSequence storedPassword)
7222N/A {
7222N/A // Split the iterations from the stored value (separated by a ':')
7222N/A // Base64-decode the remaining value and take the last 8 bytes as the salt.
7222N/A try
7222N/A {
7227N/A final String stored = storedPassword.toString();
7227N/A final int pos = stored.indexOf(':');
7224N/A if (pos == -1)
7224N/A {
7224N/A throw new Exception();
7224N/A }
7222N/A
7222N/A final int iterations = Integer.parseInt(stored.substring(0, pos));
7222N/A byte[] decodedBytes = Base64.decode(stored.substring(pos + 1));
7222N/A
7222N/A final int saltLength = decodedBytes.length - SHA1_LENGTH;
7222N/A if (saltLength <= 0)
7222N/A {
7222N/A Message message =
7227N/A ERR_PWSCHEME_INVALID_BASE64_DECODED_STORED_PASSWORD.get(
7227N/A storedPassword.toString());
7222N/A ErrorLogger.logError(message);
7222N/A return false;
7222N/A }
7222N/A
7222N/A final byte[] digestBytes = new byte[SHA1_LENGTH];
7222N/A final byte[] saltBytes = new byte[saltLength];
7222N/A System.arraycopy(decodedBytes, 0, digestBytes, 0, SHA1_LENGTH);
7222N/A System.arraycopy(decodedBytes, SHA1_LENGTH, saltBytes, 0, saltLength);
7222N/A return encodeAndMatch(plaintextPassword, saltBytes, digestBytes, iterations);
7222N/A }
7222N/A catch (Exception e)
7222N/A {
7222N/A if (debugEnabled())
7222N/A {
7222N/A TRACER.debugCaught(DebugLogLevel.ERROR, e);
7222N/A }
7222N/A
7222N/A Message message = ERR_PWSCHEME_CANNOT_BASE64_DECODE_STORED_PASSWORD.get(
7222N/A storedPassword.toString(), String.valueOf(e));
7222N/A ErrorLogger.logError(message);
7222N/A return false;
7222N/A }
7222N/A }
7222N/A
7222N/A /** {@inheritDoc} */
7222N/A @Override
7222N/A public boolean supportsAuthPasswordSyntax()
7222N/A {
7222N/A return true;
7222N/A }
7222N/A
7222N/A /** {@inheritDoc} */
7222N/A @Override
7222N/A public String getAuthPasswordSchemeName()
7222N/A {
7222N/A return AUTH_PASSWORD_SCHEME_NAME_PBKDF2;
7222N/A }
7222N/A
7222N/A /** {@inheritDoc} */
7222N/A @Override
7222N/A public ByteString encodeAuthPassword(ByteSequence plaintext)
7227N/A throws DirectoryException
7222N/A {
7222N/A byte[] saltBytes = new byte[NUM_SALT_BYTES];
7222N/A int iterations = config.getPBKDF2Iterations();
7227N/A byte[] digestBytes = encodeWithRandomSalt(plaintext, saltBytes, iterations);
7222N/A
7222N/A // Encode and return the value.
7222N/A return ByteString.valueOf(AUTH_PASSWORD_SCHEME_NAME_PBKDF2 + '$'
7222N/A + iterations + ':' + Base64.encode(saltBytes) + '$'
7222N/A + Base64.encode(digestBytes));
7222N/A }
7222N/A
7222N/A /** {@inheritDoc} */
7222N/A @Override
7222N/A public boolean authPasswordMatches(ByteSequence plaintextPassword,
7222N/A String authInfo, String authValue)
6045N/A {
6045N/A try
6045N/A {
7222N/A int pos = authInfo.indexOf(':');
7224N/A if (pos == -1)
7224N/A {
7224N/A throw new Exception();
7224N/A }
7222N/A int iterations = Integer.parseInt(authInfo.substring(0, pos));
7222N/A byte[] saltBytes = Base64.decode(authInfo.substring(pos + 1));
7222N/A byte[] digestBytes = Base64.decode(authValue);
7222N/A return encodeAndMatch(plaintextPassword, saltBytes, digestBytes, iterations);
6045N/A }
6045N/A catch (Exception e)
6045N/A {
6045N/A if (debugEnabled())
6045N/A {
6045N/A TRACER.debugCaught(DebugLogLevel.ERROR, e);
6045N/A }
6045N/A return false;
6045N/A }
6045N/A }
6045N/A
7222N/A /** {@inheritDoc} */
7222N/A @Override
6045N/A public boolean isReversible()
6045N/A {
6045N/A return false;
6045N/A }
6045N/A
7222N/A /** {@inheritDoc} */
7222N/A @Override
6045N/A public ByteString getPlaintextValue(ByteSequence storedPassword)
7227N/A throws DirectoryException
6045N/A {
6045N/A Message message =
6045N/A ERR_PWSCHEME_NOT_REVERSIBLE.get(STORAGE_SCHEME_NAME_PBKDF2);
6045N/A throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
6045N/A }
6045N/A
7222N/A /** {@inheritDoc} */
7222N/A @Override
6045N/A public ByteString getAuthPasswordPlaintextValue(String authInfo,
6045N/A String authValue)
7227N/A throws DirectoryException
6045N/A {
6045N/A Message message =
6045N/A ERR_PWSCHEME_NOT_REVERSIBLE.get(AUTH_PASSWORD_SCHEME_NAME_PBKDF2);
6045N/A throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
6045N/A }
6045N/A
7222N/A /** {@inheritDoc} */
7222N/A @Override
6045N/A public boolean isStorageSchemeSecure()
6045N/A {
6045N/A return true;
6045N/A }
6045N/A
6045N/A
6045N/A
6045N/A /**
6045N/A * Generates an encoded password string from the given clear-text password.
6045N/A * This method is primarily intended for use when it is necessary to generate
6045N/A * a password with the server offline (e.g., when setting the initial root
6045N/A * user password).
6045N/A *
6045N/A * @param passwordBytes The bytes that make up the clear-text password.
6045N/A * @return The encoded password string, including the scheme name in curly
6045N/A * braces.
6045N/A * @throws DirectoryException If a problem occurs during processing.
6045N/A */
6045N/A public static String encodeOffline(byte[] passwordBytes)
7227N/A throws DirectoryException
6045N/A {
6469N/A byte[] saltBytes = new byte[NUM_SALT_BYTES];
6469N/A int iterations = 10000;
6045N/A
7227N/A final ByteString password = ByteString.wrap(passwordBytes);
7227N/A byte[] digestBytes = encodeWithRandomSalt(password, saltBytes, iterations);
7227N/A byte[] hashPlusSalt = concatenateHashPlusSalt(saltBytes, digestBytes);
7222N/A
7227N/A return '{' + STORAGE_SCHEME_NAME_PBKDF2 + '}' + iterations + ':'
7227N/A + Base64.encode(hashPlusSalt);
7222N/A }
7222N/A
7227N/A private static byte[] encodeWithRandomSalt(ByteString plaintext, byte[] saltBytes,
7222N/A int iterations) throws DirectoryException
7222N/A {
6045N/A try
6045N/A {
7227N/A final SecureRandom random = SecureRandom.getInstance(SECURE_PRNG_SHA1);
7227N/A final SecretKeyFactory factory = SecretKeyFactory.getInstance(MESSAGE_DIGEST_ALGORITHM_PBKDF2);
7227N/A return encodeWithRandomSalt(plaintext, saltBytes, iterations, random, factory);
7227N/A }
7227N/A catch (DirectoryException e)
7227N/A {
7227N/A throw e;
6045N/A }
6045N/A catch (Exception e)
6045N/A {
7227N/A throw cannotEncodePassword(e);
7227N/A }
7227N/A }
6045N/A
7227N/A private static byte[] encodeWithSalt(ByteSequence plaintext, byte[] saltBytes,
7227N/A int iterations, final SecretKeyFactory factory) throws DirectoryException
7227N/A {
7227N/A final char[] plaintextChars = plaintext.toString().toCharArray();
7227N/A try
7227N/A {
7227N/A KeySpec spec =
7227N/A new PBEKeySpec(plaintextChars, saltBytes, iterations, SHA1_LENGTH * 8);
7227N/A return factory.generateSecret(spec).getEncoded();
7227N/A }
7227N/A catch (Exception e)
7227N/A {
7227N/A throw cannotEncodePassword(e);
6045N/A }
6469N/A finally
6469N/A {
6469N/A if (plaintextChars != null)
7222N/A {
6469N/A Arrays.fill(plaintextChars, '0');
7222N/A }
6469N/A }
6045N/A }
6045N/A
7227N/A private boolean encodeAndMatch(ByteSequence plaintext, byte[] saltBytes,
7227N/A byte[] digestBytes, int iterations)
7227N/A {
7227N/A synchronized (factoryLock)
7227N/A {
7227N/A try
7227N/A {
7227N/A final byte[] userDigestBytes =
7227N/A encodeWithSalt(plaintext, saltBytes, iterations, factory);
7227N/A return Arrays.equals(digestBytes, userDigestBytes);
7227N/A }
7227N/A catch (Exception e)
7227N/A {
7227N/A return false;
7227N/A }
7227N/A }
7227N/A }
7227N/A
7227N/A private byte[] encodeWithRandomSalt(ByteSequence plaintext, byte[] saltBytes,
7227N/A int iterations) throws DirectoryException
7227N/A {
7227N/A synchronized (factoryLock)
7227N/A {
7227N/A return encodeWithRandomSalt(plaintext, saltBytes, iterations, random, factory);
7227N/A }
7227N/A }
7227N/A
7227N/A private static byte[] encodeWithRandomSalt(ByteSequence plaintext, byte[] saltBytes,
7227N/A int iterations, SecureRandom random, final SecretKeyFactory factory) throws DirectoryException
7227N/A {
7227N/A random.nextBytes(saltBytes);
7227N/A return encodeWithSalt(plaintext, saltBytes, iterations, factory);
7227N/A }
7227N/A
7227N/A private static DirectoryException cannotEncodePassword(Exception e)
7227N/A {
7227N/A if (debugEnabled())
7227N/A {
7227N/A TRACER.debugCaught(DebugLogLevel.ERROR, e);
7227N/A }
7227N/A
7227N/A Message message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
7227N/A CLASS_NAME, getExceptionMessage(e));
7227N/A return new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e);
7227N/A }
7227N/A
7227N/A private static byte[] concatenateHashPlusSalt(byte[] saltBytes, byte[] digestBytes) {
7227N/A final byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES];
7227N/A System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length);
7227N/A System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length, NUM_SALT_BYTES);
7227N/A return hashPlusSalt;
7227N/A }
7227N/A
6045N/A}