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.SaltedSHA512PasswordStorageSchemeCfg;
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 * 512-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 SaltedSHA512PasswordStorageScheme
055       extends PasswordStorageScheme<SaltedSHA512PasswordStorageSchemeCfg>
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.SaltedSHA512PasswordStorageScheme";
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 SHA512_LENGTH = 512 / 8;
076
077  /**
078   * The message digest that will actually be used to generate the 512-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 SaltedSHA512PasswordStorageScheme()
097  {
098    super();
099  }
100
101
102
103  /** {@inheritDoc} */
104  @Override
105  public void initializePasswordStorageScheme(
106                   SaltedSHA512PasswordStorageSchemeCfg configuration)
107         throws ConfigException, InitializationException
108  {
109    try
110    {
111      messageDigest =
112           MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_512);
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_512, e);
120      throw new InitializationException(message, e);
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_512;
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_512);
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 512 bits
254    // (SHA512_LENGTH) as the digest.
255    byte[] saltBytes;
256    byte[] digestBytes = new byte[SHA512_LENGTH];
257    int saltLength = 0;
258
259    try
260    {
261      byte[] decodedBytes = Base64.decode(storedPassword.toString());
262
263      saltLength = decodedBytes.length - SHA512_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, SHA512_LENGTH);
271      System.arraycopy(decodedBytes, SHA512_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_512;
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_512);
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_512);
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_512);
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
472
473  /**
474   * Generates an encoded password string from the given clear-text password.
475   * This method is primarily intended for use when it is necessary to generate
476   * a password with the server offline (e.g., when setting the initial root
477   * user password).
478   *
479   * @param  passwordBytes  The bytes that make up the clear-text password.
480   *
481   * @return  The encoded password string, including the scheme name in curly
482   *          braces.
483   *
484   * @throws  DirectoryException  If a problem occurs during processing.
485   */
486  public static String encodeOffline(byte[] passwordBytes)
487         throws DirectoryException
488  {
489    byte[] saltBytes = new byte[NUM_SALT_BYTES];
490    new Random().nextBytes(saltBytes);
491
492    byte[] passwordPlusSalt = new byte[passwordBytes.length + NUM_SALT_BYTES];
493    System.arraycopy(passwordBytes, 0, passwordPlusSalt, 0,
494                     passwordBytes.length);
495    System.arraycopy(saltBytes, 0, passwordPlusSalt, passwordBytes.length,
496                     NUM_SALT_BYTES);
497
498    MessageDigest messageDigest;
499    try
500    {
501      messageDigest =
502           MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_512);
503    }
504    catch (Exception e)
505    {
506      LocalizableMessage message = ERR_PWSCHEME_CANNOT_INITIALIZE_MESSAGE_DIGEST.get(
507          MESSAGE_DIGEST_ALGORITHM_SHA_512, e);
508      throw new DirectoryException(ResultCode.OTHER, message, e);
509    }
510
511
512    byte[] digestBytes    = messageDigest.digest(passwordPlusSalt);
513    byte[] digestPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES];
514    System.arraycopy(digestBytes, 0, digestPlusSalt, 0, digestBytes.length);
515    System.arraycopy(saltBytes, 0, digestPlusSalt, digestBytes.length,
516                     NUM_SALT_BYTES);
517    Arrays.fill(passwordPlusSalt, (byte) 0);
518
519    return "{" + STORAGE_SCHEME_NAME_SALTED_SHA_512 + "}" +
520           Base64.encode(digestPlusSalt);
521  }
522}
523