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.SaltedSHA512PasswordStorageSchemeCfg;
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     * 512-bit SHA-2 algorithm defined in FIPS 180-2.  This is a one-way digest
060     * algorithm so there is no way to retrieve the original clear-text version of
061     * the password from the hashed value (although this means that it is not
062     * suitable for things that need the clear-text password like DIGEST-MD5).  The
063     * values that it generates are also salted, which protects against dictionary
064     * attacks. It does this by generating a 64-bit random salt which is appended to
065     * the clear-text value.  A SHA-2 hash is then generated based on this, the salt
066     * is appended to the hash, and then the entire value is base64-encoded.
067     */
068    public class SaltedSHA512PasswordStorageScheme
069           extends PasswordStorageScheme<SaltedSHA512PasswordStorageSchemeCfg>
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.SaltedSHA512PasswordStorageScheme";
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 512-bit SHA-2
093      // hashes.
094      private MessageDigest messageDigest;
095    
096      // The lock used to provide threadsafe access to the message digest.
097      private Object digestLock;
098    
099      // The secure random number generator to use to generate the salt values.
100      private Random random;
101    
102    
103    
104      /**
105       * Creates a new instance of this password storage scheme.  Note that no
106       * initialization should be performed here, as all initialization should be
107       * done in the <CODE>initializePasswordStorageScheme</CODE> method.
108       */
109      public SaltedSHA512PasswordStorageScheme()
110      {
111        super();
112      }
113    
114    
115    
116      /**
117       * {@inheritDoc}
118       */
119      @Override()
120      public void initializePasswordStorageScheme(
121                       SaltedSHA512PasswordStorageSchemeCfg configuration)
122             throws ConfigException, InitializationException
123      {
124        try
125        {
126          messageDigest =
127               MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_512);
128        }
129        catch (Exception e)
130        {
131          if (debugEnabled())
132          {
133            TRACER.debugCaught(DebugLogLevel.ERROR, e);
134          }
135    
136          Message message = ERR_PWSCHEME_CANNOT_INITIALIZE_MESSAGE_DIGEST.get(
137              MESSAGE_DIGEST_ALGORITHM_SHA_512, String.valueOf(e));
138          throw new InitializationException(message, e);
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_SHA_512;
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_SHA_512);
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_SHA_512;
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_SHA_512);
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_SHA_512);
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 = ERR_PWSCHEME_NOT_REVERSIBLE.get(
487            AUTH_PASSWORD_SCHEME_NAME_SALTED_SHA_512);
488        throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
489      }
490    
491    
492    
493      /**
494       * {@inheritDoc}
495       */
496      @Override()
497      public boolean isStorageSchemeSecure()
498      {
499        // SHA-2 should be considered secure.
500        return true;
501      }
502    
503    
504    
505      /**
506       * Generates an encoded password string from the given clear-text password.
507       * This method is primarily intended for use when it is necessary to generate
508       * a password with the server offline (e.g., when setting the initial root
509       * user password).
510       *
511       * @param  passwordBytes  The bytes that make up the clear-text password.
512       *
513       * @return  The encoded password string, including the scheme name in curly
514       *          braces.
515       *
516       * @throws  DirectoryException  If a problem occurs during processing.
517       */
518      public static String encodeOffline(byte[] passwordBytes)
519             throws DirectoryException
520      {
521        byte[] saltBytes = new byte[NUM_SALT_BYTES];
522        new Random().nextBytes(saltBytes);
523    
524        byte[] passwordPlusSalt = new byte[passwordBytes.length + NUM_SALT_BYTES];
525        System.arraycopy(passwordBytes, 0, passwordPlusSalt, 0,
526                         passwordBytes.length);
527        System.arraycopy(saltBytes, 0, passwordPlusSalt, passwordBytes.length,
528                         NUM_SALT_BYTES);
529    
530        MessageDigest messageDigest;
531        try
532        {
533          messageDigest =
534               MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_512);
535        }
536        catch (Exception e)
537        {
538          Message message = ERR_PWSCHEME_CANNOT_INITIALIZE_MESSAGE_DIGEST.get(
539              MESSAGE_DIGEST_ALGORITHM_SHA_512, String.valueOf(e));
540          throw new DirectoryException(ResultCode.OTHER, message, e);
541        }
542    
543    
544        byte[] digestBytes    = messageDigest.digest(passwordPlusSalt);
545        byte[] digestPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES];
546        System.arraycopy(digestBytes, 0, digestPlusSalt, 0, digestBytes.length);
547        System.arraycopy(saltBytes, 0, digestPlusSalt, digestBytes.length,
548                         NUM_SALT_BYTES);
549    
550        return "{" + STORAGE_SCHEME_NAME_SALTED_SHA_512 + "}" +
551               Base64.encode(digestPlusSalt);
552      }
553    }
554