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-2015 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.opends.server.admin.std.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  /** {@inheritDoc} */
083  @Override
084  public void initializePasswordStorageScheme(PKCS5S2PasswordStorageSchemeCfg configuration)
085      throws InitializationException
086  {
087    try
088    {
089      random = SecureRandom.getInstance(SECURE_PRNG_SHA1);
090      // Just try to verify if the algorithm is supported
091      SecretKeyFactory.getInstance(MESSAGE_DIGEST_ALGORITHM_PBKDF2);
092    }
093    catch (NoSuchAlgorithmException e)
094    {
095      throw new InitializationException(null);
096    }
097  }
098
099  /** {@inheritDoc} */
100  @Override
101  public String getStorageSchemeName()
102  {
103    return STORAGE_SCHEME_NAME_PKCS5S2;
104  }
105
106  /** {@inheritDoc} */
107  @Override
108  public ByteString encodePassword(ByteSequence plaintext)
109      throws DirectoryException
110  {
111    byte[] saltBytes      = new byte[NUM_SALT_BYTES];
112    byte[] digestBytes = encodeWithRandomSalt(plaintext, saltBytes,random);
113    byte[] hashPlusSalt = concatenateSaltPlusHash(saltBytes, digestBytes);
114
115    return ByteString.valueOfUtf8(Base64.encode(hashPlusSalt));
116  }
117
118  /** {@inheritDoc} */
119  @Override
120  public ByteString encodePasswordWithScheme(ByteSequence plaintext)
121      throws DirectoryException
122  {
123    return ByteString.valueOfUtf8('{' + STORAGE_SCHEME_NAME_PKCS5S2 + '}' + encodePassword(plaintext));
124  }
125
126  /** {@inheritDoc} */
127  @Override
128  public boolean passwordMatches(ByteSequence plaintextPassword, ByteSequence storedPassword)
129  {
130    // Base64-decode the value and take the first 16 bytes as the salt.
131    try
132    {
133      String stored = storedPassword.toString();
134      byte[] decodedBytes = Base64.decode(stored);
135
136      if (decodedBytes.length != NUM_SALT_BYTES + SHA1_LENGTH)
137      {
138        logger.error(ERR_PWSCHEME_INVALID_BASE64_DECODED_STORED_PASSWORD.get(storedPassword.toString()));
139        return false;
140      }
141
142      final int saltLength = NUM_SALT_BYTES;
143      final byte[] digestBytes = new byte[SHA1_LENGTH];
144      final byte[] saltBytes = new byte[saltLength];
145      System.arraycopy(decodedBytes, 0, saltBytes, 0, saltLength);
146      System.arraycopy(decodedBytes, saltLength, digestBytes, 0, SHA1_LENGTH);
147      return encodeAndMatch(plaintextPassword, saltBytes, digestBytes, iterations);
148    }
149    catch (Exception e)
150    {
151      logger.traceException(e);
152      logger.error(ERR_PWSCHEME_CANNOT_BASE64_DECODE_STORED_PASSWORD.get(storedPassword.toString(), String.valueOf(e)));
153      return false;
154    }
155  }
156
157  /** {@inheritDoc} */
158  @Override
159  public boolean supportsAuthPasswordSyntax()
160  {
161    return true;
162  }
163
164  /** {@inheritDoc} */
165  @Override
166  public String getAuthPasswordSchemeName()
167  {
168    return AUTH_PASSWORD_SCHEME_NAME_PKCS5S2;
169  }
170
171  /** {@inheritDoc} */
172  @Override
173  public ByteString encodeAuthPassword(ByteSequence plaintext)
174      throws DirectoryException
175  {
176    byte[] saltBytes      = new byte[NUM_SALT_BYTES];
177    byte[] digestBytes = encodeWithRandomSalt(plaintext, saltBytes,random);
178    // Encode and return the value.
179    return ByteString.valueOfUtf8(AUTH_PASSWORD_SCHEME_NAME_PKCS5S2 + '$' + iterations
180        + ':' + Base64.encode(saltBytes) + '$' + Base64.encode(digestBytes));
181  }
182
183  /** {@inheritDoc} */
184  @Override
185  public boolean authPasswordMatches(ByteSequence plaintextPassword, String authInfo, String authValue)
186  {
187    try
188    {
189      int pos = authInfo.indexOf(':');
190      if (pos == -1)
191      {
192        throw new Exception();
193      }
194      int iterations = Integer.parseInt(authInfo.substring(0, pos));
195      byte[] saltBytes   = Base64.decode(authInfo.substring(pos + 1));
196      byte[] digestBytes = Base64.decode(authValue);
197      return encodeAndMatch(plaintextPassword, saltBytes, digestBytes, iterations);
198    }
199    catch (Exception e)
200    {
201      logger.traceException(e);
202      return false;
203    }
204  }
205
206  /** {@inheritDoc} */
207  @Override
208  public boolean isReversible()
209  {
210    return false;
211  }
212
213  /** {@inheritDoc} */
214  @Override
215  public ByteString getPlaintextValue(ByteSequence storedPassword)
216      throws DirectoryException
217  {
218    LocalizableMessage message = ERR_PWSCHEME_NOT_REVERSIBLE.get(STORAGE_SCHEME_NAME_PKCS5S2);
219    throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
220  }
221
222  /** {@inheritDoc} */
223  @Override
224  public ByteString getAuthPasswordPlaintextValue(String authInfo, String authValue)
225      throws DirectoryException
226  {
227    LocalizableMessage message = ERR_PWSCHEME_NOT_REVERSIBLE.get(AUTH_PASSWORD_SCHEME_NAME_PKCS5S2);
228    throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
229  }
230
231  /** {@inheritDoc} */
232  @Override
233  public boolean isStorageSchemeSecure()
234  {
235    return true;
236  }
237
238
239
240  /**
241   * Generates an encoded password string from the given clear-text password.
242   * This method is primarily intended for use when it is necessary to generate a password with the server
243   * offline (e.g., when setting the initial root user password).
244   *
245   * @param  passwordBytes  The bytes that make up the clear-text password.
246   * @return  The encoded password string, including the scheme name in curly braces.
247   * @throws  DirectoryException  If a problem occurs during processing.
248   */
249  public static String encodeOffline(byte[] passwordBytes)
250      throws DirectoryException
251  {
252    byte[] saltBytes = new byte[NUM_SALT_BYTES];
253    byte[] digestBytes = encodeWithRandomSalt(ByteString.wrap(passwordBytes), saltBytes);
254    byte[] hashPlusSalt = concatenateSaltPlusHash(saltBytes, digestBytes);
255
256    return '{' + STORAGE_SCHEME_NAME_PKCS5S2 + '}' + Base64.encode(hashPlusSalt);
257  }
258
259  private static byte[] encodeWithRandomSalt(ByteString plaintext, byte[] saltBytes)
260      throws DirectoryException
261  {
262    try
263    {
264      final SecureRandom random = SecureRandom.getInstance(SECURE_PRNG_SHA1);
265      return encodeWithRandomSalt(plaintext, saltBytes, random);
266    }
267    catch (DirectoryException e)
268    {
269      throw e;
270    }
271    catch (Exception e)
272    {
273      throw cannotEncodePassword(e);
274    }
275  }
276
277  private static byte[] encodeWithSalt(ByteSequence plaintext, byte[] saltBytes, int iterations)
278      throws DirectoryException
279  {
280    final char[] plaintextChars = plaintext.toString().toCharArray();
281    try
282    {
283      final SecretKeyFactory factory = SecretKeyFactory.getInstance(MESSAGE_DIGEST_ALGORITHM_PBKDF2);
284      KeySpec spec = new PBEKeySpec(plaintextChars, saltBytes, iterations, SHA1_LENGTH * 8);
285      return factory.generateSecret(spec).getEncoded();
286    }
287    catch (Exception e)
288    {
289      throw cannotEncodePassword(e);
290    }
291    finally
292    {
293      Arrays.fill(plaintextChars, '0');
294    }
295  }
296
297  private boolean encodeAndMatch(ByteSequence plaintext, byte[] saltBytes, byte[] digestBytes, int iterations)
298  {
299     try
300     {
301       final byte[] userDigestBytes = encodeWithSalt(plaintext, saltBytes, iterations);
302       return Arrays.equals(digestBytes, userDigestBytes);
303     }
304     catch (Exception e)
305     {
306       return false;
307     }
308  }
309
310  private static byte[] encodeWithRandomSalt(ByteSequence plaintext, byte[] saltBytes, SecureRandom random)
311      throws DirectoryException
312  {
313    random.nextBytes(saltBytes);
314    return encodeWithSalt(plaintext, saltBytes, iterations);
315  }
316
317  private static DirectoryException cannotEncodePassword(Exception e)
318  {
319    logger.traceException(e);
320
321    LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(CLASS_NAME, getExceptionMessage(e));
322    return new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e);
323  }
324
325  private static byte[] concatenateSaltPlusHash(byte[] saltBytes, byte[] digestBytes) {
326    final byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES];
327    System.arraycopy(saltBytes, 0, hashPlusSalt, 0, NUM_SALT_BYTES);
328    System.arraycopy(digestBytes, 0, hashPlusSalt, NUM_SALT_BYTES, digestBytes.length);
329    return hashPlusSalt;
330  }
331
332}