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
010     * trunk/opends/resource/legal-notices/OpenDS.LICENSE
011     * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
012     * See the License for the specific language governing permissions
013     * and limitations under the License.
014     *
015     * When distributing Covered Code, include this CDDL HEADER in each
016     * file and include the License file at
017     * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
018     * add the following below this CDDL HEADER, with the fields enclosed
019     * by brackets "[]" replaced with your own identifying information:
020     *      Portions Copyright [yyyy] [name of copyright owner]
021     *
022     * CDDL HEADER END
023     *
024     *
025     *      Copyright 2006-2008 Sun Microsystems, Inc.
026     */
027    package org.opends.server.extensions;
028    
029    
030    
031    import java.security.MessageDigest;
032    import java.util.Arrays;
033    import java.util.Random;
034    
035    import org.opends.messages.Message;
036    import org.opends.server.admin.std.server.SaltedMD5PasswordStorageSchemeCfg;
037    import org.opends.server.api.PasswordStorageScheme;
038    import org.opends.server.config.ConfigException;
039    import org.opends.server.core.DirectoryServer;
040    import org.opends.server.loggers.ErrorLogger;
041    import org.opends.server.loggers.debug.DebugTracer;
042    import org.opends.server.types.ByteString;
043    import org.opends.server.types.ByteStringFactory;
044    import org.opends.server.types.DebugLogLevel;
045    import org.opends.server.types.DirectoryException;
046    import org.opends.server.types.InitializationException;
047    import org.opends.server.types.ResultCode;
048    import org.opends.server.util.Base64;
049    
050    import static org.opends.messages.ExtensionMessages.*;
051    import static org.opends.server.extensions.ExtensionsConstants.*;
052    import static org.opends.server.loggers.debug.DebugLogger.*;
053    import static org.opends.server.util.StaticUtils.*;
054    
055    
056    
057    /**
058     * This class defines a Directory Server password storage scheme based on the
059     * MD5 algorithm defined in RFC 1321.  This is a one-way digest algorithm so
060     * there is no way to retrieve the original clear-text version of the
061     * password from the hashed value (although this means that it is not suitable
062     * for things that need the clear-text password like DIGEST-MD5).  The values
063     * that it generates are also salted, which protects against dictionary attacks.
064     * It does this by generating a 64-bit random salt which is appended to the
065     * clear-text value.  A MD5 hash is then generated based on this, the salt is
066     * appended to the hash, and then the entire value is base64-encoded.
067     */
068    public class SaltedMD5PasswordStorageScheme
069           extends PasswordStorageScheme<SaltedMD5PasswordStorageSchemeCfg>
070    {
071      /**
072       * The tracer object for the debug logger.
073       */
074      private static final DebugTracer TRACER = getTracer();
075    
076      /**
077       * The fully-qualified name of this class.
078       */
079      private static final String CLASS_NAME =
080           "org.opends.server.extensions.SaltedMD5PasswordStorageScheme";
081    
082    
083    
084      /**
085       * The number of bytes of random data to use as the salt when generating the
086       * hashes.
087       */
088      private static final int NUM_SALT_BYTES = 8;
089    
090    
091    
092      // The message digest that will actually be used to generate the MD5 hashes.
093      private MessageDigest messageDigest;
094    
095      // The lock used to provide threadsafe access to the message digest.
096      private Object digestLock;
097    
098      // The secure random number generator to use to generate the salt values.
099      private Random random;
100    
101    
102    
103      /**
104       * Creates a new instance of this password storage scheme.  Note that no
105       * initialization should be performed here, as all initialization should be
106       * done in the <CODE>initializePasswordStorageScheme</CODE> method.
107       */
108      public SaltedMD5PasswordStorageScheme()
109      {
110        super();
111    
112      }
113    
114    
115    
116      /**
117       * {@inheritDoc}
118       */
119      @Override()
120      public void initializePasswordStorageScheme(
121                       SaltedMD5PasswordStorageSchemeCfg configuration)
122             throws ConfigException, InitializationException
123      {
124        try
125        {
126          messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_MD5);
127        }
128        catch (Exception e)
129        {
130          if (debugEnabled())
131          {
132            TRACER.debugCaught(DebugLogLevel.ERROR, e);
133          }
134    
135          Message message = ERR_PWSCHEME_CANNOT_INITIALIZE_MESSAGE_DIGEST.get(
136              MESSAGE_DIGEST_ALGORITHM_MD5, String.valueOf(e));
137          throw new InitializationException(message, e);
138        }
139    
140    
141        digestLock = new Object();
142        random     = new Random();
143      }
144    
145    
146    
147      /**
148       * {@inheritDoc}
149       */
150      @Override()
151      public String getStorageSchemeName()
152      {
153        return STORAGE_SCHEME_NAME_SALTED_MD5;
154      }
155    
156    
157    
158      /**
159       * {@inheritDoc}
160       */
161      @Override()
162      public ByteString encodePassword(ByteString plaintext)
163             throws DirectoryException
164      {
165        byte[] plainBytes    = plaintext.value();
166        byte[] saltBytes     = new byte[NUM_SALT_BYTES];
167        byte[] plainPlusSalt = new byte[plainBytes.length + NUM_SALT_BYTES];
168    
169        System.arraycopy(plainBytes, 0, plainPlusSalt, 0, plainBytes.length);
170    
171        byte[] digestBytes;
172    
173        synchronized (digestLock)
174        {
175          try
176          {
177            // Generate the salt and put in the plain+salt array.
178            random.nextBytes(saltBytes);
179            System.arraycopy(saltBytes,0, plainPlusSalt, plainBytes.length,
180                             NUM_SALT_BYTES);
181    
182            // Create the hash from the concatenated value.
183            digestBytes = messageDigest.digest(plainPlusSalt);
184          }
185          catch (Exception e)
186          {
187            if (debugEnabled())
188            {
189              TRACER.debugCaught(DebugLogLevel.ERROR, e);
190            }
191    
192            Message message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
193                CLASS_NAME, getExceptionMessage(e));
194            throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
195                                         message, e);
196          }
197        }
198    
199        // Append the salt to the hashed value and base64-the whole thing.
200        byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES];
201    
202        System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length);
203        System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length,
204                         NUM_SALT_BYTES);
205    
206        return ByteStringFactory.create(Base64.encode(hashPlusSalt));
207      }
208    
209    
210    
211      /**
212       * {@inheritDoc}
213       */
214      @Override()
215      public ByteString encodePasswordWithScheme(ByteString plaintext)
216             throws DirectoryException
217      {
218        StringBuilder buffer = new StringBuilder();
219        buffer.append('{');
220        buffer.append(STORAGE_SCHEME_NAME_SALTED_MD5);
221        buffer.append('}');
222    
223        byte[] plainBytes    = plaintext.value();
224        byte[] saltBytes     = new byte[NUM_SALT_BYTES];
225        byte[] plainPlusSalt = new byte[plainBytes.length + NUM_SALT_BYTES];
226    
227        System.arraycopy(plainBytes, 0, plainPlusSalt, 0, plainBytes.length);
228    
229        byte[] digestBytes;
230    
231        synchronized (digestLock)
232        {
233          try
234          {
235            // Generate the salt and put in the plain+salt array.
236            random.nextBytes(saltBytes);
237            System.arraycopy(saltBytes,0, plainPlusSalt, plainBytes.length,
238                             NUM_SALT_BYTES);
239    
240            // Create the hash from the concatenated value.
241            digestBytes = messageDigest.digest(plainPlusSalt);
242          }
243          catch (Exception e)
244          {
245            if (debugEnabled())
246            {
247              TRACER.debugCaught(DebugLogLevel.ERROR, e);
248            }
249    
250            Message message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
251                CLASS_NAME, getExceptionMessage(e));
252            throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
253                                         message, e);
254          }
255        }
256    
257        // Append the salt to the hashed value and base64-the whole thing.
258        byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES];
259    
260        System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length);
261        System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length,
262                         NUM_SALT_BYTES);
263        buffer.append(Base64.encode(hashPlusSalt));
264    
265        return ByteStringFactory.create(buffer.toString());
266      }
267    
268    
269    
270      /**
271       * {@inheritDoc}
272       */
273      @Override()
274      public boolean passwordMatches(ByteString plaintextPassword,
275                                     ByteString storedPassword)
276      {
277        // Base64-decode the stored value and take the last 8 bytes as the salt.
278        byte[] saltBytes = new byte[NUM_SALT_BYTES];
279        byte[] digestBytes;
280        try
281        {
282          byte[] decodedBytes = Base64.decode(storedPassword.stringValue());
283    
284          int digestLength = decodedBytes.length - NUM_SALT_BYTES;
285          digestBytes = new byte[digestLength];
286          System.arraycopy(decodedBytes, 0, digestBytes, 0, digestLength);
287          System.arraycopy(decodedBytes, digestLength, saltBytes, 0,
288                           NUM_SALT_BYTES);
289        }
290        catch (Exception e)
291        {
292          if (debugEnabled())
293          {
294            TRACER.debugCaught(DebugLogLevel.ERROR, e);
295          }
296    
297          Message message = ERR_PWSCHEME_CANNOT_BASE64_DECODE_STORED_PASSWORD.get(
298              storedPassword.stringValue(), String.valueOf(e));
299          ErrorLogger.logError(message);
300          return false;
301        }
302    
303    
304        // Use the salt to generate a digest based on the provided plain-text value.
305        byte[] plainBytes    = plaintextPassword.value();
306        byte[] plainPlusSalt = new byte[plainBytes.length + NUM_SALT_BYTES];
307        System.arraycopy(plainBytes, 0, plainPlusSalt, 0, plainBytes.length);
308        System.arraycopy(saltBytes, 0,plainPlusSalt, plainBytes.length,
309                         NUM_SALT_BYTES);
310    
311        byte[] userDigestBytes;
312    
313        synchronized (digestLock)
314        {
315          try
316          {
317            userDigestBytes = messageDigest.digest(plainPlusSalt);
318          }
319          catch (Exception e)
320          {
321            if (debugEnabled())
322            {
323              TRACER.debugCaught(DebugLogLevel.ERROR, e);
324            }
325    
326            return false;
327          }
328        }
329    
330        return Arrays.equals(digestBytes, userDigestBytes);
331      }
332    
333    
334    
335      /**
336       * {@inheritDoc}
337       */
338      @Override()
339      public boolean supportsAuthPasswordSyntax()
340      {
341        // This storage scheme does support the authentication password syntax.
342        return true;
343      }
344    
345    
346    
347      /**
348       * {@inheritDoc}
349       */
350      @Override()
351      public String getAuthPasswordSchemeName()
352      {
353        return AUTH_PASSWORD_SCHEME_NAME_SALTED_MD5;
354      }
355    
356    
357    
358      /**
359       * {@inheritDoc}
360       */
361      @Override()
362      public ByteString encodeAuthPassword(ByteString plaintext)
363             throws DirectoryException
364      {
365        byte[] plainBytes    = plaintext.value();
366        byte[] saltBytes     = new byte[NUM_SALT_BYTES];
367        byte[] plainPlusSalt = new byte[plainBytes.length + NUM_SALT_BYTES];
368    
369        System.arraycopy(plainBytes, 0, plainPlusSalt, 0, plainBytes.length);
370    
371        byte[] digestBytes;
372    
373        synchronized (digestLock)
374        {
375          try
376          {
377            // Generate the salt and put in the plain+salt array.
378            random.nextBytes(saltBytes);
379            System.arraycopy(saltBytes,0, plainPlusSalt, plainBytes.length,
380                             NUM_SALT_BYTES);
381    
382            // Create the hash from the concatenated value.
383            digestBytes = messageDigest.digest(plainPlusSalt);
384          }
385          catch (Exception e)
386          {
387            if (debugEnabled())
388            {
389              TRACER.debugCaught(DebugLogLevel.ERROR, e);
390            }
391    
392            Message message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
393                CLASS_NAME, getExceptionMessage(e));
394            throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
395                                         message, e);
396          }
397        }
398    
399    
400        // Encode and return the value.
401        StringBuilder authPWValue = new StringBuilder();
402        authPWValue.append(AUTH_PASSWORD_SCHEME_NAME_SALTED_MD5);
403        authPWValue.append('$');
404        authPWValue.append(Base64.encode(saltBytes));
405        authPWValue.append('$');
406        authPWValue.append(Base64.encode(digestBytes));
407    
408        return ByteStringFactory.create(authPWValue.toString());
409      }
410    
411    
412    
413      /**
414       * {@inheritDoc}
415       */
416      @Override()
417      public boolean authPasswordMatches(ByteString plaintextPassword,
418                                         String authInfo, String authValue)
419      {
420        byte[] saltBytes;
421        byte[] digestBytes;
422        try
423        {
424          saltBytes   = Base64.decode(authInfo);
425          digestBytes = Base64.decode(authValue);
426        }
427        catch (Exception e)
428        {
429          if (debugEnabled())
430          {
431            TRACER.debugCaught(DebugLogLevel.ERROR, e);
432          }
433    
434          return false;
435        }
436    
437    
438        byte[] plainBytes = plaintextPassword.value();
439        byte[] plainPlusSaltBytes = new byte[plainBytes.length + saltBytes.length];
440        System.arraycopy(plainBytes, 0, plainPlusSaltBytes, 0, plainBytes.length);
441        System.arraycopy(saltBytes, 0, plainPlusSaltBytes, plainBytes.length,
442                         saltBytes.length);
443    
444        synchronized (digestLock)
445        {
446          return Arrays.equals(digestBytes,
447                                    messageDigest.digest(plainPlusSaltBytes));
448        }
449      }
450    
451    
452    
453      /**
454       * {@inheritDoc}
455       */
456      @Override()
457      public boolean isReversible()
458      {
459        return false;
460      }
461    
462    
463    
464      /**
465       * {@inheritDoc}
466       */
467      @Override()
468      public ByteString getPlaintextValue(ByteString storedPassword)
469             throws DirectoryException
470      {
471        Message message =
472            ERR_PWSCHEME_NOT_REVERSIBLE.get(STORAGE_SCHEME_NAME_SALTED_MD5);
473        throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
474      }
475    
476    
477    
478      /**
479       * {@inheritDoc}
480       */
481      @Override()
482      public ByteString getAuthPasswordPlaintextValue(String authInfo,
483                                                      String authValue)
484             throws DirectoryException
485      {
486        Message message =
487            ERR_PWSCHEME_NOT_REVERSIBLE.get(AUTH_PASSWORD_SCHEME_NAME_SALTED_MD5);
488        throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
489      }
490    
491    
492    
493      /**
494       * {@inheritDoc}
495       */
496      @Override()
497      public boolean isStorageSchemeSecure()
498      {
499        // MD5 may be considered reasonably secure for this purpose.
500        return true;
501      }
502    }
503