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.SaltedSHA256PasswordStorageSchemeCfg;
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 * 256-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 SaltedSHA256PasswordStorageScheme
055       extends PasswordStorageScheme<SaltedSHA256PasswordStorageSchemeCfg>
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.SaltedSHA256PasswordStorageScheme";
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  /** Size of the dgiest in bytes. */
074  private static final int SHA256_LENGTH = 256 / 8;
075
076  /**
077   * The message digest that will actually be used to generate the 256-bit SHA-2
078   * hashes.
079   */
080  private MessageDigest messageDigest;
081
082  /** The lock used to provide threadsafe access to the message digest. */
083  private Object digestLock;
084
085  /** The secure random number generator to use to generate the salt values. */
086  private Random random;
087
088
089
090  /**
091   * Creates a new instance of this password storage scheme.  Note that no
092   * initialization should be performed here, as all initialization should be
093   * done in the <CODE>initializePasswordStorageScheme</CODE> method.
094   */
095  public SaltedSHA256PasswordStorageScheme()
096  {
097    super();
098  }
099
100
101
102  /** {@inheritDoc} */
103  @Override
104  public void initializePasswordStorageScheme(
105                   SaltedSHA256PasswordStorageSchemeCfg configuration)
106         throws ConfigException, InitializationException
107  {
108    try
109    {
110      messageDigest =
111           MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_256);
112    }
113    catch (Exception e)
114    {
115      logger.traceException(e);
116
117      LocalizableMessage message = ERR_PWSCHEME_CANNOT_INITIALIZE_MESSAGE_DIGEST.get(
118          MESSAGE_DIGEST_ALGORITHM_SHA_256, e);
119      throw new InitializationException(message, e);
120    }
121
122
123    digestLock = new Object();
124    random     = new Random();
125  }
126
127
128
129  /** {@inheritDoc} */
130  @Override
131  public String getStorageSchemeName()
132  {
133    return STORAGE_SCHEME_NAME_SALTED_SHA_256;
134  }
135
136
137
138  /** {@inheritDoc} */
139  @Override
140  public ByteString encodePassword(ByteSequence plaintext)
141         throws DirectoryException
142  {
143    int plainBytesLength = plaintext.length();
144    byte[] saltBytes     = new byte[NUM_SALT_BYTES];
145    byte[] plainPlusSalt = new byte[plainBytesLength + NUM_SALT_BYTES];
146
147    plaintext.copyTo(plainPlusSalt);
148
149    byte[] digestBytes;
150
151    synchronized (digestLock)
152    {
153      try
154      {
155        // Generate the salt and put in the plain+salt array.
156        random.nextBytes(saltBytes);
157        System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength,
158                         NUM_SALT_BYTES);
159
160        // Create the hash from the concatenated value.
161        digestBytes = messageDigest.digest(plainPlusSalt);
162      }
163      catch (Exception e)
164      {
165        logger.traceException(e);
166
167        LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
168            CLASS_NAME, getExceptionMessage(e));
169        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
170                                     message, e);
171      }
172      finally
173      {
174        Arrays.fill(plainPlusSalt, (byte) 0);
175      }
176    }
177
178    // Append the salt to the hashed value and base64-the whole thing.
179    byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES];
180
181    System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length);
182    System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length,
183                     NUM_SALT_BYTES);
184
185    return ByteString.valueOfUtf8(Base64.encode(hashPlusSalt));
186  }
187
188
189
190  /** {@inheritDoc} */
191  @Override
192  public ByteString encodePasswordWithScheme(ByteSequence plaintext)
193         throws DirectoryException
194  {
195    StringBuilder buffer = new StringBuilder();
196    buffer.append('{');
197    buffer.append(STORAGE_SCHEME_NAME_SALTED_SHA_256);
198    buffer.append('}');
199
200    int plainBytesLength = plaintext.length();
201    byte[] saltBytes     = new byte[NUM_SALT_BYTES];
202    byte[] plainPlusSalt = new byte[plainBytesLength + NUM_SALT_BYTES];
203
204    plaintext.copyTo(plainPlusSalt);
205
206    byte[] digestBytes;
207
208    synchronized (digestLock)
209    {
210      try
211      {
212        // Generate the salt and put in the plain+salt array.
213        random.nextBytes(saltBytes);
214        System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength,
215                         NUM_SALT_BYTES);
216
217        // Create the hash from the concatenated value.
218        digestBytes = messageDigest.digest(plainPlusSalt);
219      }
220      catch (Exception e)
221      {
222        logger.traceException(e);
223
224        LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
225            CLASS_NAME, getExceptionMessage(e));
226        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
227                                     message, e);
228      }
229      finally
230      {
231        Arrays.fill(plainPlusSalt, (byte) 0);
232      }
233    }
234
235    // Append the salt to the hashed value and base64-the whole thing.
236    byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES];
237
238    System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length);
239    System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length,
240                     NUM_SALT_BYTES);
241    buffer.append(Base64.encode(hashPlusSalt));
242
243    return ByteString.valueOfUtf8(buffer);
244  }
245
246
247
248  /** {@inheritDoc} */
249  @Override
250  public boolean passwordMatches(ByteSequence plaintextPassword,
251                                 ByteSequence storedPassword)
252  {
253    // Base64-decode the stored value and take the first 256 bits
254    // (SHA256_LENGTH) as the digest.
255    byte[] saltBytes;
256    byte[] digestBytes = new byte[SHA256_LENGTH];
257    int saltLength = 0;
258
259    try
260    {
261      byte[] decodedBytes = Base64.decode(storedPassword.toString());
262
263      saltLength = decodedBytes.length - SHA256_LENGTH;
264      if (saltLength <= 0)
265      {
266        logger.error(ERR_PWSCHEME_INVALID_BASE64_DECODED_STORED_PASSWORD, storedPassword);
267        return false;
268      }
269      saltBytes = new byte[saltLength];
270      System.arraycopy(decodedBytes, 0, digestBytes, 0, SHA256_LENGTH);
271      System.arraycopy(decodedBytes, SHA256_LENGTH, saltBytes, 0,
272                       saltLength);
273    }
274    catch (Exception e)
275    {
276      logger.traceException(e);
277      logger.error(ERR_PWSCHEME_CANNOT_BASE64_DECODE_STORED_PASSWORD, storedPassword, e);
278      return false;
279    }
280
281
282    // Use the salt to generate a digest based on the provided plain-text value.
283    int plainBytesLength = plaintextPassword.length();
284    byte[] plainPlusSalt = new byte[plainBytesLength + saltLength];
285    plaintextPassword.copyTo(plainPlusSalt);
286    System.arraycopy(saltBytes, 0,plainPlusSalt, plainBytesLength,
287                     saltLength);
288
289    byte[] userDigestBytes;
290
291    synchronized (digestLock)
292    {
293      try
294      {
295        userDigestBytes = messageDigest.digest(plainPlusSalt);
296      }
297      catch (Exception e)
298      {
299        logger.traceException(e);
300
301        return false;
302      }
303      finally
304      {
305        Arrays.fill(plainPlusSalt, (byte) 0);
306      }
307    }
308
309    return Arrays.equals(digestBytes, userDigestBytes);
310  }
311
312
313
314  /** {@inheritDoc} */
315  @Override
316  public boolean supportsAuthPasswordSyntax()
317  {
318    // This storage scheme does support the authentication password syntax.
319    return true;
320  }
321
322
323
324  /** {@inheritDoc} */
325  @Override
326  public String getAuthPasswordSchemeName()
327  {
328    return AUTH_PASSWORD_SCHEME_NAME_SALTED_SHA_256;
329  }
330
331
332
333  /** {@inheritDoc} */
334  @Override
335  public ByteString encodeAuthPassword(ByteSequence plaintext)
336         throws DirectoryException
337  {
338    int plaintextLength = plaintext.length();
339    byte[] saltBytes     = new byte[NUM_SALT_BYTES];
340    byte[] plainPlusSalt = new byte[plaintextLength + NUM_SALT_BYTES];
341
342    plaintext.copyTo(plainPlusSalt);
343
344    byte[] digestBytes;
345
346    synchronized (digestLock)
347    {
348      try
349      {
350        // Generate the salt and put in the plain+salt array.
351        random.nextBytes(saltBytes);
352        System.arraycopy(saltBytes,0, plainPlusSalt, plaintextLength,
353                         NUM_SALT_BYTES);
354
355        // Create the hash from the concatenated value.
356        digestBytes = messageDigest.digest(plainPlusSalt);
357      }
358      catch (Exception e)
359      {
360        logger.traceException(e);
361
362        LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
363            CLASS_NAME, getExceptionMessage(e));
364        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
365                                     message, e);
366      }
367      finally
368      {
369        Arrays.fill(plainPlusSalt, (byte) 0);
370      }
371    }
372
373
374    // Encode and return the value.
375    StringBuilder authPWValue = new StringBuilder();
376    authPWValue.append(AUTH_PASSWORD_SCHEME_NAME_SALTED_SHA_256);
377    authPWValue.append('$');
378    authPWValue.append(Base64.encode(saltBytes));
379    authPWValue.append('$');
380    authPWValue.append(Base64.encode(digestBytes));
381
382    return ByteString.valueOfUtf8(authPWValue);
383  }
384
385
386
387  /** {@inheritDoc} */
388  @Override
389  public boolean authPasswordMatches(ByteSequence plaintextPassword,
390                                     String authInfo, String authValue)
391  {
392    byte[] saltBytes;
393    byte[] digestBytes;
394    try
395    {
396      saltBytes   = Base64.decode(authInfo);
397      digestBytes = Base64.decode(authValue);
398    }
399    catch (Exception e)
400    {
401      logger.traceException(e);
402
403      return false;
404    }
405
406
407    int plainBytesLength = plaintextPassword.length();
408    byte[] plainPlusSaltBytes = new byte[plainBytesLength + saltBytes.length];
409    plaintextPassword.copyTo(plainPlusSaltBytes);
410    System.arraycopy(saltBytes, 0, plainPlusSaltBytes, plainBytesLength,
411                     saltBytes.length);
412
413    synchronized (digestLock)
414    {
415      try
416      {
417        return Arrays.equals(digestBytes,
418                                  messageDigest.digest(plainPlusSaltBytes));
419      }
420      finally
421      {
422        Arrays.fill(plainPlusSaltBytes, (byte) 0);
423      }
424    }
425  }
426
427
428
429  /** {@inheritDoc} */
430  @Override
431  public boolean isReversible()
432  {
433    return false;
434  }
435
436
437
438  /** {@inheritDoc} */
439  @Override
440  public ByteString getPlaintextValue(ByteSequence storedPassword)
441         throws DirectoryException
442  {
443    LocalizableMessage message =
444        ERR_PWSCHEME_NOT_REVERSIBLE.get(STORAGE_SCHEME_NAME_SALTED_SHA_256);
445    throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
446  }
447
448
449
450  /** {@inheritDoc} */
451  @Override
452  public ByteString getAuthPasswordPlaintextValue(String authInfo,
453                                                  String authValue)
454         throws DirectoryException
455  {
456    LocalizableMessage message = ERR_PWSCHEME_NOT_REVERSIBLE.get(
457        AUTH_PASSWORD_SCHEME_NAME_SALTED_SHA_256);
458    throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
459  }
460
461
462
463  /** {@inheritDoc} */
464  @Override
465  public boolean isStorageSchemeSecure()
466  {
467    // SHA-2 should be considered secure.
468    return true;
469  }
470}
471