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