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.SaltedSHA512PasswordStorageSchemeCfg;
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 * 512-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 SaltedSHA512PasswordStorageScheme
065       extends PasswordStorageScheme<SaltedSHA512PasswordStorageSchemeCfg>
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.SaltedSHA512PasswordStorageScheme";
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 SHA512_LENGTH = 512 / 8;
086
087  /**
088   * The message digest that will actually be used to generate the 512-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 SaltedSHA512PasswordStorageScheme()
107  {
108    super();
109  }
110
111
112
113  /** {@inheritDoc} */
114  @Override
115  public void initializePasswordStorageScheme(
116                   SaltedSHA512PasswordStorageSchemeCfg configuration)
117         throws ConfigException, InitializationException
118  {
119    try
120    {
121      messageDigest =
122           MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_512);
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_512, e);
130      throw new InitializationException(message, e);
131    }
132
133    digestLock = new Object();
134    random     = new Random();
135  }
136
137
138
139  /** {@inheritDoc} */
140  @Override
141  public String getStorageSchemeName()
142  {
143    return STORAGE_SCHEME_NAME_SALTED_SHA_512;
144  }
145
146
147
148  /** {@inheritDoc} */
149  @Override
150  public ByteString encodePassword(ByteSequence plaintext)
151         throws DirectoryException
152  {
153    int plainBytesLength = plaintext.length();
154    byte[] saltBytes     = new byte[NUM_SALT_BYTES];
155    byte[] plainPlusSalt = new byte[plainBytesLength + NUM_SALT_BYTES];
156
157    plaintext.copyTo(plainPlusSalt);
158
159    byte[] digestBytes;
160
161    synchronized (digestLock)
162    {
163      try
164      {
165        // Generate the salt and put in the plain+salt array.
166        random.nextBytes(saltBytes);
167        System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength,
168                         NUM_SALT_BYTES);
169
170        // Create the hash from the concatenated value.
171        digestBytes = messageDigest.digest(plainPlusSalt);
172      }
173      catch (Exception e)
174      {
175        logger.traceException(e);
176
177        LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
178            CLASS_NAME, getExceptionMessage(e));
179        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
180                                     message, e);
181      }
182      finally
183      {
184        Arrays.fill(plainPlusSalt, (byte) 0);
185      }
186    }
187
188    // Append the salt to the hashed value and base64-the whole thing.
189    byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES];
190
191    System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length);
192    System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length,
193                     NUM_SALT_BYTES);
194
195    return ByteString.valueOfUtf8(Base64.encode(hashPlusSalt));
196  }
197
198
199
200  /** {@inheritDoc} */
201  @Override
202  public ByteString encodePasswordWithScheme(ByteSequence plaintext)
203         throws DirectoryException
204  {
205    StringBuilder buffer = new StringBuilder();
206    buffer.append('{');
207    buffer.append(STORAGE_SCHEME_NAME_SALTED_SHA_512);
208    buffer.append('}');
209
210    int plainBytesLength = plaintext.length();
211    byte[] saltBytes     = new byte[NUM_SALT_BYTES];
212    byte[] plainPlusSalt = new byte[plainBytesLength + NUM_SALT_BYTES];
213
214    plaintext.copyTo(plainPlusSalt);
215
216    byte[] digestBytes;
217
218    synchronized (digestLock)
219    {
220      try
221      {
222        // Generate the salt and put in the plain+salt array.
223        random.nextBytes(saltBytes);
224        System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength,
225                         NUM_SALT_BYTES);
226
227        // Create the hash from the concatenated value.
228        digestBytes = messageDigest.digest(plainPlusSalt);
229      }
230      catch (Exception e)
231      {
232        logger.traceException(e);
233
234        LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
235            CLASS_NAME, getExceptionMessage(e));
236        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
237                                     message, e);
238      }
239      finally
240      {
241        Arrays.fill(plainPlusSalt, (byte) 0);
242      }
243    }
244
245    // Append the salt to the hashed value and base64-the whole thing.
246    byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES];
247
248    System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length);
249    System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length,
250                     NUM_SALT_BYTES);
251    buffer.append(Base64.encode(hashPlusSalt));
252
253    return ByteString.valueOfUtf8(buffer);
254  }
255
256
257
258  /** {@inheritDoc} */
259  @Override
260  public boolean passwordMatches(ByteSequence plaintextPassword,
261                                 ByteSequence storedPassword)
262  {
263    // Base64-decode the stored value and take the first 512 bits
264    // (SHA512_LENGTH) as the digest.
265    byte[] saltBytes;
266    byte[] digestBytes = new byte[SHA512_LENGTH];
267    int saltLength = 0;
268
269    try
270    {
271      byte[] decodedBytes = Base64.decode(storedPassword.toString());
272
273      saltLength = decodedBytes.length - SHA512_LENGTH;
274      if (saltLength <= 0)
275      {
276        logger.error(ERR_PWSCHEME_INVALID_BASE64_DECODED_STORED_PASSWORD, storedPassword);
277        return false;
278      }
279      saltBytes = new byte[saltLength];
280      System.arraycopy(decodedBytes, 0, digestBytes, 0, SHA512_LENGTH);
281      System.arraycopy(decodedBytes, SHA512_LENGTH, saltBytes, 0,
282                       saltLength);
283    }
284    catch (Exception e)
285    {
286      logger.traceException(e);
287      logger.error(ERR_PWSCHEME_CANNOT_BASE64_DECODE_STORED_PASSWORD, storedPassword, e);
288      return false;
289    }
290
291
292    // Use the salt to generate a digest based on the provided plain-text value.
293    int plainBytesLength = plaintextPassword.length();
294    byte[] plainPlusSalt = new byte[plainBytesLength + saltLength];
295    plaintextPassword.copyTo(plainPlusSalt);
296    System.arraycopy(saltBytes, 0,plainPlusSalt, plainBytesLength,
297                     saltLength);
298
299    byte[] userDigestBytes;
300
301    synchronized (digestLock)
302    {
303      try
304      {
305        userDigestBytes = messageDigest.digest(plainPlusSalt);
306      }
307      catch (Exception e)
308      {
309        logger.traceException(e);
310
311        return false;
312      }
313      finally
314      {
315        Arrays.fill(plainPlusSalt, (byte) 0);
316      }
317    }
318
319    return Arrays.equals(digestBytes, userDigestBytes);
320  }
321
322
323
324  /** {@inheritDoc} */
325  @Override
326  public boolean supportsAuthPasswordSyntax()
327  {
328    // This storage scheme does support the authentication password syntax.
329    return true;
330  }
331
332
333
334  /** {@inheritDoc} */
335  @Override
336  public String getAuthPasswordSchemeName()
337  {
338    return AUTH_PASSWORD_SCHEME_NAME_SALTED_SHA_512;
339  }
340
341
342
343  /** {@inheritDoc} */
344  @Override
345  public ByteString encodeAuthPassword(ByteSequence plaintext)
346         throws DirectoryException
347  {
348    int plaintextLength = plaintext.length();
349    byte[] saltBytes     = new byte[NUM_SALT_BYTES];
350    byte[] plainPlusSalt = new byte[plaintextLength + NUM_SALT_BYTES];
351
352    plaintext.copyTo(plainPlusSalt);
353
354    byte[] digestBytes;
355
356    synchronized (digestLock)
357    {
358      try
359      {
360        // Generate the salt and put in the plain+salt array.
361        random.nextBytes(saltBytes);
362        System.arraycopy(saltBytes,0, plainPlusSalt, plaintextLength,
363                         NUM_SALT_BYTES);
364
365        // Create the hash from the concatenated value.
366        digestBytes = messageDigest.digest(plainPlusSalt);
367      }
368      catch (Exception e)
369      {
370        logger.traceException(e);
371
372        LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
373            CLASS_NAME, getExceptionMessage(e));
374        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
375                                     message, e);
376      }
377      finally
378      {
379        Arrays.fill(plainPlusSalt, (byte) 0);
380      }
381    }
382
383
384    // Encode and return the value.
385    StringBuilder authPWValue = new StringBuilder();
386    authPWValue.append(AUTH_PASSWORD_SCHEME_NAME_SALTED_SHA_512);
387    authPWValue.append('$');
388    authPWValue.append(Base64.encode(saltBytes));
389    authPWValue.append('$');
390    authPWValue.append(Base64.encode(digestBytes));
391
392    return ByteString.valueOfUtf8(authPWValue);
393  }
394
395
396
397  /** {@inheritDoc} */
398  @Override
399  public boolean authPasswordMatches(ByteSequence plaintextPassword,
400                                     String authInfo, String authValue)
401  {
402    byte[] saltBytes;
403    byte[] digestBytes;
404    try
405    {
406      saltBytes   = Base64.decode(authInfo);
407      digestBytes = Base64.decode(authValue);
408    }
409    catch (Exception e)
410    {
411      logger.traceException(e);
412
413      return false;
414    }
415
416
417    int plainBytesLength = plaintextPassword.length();
418    byte[] plainPlusSaltBytes = new byte[plainBytesLength + saltBytes.length];
419    plaintextPassword.copyTo(plainPlusSaltBytes);
420    System.arraycopy(saltBytes, 0, plainPlusSaltBytes, plainBytesLength,
421                     saltBytes.length);
422
423    synchronized (digestLock)
424    {
425      try
426      {
427        return Arrays.equals(digestBytes,
428                                  messageDigest.digest(plainPlusSaltBytes));
429      }
430      finally
431      {
432        Arrays.fill(plainPlusSaltBytes, (byte) 0);
433      }
434    }
435  }
436
437
438
439  /** {@inheritDoc} */
440  @Override
441  public boolean isReversible()
442  {
443    return false;
444  }
445
446
447
448  /** {@inheritDoc} */
449  @Override
450  public ByteString getPlaintextValue(ByteSequence storedPassword)
451         throws DirectoryException
452  {
453    LocalizableMessage message =
454        ERR_PWSCHEME_NOT_REVERSIBLE.get(STORAGE_SCHEME_NAME_SALTED_SHA_512);
455    throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
456  }
457
458
459
460  /** {@inheritDoc} */
461  @Override
462  public ByteString getAuthPasswordPlaintextValue(String authInfo,
463                                                  String authValue)
464         throws DirectoryException
465  {
466    LocalizableMessage message = ERR_PWSCHEME_NOT_REVERSIBLE.get(
467        AUTH_PASSWORD_SCHEME_NAME_SALTED_SHA_512);
468    throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
469  }
470
471
472
473  /** {@inheritDoc} */
474  @Override
475  public boolean isStorageSchemeSecure()
476  {
477    // SHA-2 should be considered secure.
478    return true;
479  }
480
481
482
483  /**
484   * Generates an encoded password string from the given clear-text password.
485   * This method is primarily intended for use when it is necessary to generate
486   * a password with the server offline (e.g., when setting the initial root
487   * user password).
488   *
489   * @param  passwordBytes  The bytes that make up the clear-text password.
490   *
491   * @return  The encoded password string, including the scheme name in curly
492   *          braces.
493   *
494   * @throws  DirectoryException  If a problem occurs during processing.
495   */
496  public static String encodeOffline(byte[] passwordBytes)
497         throws DirectoryException
498  {
499    byte[] saltBytes = new byte[NUM_SALT_BYTES];
500    new Random().nextBytes(saltBytes);
501
502    byte[] passwordPlusSalt = new byte[passwordBytes.length + NUM_SALT_BYTES];
503    System.arraycopy(passwordBytes, 0, passwordPlusSalt, 0,
504                     passwordBytes.length);
505    System.arraycopy(saltBytes, 0, passwordPlusSalt, passwordBytes.length,
506                     NUM_SALT_BYTES);
507
508    MessageDigest messageDigest;
509    try
510    {
511      messageDigest =
512           MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_512);
513    }
514    catch (Exception e)
515    {
516      LocalizableMessage message = ERR_PWSCHEME_CANNOT_INITIALIZE_MESSAGE_DIGEST.get(
517          MESSAGE_DIGEST_ALGORITHM_SHA_512, e);
518      throw new DirectoryException(ResultCode.OTHER, message, e);
519    }
520
521
522    byte[] digestBytes    = messageDigest.digest(passwordPlusSalt);
523    byte[] digestPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES];
524    System.arraycopy(digestBytes, 0, digestPlusSalt, 0, digestBytes.length);
525    System.arraycopy(saltBytes, 0, digestPlusSalt, digestBytes.length,
526                     NUM_SALT_BYTES);
527    Arrays.fill(passwordPlusSalt, (byte) 0);
528
529    return "{" + STORAGE_SCHEME_NAME_SALTED_SHA_512 + "}" +
530           Base64.encode(digestPlusSalt);
531  }
532}
533