001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2006-2008 Sun Microsystems, Inc.
025 *      Portions Copyright 2010-2015 ForgeRock AS.
026 */
027package org.opends.server.extensions;
028
029
030
031import java.security.MessageDigest;
032import java.util.Arrays;
033import java.util.Random;
034
035import org.forgerock.i18n.LocalizableMessage;
036import org.opends.server.admin.std.server.SaltedSHA384PasswordStorageSchemeCfg;
037import org.opends.server.api.PasswordStorageScheme;
038import org.forgerock.opendj.config.server.ConfigException;
039import org.opends.server.core.DirectoryServer;
040import org.forgerock.i18n.slf4j.LocalizedLogger;
041import org.opends.server.types.*;
042import org.forgerock.opendj.ldap.ResultCode;
043import org.forgerock.opendj.ldap.ByteString;
044import org.forgerock.opendj.ldap.ByteSequence;
045import org.opends.server.util.Base64;
046
047import static org.opends.messages.ExtensionMessages.*;
048import static org.opends.server.extensions.ExtensionsConstants.*;
049import static org.opends.server.util.StaticUtils.*;
050
051
052
053/**
054 * This class defines a Directory Server password storage scheme based on the
055 * 384-bit SHA-2 algorithm defined in FIPS 180-2.  This is a one-way digest
056 * algorithm so there is no way to retrieve the original clear-text version of
057 * the password from the hashed value (although this means that it is not
058 * suitable for things that need the clear-text password like DIGEST-MD5).  The
059 * values that it generates are also salted, which protects against dictionary
060 * attacks. It does this by generating a 64-bit random salt which is appended to
061 * the clear-text value.  A SHA-2 hash is then generated based on this, the salt
062 * is appended to the hash, and then the entire value is base64-encoded.
063 */
064public class SaltedSHA384PasswordStorageScheme
065       extends PasswordStorageScheme<SaltedSHA384PasswordStorageSchemeCfg>
066{
067  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
068
069  /**
070   * The fully-qualified name of this class.
071   */
072  private static final String CLASS_NAME =
073       "org.opends.server.extensions.SaltedSHA384PasswordStorageScheme";
074
075
076
077  /**
078   * The number of bytes of random data to use as the salt when generating the
079   * hashes.
080   */
081  private static final int NUM_SALT_BYTES = 8;
082
083
084  /** The size of the digest in bytes. */
085  private static final int SHA384_LENGTH = 384 / 8;
086
087  /**
088   * The message digest that will actually be used to generate the 384-bit SHA-2
089   * hashes.
090   */
091  private MessageDigest messageDigest;
092
093  /** The lock used to provide threadsafe access to the message digest. */
094  private Object digestLock;
095
096  /** The secure random number generator to use to generate the salt values. */
097  private Random random;
098
099
100
101  /**
102   * Creates a new instance of this password storage scheme.  Note that no
103   * initialization should be performed here, as all initialization should be
104   * done in the <CODE>initializePasswordStorageScheme</CODE> method.
105   */
106  public SaltedSHA384PasswordStorageScheme()
107  {
108    super();
109  }
110
111
112
113  /** {@inheritDoc} */
114  @Override
115  public void initializePasswordStorageScheme(
116                   SaltedSHA384PasswordStorageSchemeCfg configuration)
117         throws ConfigException, InitializationException
118  {
119    try
120    {
121      messageDigest =
122           MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_384);
123    }
124    catch (Exception e)
125    {
126      logger.traceException(e);
127
128      LocalizableMessage message = ERR_PWSCHEME_CANNOT_INITIALIZE_MESSAGE_DIGEST.get(
129          MESSAGE_DIGEST_ALGORITHM_SHA_384, e);
130      throw new InitializationException(message, e);
131    }
132
133
134    digestLock = new Object();
135    random     = new Random();
136  }
137
138
139
140  /** {@inheritDoc} */
141  @Override
142  public String getStorageSchemeName()
143  {
144    return STORAGE_SCHEME_NAME_SALTED_SHA_384;
145  }
146
147
148
149  /** {@inheritDoc} */
150  @Override
151  public ByteString encodePassword(ByteSequence plaintext)
152         throws DirectoryException
153  {
154    int plainBytesLength = plaintext.length();
155    byte[] saltBytes     = new byte[NUM_SALT_BYTES];
156    byte[] plainPlusSalt = new byte[plainBytesLength + NUM_SALT_BYTES];
157
158    plaintext.copyTo(plainPlusSalt);
159
160    byte[] digestBytes;
161
162    synchronized (digestLock)
163    {
164      try
165      {
166        // Generate the salt and put in the plain+salt array.
167        random.nextBytes(saltBytes);
168        System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength,
169                         NUM_SALT_BYTES);
170
171        // Create the hash from the concatenated value.
172        digestBytes = messageDigest.digest(plainPlusSalt);
173      }
174      catch (Exception e)
175      {
176        logger.traceException(e);
177
178        LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
179            CLASS_NAME, getExceptionMessage(e));
180        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
181                                     message, e);
182      }
183      finally
184      {
185        Arrays.fill(plainPlusSalt, (byte) 0);
186      }
187    }
188
189    // Append the salt to the hashed value and base64-the whole thing.
190    byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES];
191
192    System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length);
193    System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length,
194                     NUM_SALT_BYTES);
195
196    return ByteString.valueOfUtf8(Base64.encode(hashPlusSalt));
197  }
198
199
200
201  /** {@inheritDoc} */
202  @Override
203  public ByteString encodePasswordWithScheme(ByteSequence plaintext)
204         throws DirectoryException
205  {
206    StringBuilder buffer = new StringBuilder();
207    buffer.append('{');
208    buffer.append(STORAGE_SCHEME_NAME_SALTED_SHA_384);
209    buffer.append('}');
210
211    int plainBytesLength = plaintext.length();
212    byte[] saltBytes     = new byte[NUM_SALT_BYTES];
213    byte[] plainPlusSalt = new byte[plainBytesLength + NUM_SALT_BYTES];
214
215    plaintext.copyTo(plainPlusSalt);
216
217    byte[] digestBytes;
218
219    synchronized (digestLock)
220    {
221      try
222      {
223        // Generate the salt and put in the plain+salt array.
224        random.nextBytes(saltBytes);
225        System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength,
226                         NUM_SALT_BYTES);
227
228        // Create the hash from the concatenated value.
229        digestBytes = messageDigest.digest(plainPlusSalt);
230      }
231      catch (Exception e)
232      {
233        logger.traceException(e);
234
235        LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
236            CLASS_NAME, getExceptionMessage(e));
237        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
238                                     message, e);
239      }
240      finally
241      {
242        Arrays.fill(plainPlusSalt, (byte) 0);
243      }
244    }
245
246    // Append the salt to the hashed value and base64-the whole thing.
247    byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES];
248
249    System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length);
250    System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length,
251                     NUM_SALT_BYTES);
252    buffer.append(Base64.encode(hashPlusSalt));
253
254    return ByteString.valueOfUtf8(buffer);
255  }
256
257
258
259  /** {@inheritDoc} */
260  @Override
261  public boolean passwordMatches(ByteSequence plaintextPassword,
262                                 ByteSequence storedPassword)
263  {
264    // Base64-decode the stored value and take the first 384 bits
265    // (SHA384_LENGTH) as the digest.
266    byte[] saltBytes;
267    byte[] digestBytes = new byte[SHA384_LENGTH];
268    int saltLength = 0;
269
270    try
271    {
272      byte[] decodedBytes = Base64.decode(storedPassword.toString());
273
274      saltLength = decodedBytes.length - SHA384_LENGTH;
275      if (saltLength <= 0)
276      {
277        logger.error(ERR_PWSCHEME_INVALID_BASE64_DECODED_STORED_PASSWORD, storedPassword);
278        return false;
279      }
280      saltBytes = new byte[saltLength];
281      System.arraycopy(decodedBytes, 0, digestBytes, 0, SHA384_LENGTH);
282      System.arraycopy(decodedBytes, SHA384_LENGTH, saltBytes, 0,
283                       saltLength);
284    }
285    catch (Exception e)
286    {
287      logger.traceException(e);
288      logger.error(ERR_PWSCHEME_CANNOT_BASE64_DECODE_STORED_PASSWORD, storedPassword, e);
289      return false;
290    }
291
292
293    // Use the salt to generate a digest based on the provided plain-text value.
294    int plainBytesLength = plaintextPassword.length();
295    byte[] plainPlusSalt = new byte[plainBytesLength + saltLength];
296    plaintextPassword.copyTo(plainPlusSalt);
297    System.arraycopy(saltBytes, 0,plainPlusSalt, plainBytesLength,
298                     saltLength);
299
300    byte[] userDigestBytes;
301
302    synchronized (digestLock)
303    {
304      try
305      {
306        userDigestBytes = messageDigest.digest(plainPlusSalt);
307      }
308      catch (Exception e)
309      {
310        logger.traceException(e);
311
312        return false;
313      }
314      finally
315      {
316        Arrays.fill(plainPlusSalt, (byte) 0);
317      }
318    }
319
320    return Arrays.equals(digestBytes, userDigestBytes);
321  }
322
323
324
325  /** {@inheritDoc} */
326  @Override
327  public boolean supportsAuthPasswordSyntax()
328  {
329    // This storage scheme does support the authentication password syntax.
330    return true;
331  }
332
333
334
335  /** {@inheritDoc} */
336  @Override
337  public String getAuthPasswordSchemeName()
338  {
339    return AUTH_PASSWORD_SCHEME_NAME_SALTED_SHA_384;
340  }
341
342
343
344  /** {@inheritDoc} */
345  @Override
346  public ByteString encodeAuthPassword(ByteSequence plaintext)
347         throws DirectoryException
348  {
349    int plaintextLength = plaintext.length();
350    byte[] saltBytes     = new byte[NUM_SALT_BYTES];
351    byte[] plainPlusSalt = new byte[plaintextLength + NUM_SALT_BYTES];
352
353    plaintext.copyTo(plainPlusSalt);
354
355    byte[] digestBytes;
356
357    synchronized (digestLock)
358    {
359      try
360      {
361        // Generate the salt and put in the plain+salt array.
362        random.nextBytes(saltBytes);
363        System.arraycopy(saltBytes,0, plainPlusSalt, plaintextLength,
364                         NUM_SALT_BYTES);
365
366        // Create the hash from the concatenated value.
367        digestBytes = messageDigest.digest(plainPlusSalt);
368      }
369      catch (Exception e)
370      {
371        logger.traceException(e);
372
373        LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
374            CLASS_NAME, getExceptionMessage(e));
375        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
376                                     message, e);
377      }
378      finally
379      {
380        Arrays.fill(plainPlusSalt, (byte) 0);
381      }
382    }
383
384
385    // Encode and return the value.
386    StringBuilder authPWValue = new StringBuilder();
387    authPWValue.append(AUTH_PASSWORD_SCHEME_NAME_SALTED_SHA_384);
388    authPWValue.append('$');
389    authPWValue.append(Base64.encode(saltBytes));
390    authPWValue.append('$');
391    authPWValue.append(Base64.encode(digestBytes));
392
393    return ByteString.valueOfUtf8(authPWValue);
394  }
395
396
397
398  /** {@inheritDoc} */
399  @Override
400  public boolean authPasswordMatches(ByteSequence plaintextPassword,
401                                     String authInfo, String authValue)
402  {
403    byte[] saltBytes;
404    byte[] digestBytes;
405    try
406    {
407      saltBytes   = Base64.decode(authInfo);
408      digestBytes = Base64.decode(authValue);
409    }
410    catch (Exception e)
411    {
412      logger.traceException(e);
413
414      return false;
415    }
416
417
418    int plainBytesLength = plaintextPassword.length();
419    byte[] plainPlusSaltBytes = new byte[plainBytesLength + saltBytes.length];
420    plaintextPassword.copyTo(plainPlusSaltBytes);
421    System.arraycopy(saltBytes, 0, plainPlusSaltBytes, plainBytesLength,
422                     saltBytes.length);
423
424    synchronized (digestLock)
425    {
426      try
427      {
428        return Arrays.equals(digestBytes,
429                                  messageDigest.digest(plainPlusSaltBytes));
430      }
431      finally
432      {
433        Arrays.fill(plainPlusSaltBytes, (byte) 0);
434      }
435    }
436  }
437
438
439
440  /** {@inheritDoc} */
441  @Override
442  public boolean isReversible()
443  {
444    return false;
445  }
446
447
448
449  /** {@inheritDoc} */
450  @Override
451  public ByteString getPlaintextValue(ByteSequence storedPassword)
452         throws DirectoryException
453  {
454    LocalizableMessage message =
455        ERR_PWSCHEME_NOT_REVERSIBLE.get(STORAGE_SCHEME_NAME_SALTED_SHA_384);
456    throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
457  }
458
459
460
461  /** {@inheritDoc} */
462  @Override
463  public ByteString getAuthPasswordPlaintextValue(String authInfo,
464                                                  String authValue)
465         throws DirectoryException
466  {
467    LocalizableMessage message = ERR_PWSCHEME_NOT_REVERSIBLE.get(
468        AUTH_PASSWORD_SCHEME_NAME_SALTED_SHA_384);
469    throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
470  }
471
472
473
474  /** {@inheritDoc} */
475  @Override
476  public boolean isStorageSchemeSecure()
477  {
478    // SHA-2 should be considered secure.
479    return true;
480  }
481}
482