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