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