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