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.SaltedSHA256PasswordStorageSchemeCfg;
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 * 256-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 SaltedSHA256PasswordStorageScheme
065       extends PasswordStorageScheme<SaltedSHA256PasswordStorageSchemeCfg>
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.SaltedSHA256PasswordStorageScheme";
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  /** Size of the dgiest in bytes. */
084  private static final int SHA256_LENGTH = 256 / 8;
085
086  /**
087   * The message digest that will actually be used to generate the 256-bit SHA-2
088   * hashes.
089   */
090  private MessageDigest messageDigest;
091
092  /** The lock used to provide threadsafe access to the message digest. */
093  private Object digestLock;
094
095  /** The secure random number generator to use to generate the salt values. */
096  private Random random;
097
098
099
100  /**
101   * Creates a new instance of this password storage scheme.  Note that no
102   * initialization should be performed here, as all initialization should be
103   * done in the <CODE>initializePasswordStorageScheme</CODE> method.
104   */
105  public SaltedSHA256PasswordStorageScheme()
106  {
107    super();
108  }
109
110
111
112  /** {@inheritDoc} */
113  @Override
114  public void initializePasswordStorageScheme(
115                   SaltedSHA256PasswordStorageSchemeCfg configuration)
116         throws ConfigException, InitializationException
117  {
118    try
119    {
120      messageDigest =
121           MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_256);
122    }
123    catch (Exception e)
124    {
125      logger.traceException(e);
126
127      LocalizableMessage message = ERR_PWSCHEME_CANNOT_INITIALIZE_MESSAGE_DIGEST.get(
128          MESSAGE_DIGEST_ALGORITHM_SHA_256, e);
129      throw new InitializationException(message, e);
130    }
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_256;
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_256);
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 256 bits
264    // (SHA256_LENGTH) as the digest.
265    byte[] saltBytes;
266    byte[] digestBytes = new byte[SHA256_LENGTH];
267    int saltLength = 0;
268
269    try
270    {
271      byte[] decodedBytes = Base64.decode(storedPassword.toString());
272
273      saltLength = decodedBytes.length - SHA256_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, SHA256_LENGTH);
281      System.arraycopy(decodedBytes, SHA256_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_256;
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_256);
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_256);
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_256);
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