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