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