PBKDF2PasswordStorageScheme.java revision 6045
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 *
6045N/A * You can obtain a copy of the license at
6045N/A * trunk/opends/resource/legal-notices/OpenDS.LICENSE
6045N/A * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
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
6045N/A * file and include the License file at
6045N/A * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable,
6045N/A * add the following below this CDDL HEADER, with the fields enclosed
6045N/A * by brackets "[]" replaced with your own identifying information:
6045N/A * Portions Copyright [yyyy] [name of copyright owner]
6045N/A *
6045N/A * CDDL HEADER END
6045N/A *
6045N/A *
6045N/A * Copyright 2013 ForgeRock AS.
6045N/A */
6045N/Apackage org.opends.server.extensions;
6045N/A
6045N/Aimport java.security.NoSuchAlgorithmException;
6045N/Aimport java.security.SecureRandom;
6045N/Aimport java.util.Arrays;
6045N/Aimport java.util.List;
6045N/Aimport java.security.spec.KeySpec;
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.*;
6045N/Aimport static org.opends.server.util.StaticUtils.getExceptionMessage;
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
6045N/A extends PasswordStorageScheme<PBKDF2PasswordStorageSchemeCfg>
6045N/A implements ConfigurationChangeListener<PBKDF2PasswordStorageSchemeCfg>
6045N/A{
6045N/A /**
6045N/A * The tracer object for the debug logger.
6045N/A */
6045N/A private static final DebugTracer TRACER = getTracer();
6045N/A
6045N/A /**
6045N/A * The fully-qualified name of this class.
6045N/A */
6045N/A private static final String CLASS_NAME =
6045N/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
6045N/A // The number of bytes the SHA-1 algorithm produces
6045N/A private static final int SHA1_LENGTH = 20;
6045N/A
6045N/A // The factory used to generate the PBKDF2 hashes.
6045N/A private SecretKeyFactory factory;
6045N/A
6045N/A // The lock used to provide threadsafe access to the message digest.
6045N/A private Object factoryLock;
6045N/A
6045N/A // The secure random number generator to use to generate the salt values.
6045N/A private SecureRandom random;
6045N/A
6045N/A // The current configuration for this storage scheme.
6045N/A private volatile PBKDF2PasswordStorageSchemeCfg config;
6045N/A
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
6045N/A
6045N/A
6045N/A /**
6045N/A * {@inheritDoc}
6045N/A */
6045N/A @Override()
6045N/A public void initializePasswordStorageScheme(
6045N/A PBKDF2PasswordStorageSchemeCfg configuration)
6045N/A throws ConfigException, InitializationException
6045N/A {
6045N/A factoryLock = new Object();
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
6045N/A
6045N/A
6045N/A /**
6045N/A * {@inheritDoc}
6045N/A */
6045N/A public boolean isConfigurationChangeAcceptable(
6045N/A PBKDF2PasswordStorageSchemeCfg configuration,
6045N/A List<Message> unacceptableReasons)
6045N/A {
6045N/A // The configuration will always be acceptable.
6045N/A return true;
6045N/A }
6045N/A
6045N/A
6045N/A
6045N/A /**
6045N/A * {@inheritDoc}
6045N/A */
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
6045N/A
6045N/A
6045N/A /**
6045N/A * {@inheritDoc}
6045N/A */
6045N/A @Override()
6045N/A public String getStorageSchemeName()
6045N/A {
6045N/A return STORAGE_SCHEME_NAME_PBKDF2;
6045N/A }
6045N/A
6045N/A
6045N/A
6045N/A /**
6045N/A * {@inheritDoc}
6045N/A */
6045N/A @Override()
6045N/A public ByteString encodePassword(ByteSequence plaintext)
6045N/A throws DirectoryException
6045N/A {
6045N/A byte[] saltBytes = new byte[NUM_SALT_BYTES];
6045N/A byte[] digestBytes;
6045N/A int iterations = config.getPBKDF2Iterations();
6045N/A
6045N/A synchronized(factoryLock)
6045N/A {
6045N/A try
6045N/A {
6045N/A random.nextBytes(saltBytes);
6045N/A
6045N/A KeySpec spec = new PBEKeySpec(plaintext.toString().toCharArray(),
6045N/A saltBytes, iterations, SHA1_LENGTH * 8);
6045N/A digestBytes = factory.generateSecret(spec).getEncoded();
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
6045N/A Message message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
6045N/A CLASS_NAME, getExceptionMessage(e));
6045N/A throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
6045N/A message, e);
6045N/A }
6045N/A }
6045N/A // Append the salt to the hashed value and base64-the whole thing.
6045N/A byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES];
6045N/A
6045N/A System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length);
6045N/A System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length,
6045N/A NUM_SALT_BYTES);
6045N/A
6045N/A StringBuilder sb = new StringBuilder();
6045N/A sb.append(Integer.toString(iterations));
6045N/A sb.append(':');
6045N/A sb.append(Base64.encode(hashPlusSalt));
6045N/A return ByteString.valueOf(sb.toString());
6045N/A }
6045N/A
6045N/A
6045N/A
6045N/A /**
6045N/A * {@inheritDoc}
6045N/A */
6045N/A @Override()
6045N/A public ByteString encodePasswordWithScheme(ByteSequence plaintext)
6045N/A throws DirectoryException
6045N/A {
6045N/A StringBuilder buffer = new StringBuilder();
6045N/A buffer.append('{');
6045N/A buffer.append(STORAGE_SCHEME_NAME_PBKDF2);
6045N/A buffer.append('}');
6045N/A
6045N/A buffer.append(encodePassword(plaintext));
6045N/A
6045N/A return ByteString.valueOf(buffer.toString());
6045N/A }
6045N/A
6045N/A
6045N/A
6045N/A /**
6045N/A * {@inheritDoc}
6045N/A */
6045N/A @Override()
6045N/A public boolean passwordMatches(ByteSequence plaintextPassword,
6045N/A ByteSequence storedPassword)
6045N/A {
6045N/A
6045N/A // Split the iterations from the stored value (separated by a ":")
6045N/A // Base64-decode the remaining value and take the last 8 bytes as the salt.
6045N/A int iterations;
6045N/A byte[] saltBytes;
6045N/A byte[] digestBytes = new byte[SHA1_LENGTH];
6045N/A int saltLength = 0;
6045N/A try
6045N/A {
6045N/A String stored = storedPassword.toString();
6045N/A int stored_length = stored.length();
6045N/A int pos = 0;
6045N/A while (pos < stored_length && stored.charAt(pos) != ':')
6045N/A {
6045N/A pos++;
6045N/A }
6045N/A if (pos >= (stored_length - 1) || pos == 0)
6045N/A throw new Exception();
6045N/A
6045N/A iterations = Integer.parseInt(stored.substring(0, pos));
6045N/A byte[] decodedBytes = Base64.decode(stored.substring(pos + 1));
6045N/A
6045N/A saltLength = decodedBytes.length - SHA1_LENGTH;
6045N/A if (saltLength <= 0)
6045N/A {
6045N/A Message message =
6045N/A ERR_PWSCHEME_INVALID_BASE64_DECODED_STORED_PASSWORD.get(
6045N/A storedPassword.toString());
6045N/A ErrorLogger.logError(message);
6045N/A return false;
6045N/A }
6045N/A saltBytes = new byte[saltLength];
6045N/A System.arraycopy(decodedBytes, 0, digestBytes, 0, SHA1_LENGTH);
6045N/A System.arraycopy(decodedBytes, SHA1_LENGTH, saltBytes, 0,
6045N/A saltLength);
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
6045N/A Message message = ERR_PWSCHEME_CANNOT_BASE64_DECODE_STORED_PASSWORD.get(
6045N/A storedPassword.toString(), String.valueOf(e));
6045N/A ErrorLogger.logError(message);
6045N/A return false;
6045N/A }
6045N/A
6045N/A
6045N/A // Use the salt to generate a digest based on the provided plain-text value.
6045N/A int plainBytesLength = plaintextPassword.length();
6045N/A byte[] plainPlusSalt = new byte[plainBytesLength + saltLength];
6045N/A plaintextPassword.copyTo(plainPlusSalt);
6045N/A System.arraycopy(saltBytes, 0, plainPlusSalt, plainBytesLength,
6045N/A saltLength);
6045N/A
6045N/A byte[] userDigestBytes;
6045N/A
6045N/A synchronized (factoryLock)
6045N/A {
6045N/A try
6045N/A {
6045N/A KeySpec spec = new PBEKeySpec(
6045N/A plaintextPassword.toString().toCharArray(), saltBytes,
6045N/A iterations, SHA1_LENGTH * 8);
6045N/A userDigestBytes = factory.generateSecret(spec).getEncoded();
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
6045N/A return false;
6045N/A }
6045N/A }
6045N/A
6045N/A return Arrays.equals(digestBytes, userDigestBytes);
6045N/A }
6045N/A
6045N/A
6045N/A
6045N/A /**
6045N/A * {@inheritDoc}
6045N/A */
6045N/A @Override()
6045N/A public boolean supportsAuthPasswordSyntax()
6045N/A {
6045N/A // This storage scheme does support the authentication password syntax.
6045N/A return true;
6045N/A }
6045N/A
6045N/A
6045N/A
6045N/A /**
6045N/A * {@inheritDoc}
6045N/A */
6045N/A @Override()
6045N/A public String getAuthPasswordSchemeName()
6045N/A {
6045N/A return AUTH_PASSWORD_SCHEME_NAME_PBKDF2;
6045N/A }
6045N/A
6045N/A
6045N/A
6045N/A /**
6045N/A * {@inheritDoc}
6045N/A */
6045N/A @Override()
6045N/A public ByteString encodeAuthPassword(ByteSequence plaintext)
6045N/A throws DirectoryException
6045N/A {
6045N/A byte[] saltBytes = new byte[NUM_SALT_BYTES];
6045N/A byte[] digestBytes;
6045N/A int iterations = config.getPBKDF2Iterations();
6045N/A
6045N/A synchronized(factoryLock)
6045N/A {
6045N/A try
6045N/A {
6045N/A random.nextBytes(saltBytes);
6045N/A
6045N/A KeySpec spec = new PBEKeySpec(
6045N/A plaintext.toString().toCharArray(), saltBytes,
6045N/A iterations, SHA1_LENGTH * 8);
6045N/A digestBytes = factory.generateSecret(spec).getEncoded();
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
6045N/A Message message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
6045N/A CLASS_NAME, getExceptionMessage(e));
6045N/A throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
6045N/A message, e);
6045N/A }
6045N/A }
6045N/A // Encode and return the value.
6045N/A StringBuilder authPWValue = new StringBuilder();
6045N/A authPWValue.append(AUTH_PASSWORD_SCHEME_NAME_PBKDF2);
6045N/A authPWValue.append('$');
6045N/A authPWValue.append(Integer.toString(iterations));
6045N/A authPWValue.append(':');
6045N/A authPWValue.append(Base64.encode(saltBytes));
6045N/A authPWValue.append('$');
6045N/A authPWValue.append(Base64.encode(digestBytes));
6045N/A
6045N/A return ByteString.valueOf(authPWValue.toString());
6045N/A }
6045N/A
6045N/A
6045N/A
6045N/A /**
6045N/A * {@inheritDoc}
6045N/A */
6045N/A @Override()
6045N/A public boolean authPasswordMatches(ByteSequence plaintextPassword,
6045N/A String authInfo, String authValue)
6045N/A {
6045N/A byte[] saltBytes;
6045N/A byte[] digestBytes;
6045N/A int iterations;
6045N/A
6045N/A try
6045N/A {
6045N/A int pos = 0;
6045N/A int length = authInfo.length();
6045N/A while (pos < length && authInfo.charAt(pos) != ':')
6045N/A {
6045N/A pos++;
6045N/A }
6045N/A if (pos >= (length - 1) || pos == 0)
6045N/A throw new Exception();
6045N/A iterations = Integer.parseInt(authInfo.substring(0, pos));
6045N/A saltBytes = Base64.decode(authInfo.substring(pos + 1));
6045N/A digestBytes = Base64.decode(authValue);
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
6045N/A return false;
6045N/A }
6045N/A
6045N/A
6045N/A int plainBytesLength = plaintextPassword.length();
6045N/A byte[] plainPlusSalt = new byte[plainBytesLength + saltBytes.length];
6045N/A plaintextPassword.copyTo(plainPlusSalt);
6045N/A System.arraycopy(saltBytes, 0, plainPlusSalt, plainBytesLength,
6045N/A saltBytes.length);
6045N/A
6045N/A byte[] userDigestBytes;
6045N/A
6045N/A synchronized (factoryLock)
6045N/A {
6045N/A try
6045N/A {
6045N/A KeySpec spec = new PBEKeySpec(
6045N/A plaintextPassword.toString().toCharArray(), saltBytes,
6045N/A iterations, SHA1_LENGTH * 8);
6045N/A userDigestBytes = factory.generateSecret(spec).getEncoded();
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
6045N/A return false;
6045N/A }
6045N/A }
6045N/A
6045N/A return Arrays.equals(digestBytes, userDigestBytes);
6045N/A }
6045N/A
6045N/A
6045N/A
6045N/A /**
6045N/A * {@inheritDoc}
6045N/A */
6045N/A @Override()
6045N/A public boolean isReversible()
6045N/A {
6045N/A return false;
6045N/A }
6045N/A
6045N/A
6045N/A
6045N/A /**
6045N/A * {@inheritDoc}
6045N/A */
6045N/A @Override()
6045N/A public ByteString getPlaintextValue(ByteSequence storedPassword)
6045N/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
6045N/A
6045N/A
6045N/A /**
6045N/A * {@inheritDoc}
6045N/A */
6045N/A @Override()
6045N/A public ByteString getAuthPasswordPlaintextValue(String authInfo,
6045N/A String authValue)
6045N/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
6045N/A
6045N/A
6045N/A /**
6045N/A * {@inheritDoc}
6045N/A */
6045N/A @Override()
6045N/A public boolean isStorageSchemeSecure()
6045N/A {
6045N/A // PBKDF2 should be considered secure.
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 *
6045N/A * @return The encoded password string, including the scheme name in curly
6045N/A * braces.
6045N/A *
6045N/A * @throws DirectoryException If a problem occurs during processing.
6045N/A */
6045N/A public static String encodeOffline(byte[] passwordBytes)
6045N/A throws DirectoryException
6045N/A {
6045N/A byte[] saltBytes = new byte[NUM_SALT_BYTES];
6045N/A byte[] digestBytes;
6045N/A int iterations = 10000;
6045N/A
6045N/A try
6045N/A {
6045N/A SecureRandom.getInstance(SECURE_PRNG_SHA1).nextBytes(saltBytes);
6045N/A
6045N/A KeySpec spec = new PBEKeySpec(
6045N/A passwordBytes.toString().toCharArray(), saltBytes,
6045N/A iterations, SHA1_LENGTH * 8);
6045N/A digestBytes = SecretKeyFactory
6045N/A .getInstance(MESSAGE_DIGEST_ALGORITHM_PBKDF2)
6045N/A .generateSecret(spec).getEncoded();
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
6045N/A Message message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
6045N/A CLASS_NAME, getExceptionMessage(e));
6045N/A throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
6045N/A message, e);
6045N/A }
6045N/A
6045N/A // Append the salt to the hashed value and base64-the whole thing.
6045N/A byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES];
6045N/A
6045N/A System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length);
6045N/A System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length,
6045N/A NUM_SALT_BYTES);
6045N/A
6045N/A return "{" + STORAGE_SCHEME_NAME_PBKDF2 + "}" + iterations + ":" +
6045N/A Base64.encode(hashPlusSalt);
6045N/A }
6045N/A
6045N/A}