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 2013-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.SaltedMD5PasswordStorageSchemeCfg;
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 * MD5 algorithm defined in RFC 1321.  This is a one-way digest algorithm so
056 * 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 MD5 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 SaltedMD5PasswordStorageScheme
065       extends PasswordStorageScheme<SaltedMD5PasswordStorageSchemeCfg>
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.SaltedMD5PasswordStorageScheme";
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 MD5 algorithm produces. */
084  private static final int MD5_LENGTH = 16;
085
086
087  /** The message digest that will actually be used to generate the MD5 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 SaltedMD5PasswordStorageScheme()
104  {
105    super();
106  }
107
108
109
110  /** {@inheritDoc} */
111  @Override
112  public void initializePasswordStorageScheme(
113                   SaltedMD5PasswordStorageSchemeCfg configuration)
114         throws ConfigException, InitializationException
115  {
116    try
117    {
118      messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_MD5);
119    }
120    catch (Exception e)
121    {
122      logger.traceException(e);
123
124      LocalizableMessage message = ERR_PWSCHEME_CANNOT_INITIALIZE_MESSAGE_DIGEST.get(MESSAGE_DIGEST_ALGORITHM_MD5, e);
125      throw new InitializationException(message, e);
126    }
127
128
129    digestLock = new Object();
130    random     = new Random();
131  }
132
133
134
135  /** {@inheritDoc} */
136  @Override
137  public String getStorageSchemeName()
138  {
139    return STORAGE_SCHEME_NAME_SALTED_MD5;
140  }
141
142
143
144  /** {@inheritDoc} */
145  @Override
146  public ByteString encodePassword(ByteSequence plaintext)
147         throws DirectoryException
148  {
149    int plainBytesLength = plaintext.length();
150    byte[] saltBytes     = new byte[NUM_SALT_BYTES];
151    byte[] plainPlusSalt = new byte[plainBytesLength + NUM_SALT_BYTES];
152
153    plaintext.copyTo(plainPlusSalt);
154
155    byte[] digestBytes;
156
157    synchronized (digestLock)
158    {
159      try
160      {
161        // Generate the salt and put in the plain+salt array.
162        random.nextBytes(saltBytes);
163        System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength,
164                         NUM_SALT_BYTES);
165
166        // Create the hash from the concatenated value.
167        digestBytes = messageDigest.digest(plainPlusSalt);
168      }
169      catch (Exception e)
170      {
171        logger.traceException(e);
172
173        LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
174            CLASS_NAME, getExceptionMessage(e));
175        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
176                                     message, e);
177      }
178      finally
179      {
180        Arrays.fill(plainPlusSalt, (byte) 0);
181      }
182    }
183
184    // Append the salt to the hashed value and base64-the whole thing.
185    byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES];
186
187    System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length);
188    System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length,
189                     NUM_SALT_BYTES);
190
191    return ByteString.valueOfUtf8(Base64.encode(hashPlusSalt));
192  }
193
194
195
196  /** {@inheritDoc} */
197  @Override
198  public ByteString encodePasswordWithScheme(ByteSequence plaintext)
199         throws DirectoryException
200  {
201    StringBuilder buffer = new StringBuilder();
202    buffer.append('{');
203    buffer.append(STORAGE_SCHEME_NAME_SALTED_MD5);
204    buffer.append('}');
205
206    int plainBytesLength = plaintext.length();
207    byte[] saltBytes     = new byte[NUM_SALT_BYTES];
208    byte[] plainPlusSalt = new byte[plainBytesLength + NUM_SALT_BYTES];
209
210    plaintext.copyTo(plainPlusSalt);
211
212    byte[] digestBytes;
213
214    synchronized (digestLock)
215    {
216      try
217      {
218        // Generate the salt and put in the plain+salt array.
219        random.nextBytes(saltBytes);
220        System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength,
221                         NUM_SALT_BYTES);
222
223        // Create the hash from the concatenated value.
224        digestBytes = messageDigest.digest(plainPlusSalt);
225      }
226      catch (Exception e)
227      {
228        logger.traceException(e);
229
230        LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
231            CLASS_NAME, getExceptionMessage(e));
232        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
233                                     message, e);
234      }
235      finally
236      {
237        Arrays.fill(plainPlusSalt, (byte) 0);
238      }
239    }
240
241    // Append the salt to the hashed value and base64-the whole thing.
242    byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES];
243
244    System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length);
245    System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length,
246                     NUM_SALT_BYTES);
247    buffer.append(Base64.encode(hashPlusSalt));
248
249    return ByteString.valueOfUtf8(buffer);
250  }
251
252
253
254  /** {@inheritDoc} */
255  @Override
256  public boolean passwordMatches(ByteSequence plaintextPassword,
257                                 ByteSequence storedPassword)
258  {
259    // Base64-decode the stored value and take the last 8 bytes as the salt.
260    byte[] saltBytes = new byte[NUM_SALT_BYTES];
261    byte[] digestBytes = new byte[MD5_LENGTH];
262    int saltLength = 0;
263    try
264    {
265      byte[] decodedBytes = Base64.decode(storedPassword.toString());
266
267      saltLength = decodedBytes.length - MD5_LENGTH;
268      if (saltLength <= 0)
269      {
270        logger.error(ERR_PWSCHEME_INVALID_BASE64_DECODED_STORED_PASSWORD, storedPassword);
271        return false;
272      }
273      saltBytes = new byte[saltLength];
274      System.arraycopy(decodedBytes, 0, digestBytes, 0, MD5_LENGTH);
275      System.arraycopy(decodedBytes, MD5_LENGTH, saltBytes, 0,
276                       saltLength);
277    }
278    catch (Exception e)
279    {
280      logger.traceException(e);
281      logger.error(ERR_PWSCHEME_CANNOT_BASE64_DECODE_STORED_PASSWORD, storedPassword, e);
282      return false;
283    }
284
285
286    // Use the salt to generate a digest based on the provided plain-text value.
287    int plainBytesLength = plaintextPassword.length();
288    byte[] plainPlusSalt = new byte[plainBytesLength + saltLength];
289    plaintextPassword.copyTo(plainPlusSalt);
290    System.arraycopy(saltBytes, 0, plainPlusSalt, plainBytesLength,
291                     saltLength);
292
293    byte[] userDigestBytes;
294
295    synchronized (digestLock)
296    {
297      try
298      {
299        userDigestBytes = messageDigest.digest(plainPlusSalt);
300      }
301      catch (Exception e)
302      {
303        logger.traceException(e);
304
305        return false;
306      }
307      finally
308      {
309        Arrays.fill(plainPlusSalt, (byte) 0);
310      }
311    }
312
313    return Arrays.equals(digestBytes, userDigestBytes);
314  }
315
316
317
318  /** {@inheritDoc} */
319  @Override
320  public boolean supportsAuthPasswordSyntax()
321  {
322    // This storage scheme does support the authentication password syntax.
323    return true;
324  }
325
326
327
328  /** {@inheritDoc} */
329  @Override
330  public String getAuthPasswordSchemeName()
331  {
332    return AUTH_PASSWORD_SCHEME_NAME_SALTED_MD5;
333  }
334
335
336
337  /** {@inheritDoc} */
338  @Override
339  public ByteString encodeAuthPassword(ByteSequence plaintext)
340         throws DirectoryException
341  {
342    int plaintextLength = plaintext.length();
343    byte[] saltBytes     = new byte[NUM_SALT_BYTES];
344    byte[] plainPlusSalt = new byte[plaintextLength + NUM_SALT_BYTES];
345
346    plaintext.copyTo(plainPlusSalt);
347
348    byte[] digestBytes;
349
350    synchronized (digestLock)
351    {
352      try
353      {
354        // Generate the salt and put in the plain+salt array.
355        random.nextBytes(saltBytes);
356        System.arraycopy(saltBytes,0, plainPlusSalt, plaintextLength,
357                         NUM_SALT_BYTES);
358
359        // Create the hash from the concatenated value.
360        digestBytes = messageDigest.digest(plainPlusSalt);
361      }
362      catch (Exception e)
363      {
364        logger.traceException(e);
365
366        LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
367            CLASS_NAME, getExceptionMessage(e));
368        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
369                                     message, e);
370      }
371      finally
372      {
373        Arrays.fill(plainPlusSalt, (byte) 0);
374      }
375    }
376
377
378    // Encode and return the value.
379    StringBuilder authPWValue = new StringBuilder();
380    authPWValue.append(AUTH_PASSWORD_SCHEME_NAME_SALTED_MD5);
381    authPWValue.append('$');
382    authPWValue.append(Base64.encode(saltBytes));
383    authPWValue.append('$');
384    authPWValue.append(Base64.encode(digestBytes));
385
386    return ByteString.valueOfUtf8(authPWValue);
387  }
388
389
390
391  /** {@inheritDoc} */
392  @Override
393  public boolean authPasswordMatches(ByteSequence plaintextPassword,
394                                     String authInfo, String authValue)
395  {
396    byte[] saltBytes;
397    byte[] digestBytes;
398    try
399    {
400      saltBytes   = Base64.decode(authInfo);
401      digestBytes = Base64.decode(authValue);
402    }
403    catch (Exception e)
404    {
405      logger.traceException(e);
406
407      return false;
408    }
409
410
411    int plainBytesLength = plaintextPassword.length();
412    byte[] plainPlusSaltBytes = new byte[plainBytesLength + saltBytes.length];
413    plaintextPassword.copyTo(plainPlusSaltBytes);
414    System.arraycopy(saltBytes, 0, plainPlusSaltBytes, plainBytesLength,
415                     saltBytes.length);
416
417    synchronized (digestLock)
418    {
419      try
420      {
421        return Arrays.equals(digestBytes,
422                                messageDigest.digest(plainPlusSaltBytes));
423      }
424      finally
425      {
426        Arrays.fill(plainPlusSaltBytes, (byte) 0);
427      }
428    }
429  }
430
431
432
433  /** {@inheritDoc} */
434  @Override
435  public boolean isReversible()
436  {
437    return false;
438  }
439
440
441
442  /** {@inheritDoc} */
443  @Override
444  public ByteString getPlaintextValue(ByteSequence storedPassword)
445         throws DirectoryException
446  {
447    LocalizableMessage message =
448        ERR_PWSCHEME_NOT_REVERSIBLE.get(STORAGE_SCHEME_NAME_SALTED_MD5);
449    throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
450  }
451
452
453
454  /** {@inheritDoc} */
455  @Override
456  public ByteString getAuthPasswordPlaintextValue(String authInfo,
457                                                  String authValue)
458         throws DirectoryException
459  {
460    LocalizableMessage message =
461        ERR_PWSCHEME_NOT_REVERSIBLE.get(AUTH_PASSWORD_SCHEME_NAME_SALTED_MD5);
462    throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
463  }
464
465
466
467  /** {@inheritDoc} */
468  @Override
469  public boolean isStorageSchemeSecure()
470  {
471    // MD5 may be considered reasonably secure for this purpose.
472    return true;
473  }
474}
475