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