001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2014-2016 ForgeRock AS.
015 * Portions Copyright 2014 Emidio Stani & Andrea Stani
016 */
017package org.opends.server.extensions;
018
019import java.security.NoSuchAlgorithmException;
020import java.security.SecureRandom;
021import java.security.spec.KeySpec;
022import java.util.Arrays;
023
024import javax.crypto.SecretKeyFactory;
025import javax.crypto.spec.PBEKeySpec;
026
027import org.forgerock.i18n.LocalizableMessage;
028import org.forgerock.i18n.slf4j.LocalizedLogger;
029import org.forgerock.opendj.ldap.ByteSequence;
030import org.forgerock.opendj.ldap.ByteString;
031import org.forgerock.opendj.ldap.ResultCode;
032import org.forgerock.opendj.server.config.server.PKCS5S2PasswordStorageSchemeCfg;
033import org.opends.server.api.PasswordStorageScheme;
034import org.opends.server.core.DirectoryServer;
035import org.opends.server.types.DirectoryException;
036import org.opends.server.types.InitializationException;
037import org.opends.server.util.Base64;
038
039import static org.opends.messages.ExtensionMessages.*;
040import static org.opends.server.extensions.ExtensionsConstants.*;
041import static org.opends.server.util.StaticUtils.*;
042
043/**
044 * This class defines a Directory Server password storage scheme based on the
045 * Atlassian PBKF2-base hash algorithm.  This is a one-way digest algorithm
046 * so there is no way to retrieve the original clear-text version of the
047 * password from the hashed value (although this means that it is not suitable
048 * for things that need the clear-text password like DIGEST-MD5).  Unlike
049 * the other PBKF2-base scheme, this implementation uses a fixed number of
050 * iterations.
051 */
052public class PKCS5S2PasswordStorageScheme
053    extends PasswordStorageScheme<PKCS5S2PasswordStorageSchemeCfg>
054{
055    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
056
057  /** The fully-qualified name of this class. */
058  private static final String CLASS_NAME = "org.opends.server.extensions.PKCS5S2PasswordStorageScheme";
059
060  /** The number of bytes of random data to use as the salt when generating the hashes. */
061  private static final int NUM_SALT_BYTES = 16;
062
063  /** The number of bytes the SHA-1 algorithm produces. */
064  private static final int SHA1_LENGTH = 32;
065
066  /** Atlassian hardcoded the number of iterations to 10000. */
067  private static final int iterations = 10000;
068
069  /** The secure random number generator to use to generate the salt values. */
070  private SecureRandom random;
071
072  /**
073   * Creates a new instance of this password storage scheme.  Note that no
074   * initialization should be performed here, as all initialization should be
075   * done in the <CODE>initializePasswordStorageScheme</CODE> method.
076   */
077  public PKCS5S2PasswordStorageScheme()
078  {
079    super();
080  }
081
082  @Override
083  public void initializePasswordStorageScheme(PKCS5S2PasswordStorageSchemeCfg configuration)
084      throws InitializationException
085  {
086    try
087    {
088      random = SecureRandom.getInstance(SECURE_PRNG_SHA1);
089      // Just try to verify if the algorithm is supported
090      SecretKeyFactory.getInstance(MESSAGE_DIGEST_ALGORITHM_PBKDF2);
091    }
092    catch (NoSuchAlgorithmException e)
093    {
094      throw new InitializationException(null);
095    }
096  }
097
098  @Override
099  public String getStorageSchemeName()
100  {
101    return STORAGE_SCHEME_NAME_PKCS5S2;
102  }
103
104  @Override
105  public ByteString encodePassword(ByteSequence plaintext)
106      throws DirectoryException
107  {
108    byte[] saltBytes      = new byte[NUM_SALT_BYTES];
109    byte[] digestBytes = encodeWithRandomSalt(plaintext, saltBytes,random);
110    byte[] hashPlusSalt = concatenateSaltPlusHash(saltBytes, digestBytes);
111
112    return ByteString.valueOfUtf8(Base64.encode(hashPlusSalt));
113  }
114
115  @Override
116  public ByteString encodePasswordWithScheme(ByteSequence plaintext)
117      throws DirectoryException
118  {
119    return ByteString.valueOfUtf8('{' + STORAGE_SCHEME_NAME_PKCS5S2 + '}' + encodePassword(plaintext));
120  }
121
122  @Override
123  public boolean passwordMatches(ByteSequence plaintextPassword, ByteSequence storedPassword)
124  {
125    // Base64-decode the value and take the first 16 bytes as the salt.
126    try
127    {
128      String stored = storedPassword.toString();
129      byte[] decodedBytes = Base64.decode(stored);
130
131      if (decodedBytes.length != NUM_SALT_BYTES + SHA1_LENGTH)
132      {
133        logger.error(ERR_PWSCHEME_INVALID_BASE64_DECODED_STORED_PASSWORD.get(storedPassword.toString()));
134        return false;
135      }
136
137      final int saltLength = NUM_SALT_BYTES;
138      final byte[] digestBytes = new byte[SHA1_LENGTH];
139      final byte[] saltBytes = new byte[saltLength];
140      System.arraycopy(decodedBytes, 0, saltBytes, 0, saltLength);
141      System.arraycopy(decodedBytes, saltLength, digestBytes, 0, SHA1_LENGTH);
142      return encodeAndMatch(plaintextPassword, saltBytes, digestBytes, iterations);
143    }
144    catch (Exception e)
145    {
146      logger.traceException(e);
147      logger.error(ERR_PWSCHEME_CANNOT_BASE64_DECODE_STORED_PASSWORD.get(storedPassword.toString(), String.valueOf(e)));
148      return false;
149    }
150  }
151
152  @Override
153  public boolean supportsAuthPasswordSyntax()
154  {
155    return true;
156  }
157
158  @Override
159  public String getAuthPasswordSchemeName()
160  {
161    return AUTH_PASSWORD_SCHEME_NAME_PKCS5S2;
162  }
163
164  @Override
165  public ByteString encodeAuthPassword(ByteSequence plaintext)
166      throws DirectoryException
167  {
168    byte[] saltBytes      = new byte[NUM_SALT_BYTES];
169    byte[] digestBytes = encodeWithRandomSalt(plaintext, saltBytes,random);
170    // Encode and return the value.
171    return ByteString.valueOfUtf8(AUTH_PASSWORD_SCHEME_NAME_PKCS5S2 + '$' + iterations
172        + ':' + Base64.encode(saltBytes) + '$' + Base64.encode(digestBytes));
173  }
174
175  @Override
176  public boolean authPasswordMatches(ByteSequence plaintextPassword, String authInfo, String authValue)
177  {
178    try
179    {
180      int pos = authInfo.indexOf(':');
181      if (pos == -1)
182      {
183        throw new Exception();
184      }
185      int iterations = Integer.parseInt(authInfo.substring(0, pos));
186      byte[] saltBytes   = Base64.decode(authInfo.substring(pos + 1));
187      byte[] digestBytes = Base64.decode(authValue);
188      return encodeAndMatch(plaintextPassword, saltBytes, digestBytes, iterations);
189    }
190    catch (Exception e)
191    {
192      logger.traceException(e);
193      return false;
194    }
195  }
196
197  @Override
198  public boolean isReversible()
199  {
200    return false;
201  }
202
203  @Override
204  public ByteString getPlaintextValue(ByteSequence storedPassword)
205      throws DirectoryException
206  {
207    LocalizableMessage message = ERR_PWSCHEME_NOT_REVERSIBLE.get(STORAGE_SCHEME_NAME_PKCS5S2);
208    throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
209  }
210
211  @Override
212  public ByteString getAuthPasswordPlaintextValue(String authInfo, String authValue)
213      throws DirectoryException
214  {
215    LocalizableMessage message = ERR_PWSCHEME_NOT_REVERSIBLE.get(AUTH_PASSWORD_SCHEME_NAME_PKCS5S2);
216    throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
217  }
218
219  @Override
220  public boolean isStorageSchemeSecure()
221  {
222    return true;
223  }
224
225  /**
226   * Generates an encoded password string from the given clear-text password.
227   * This method is primarily intended for use when it is necessary to generate a password with the server
228   * offline (e.g., when setting the initial root user password).
229   *
230   * @param  passwordBytes  The bytes that make up the clear-text password.
231   * @return  The encoded password string, including the scheme name in curly braces.
232   * @throws  DirectoryException  If a problem occurs during processing.
233   */
234  public static String encodeOffline(byte[] passwordBytes)
235      throws DirectoryException
236  {
237    byte[] saltBytes = new byte[NUM_SALT_BYTES];
238    byte[] digestBytes = encodeWithRandomSalt(ByteString.wrap(passwordBytes), saltBytes);
239    byte[] hashPlusSalt = concatenateSaltPlusHash(saltBytes, digestBytes);
240
241    return '{' + STORAGE_SCHEME_NAME_PKCS5S2 + '}' + Base64.encode(hashPlusSalt);
242  }
243
244  private static byte[] encodeWithRandomSalt(ByteString plaintext, byte[] saltBytes)
245      throws DirectoryException
246  {
247    try
248    {
249      final SecureRandom random = SecureRandom.getInstance(SECURE_PRNG_SHA1);
250      return encodeWithRandomSalt(plaintext, saltBytes, random);
251    }
252    catch (DirectoryException e)
253    {
254      throw e;
255    }
256    catch (Exception e)
257    {
258      throw cannotEncodePassword(e);
259    }
260  }
261
262  private static byte[] encodeWithSalt(ByteSequence plaintext, byte[] saltBytes, int iterations)
263      throws DirectoryException
264  {
265    final char[] plaintextChars = plaintext.toString().toCharArray();
266    try
267    {
268      final SecretKeyFactory factory = SecretKeyFactory.getInstance(MESSAGE_DIGEST_ALGORITHM_PBKDF2);
269      KeySpec spec = new PBEKeySpec(plaintextChars, saltBytes, iterations, SHA1_LENGTH * 8);
270      return factory.generateSecret(spec).getEncoded();
271    }
272    catch (Exception e)
273    {
274      throw cannotEncodePassword(e);
275    }
276    finally
277    {
278      Arrays.fill(plaintextChars, '0');
279    }
280  }
281
282  private boolean encodeAndMatch(ByteSequence plaintext, byte[] saltBytes, byte[] digestBytes, int iterations)
283  {
284     try
285     {
286       final byte[] userDigestBytes = encodeWithSalt(plaintext, saltBytes, iterations);
287       return Arrays.equals(digestBytes, userDigestBytes);
288     }
289     catch (Exception e)
290     {
291       return false;
292     }
293  }
294
295  private static byte[] encodeWithRandomSalt(ByteSequence plaintext, byte[] saltBytes, SecureRandom random)
296      throws DirectoryException
297  {
298    random.nextBytes(saltBytes);
299    return encodeWithSalt(plaintext, saltBytes, iterations);
300  }
301
302  private static DirectoryException cannotEncodePassword(Exception e)
303  {
304    logger.traceException(e);
305
306    LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(CLASS_NAME, getExceptionMessage(e));
307    return new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e);
308  }
309
310  private static byte[] concatenateSaltPlusHash(byte[] saltBytes, byte[] digestBytes) {
311    final byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES];
312    System.arraycopy(saltBytes, 0, hashPlusSalt, 0, NUM_SALT_BYTES);
313    System.arraycopy(digestBytes, 0, hashPlusSalt, NUM_SALT_BYTES, digestBytes.length);
314    return hashPlusSalt;
315  }
316}