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 legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2008 Sun Microsystems, Inc.
025 *      Portions Copyright 2010-2015 ForgeRock AS
026 *      Portions Copyright 2012 Dariusz Janny <dariusz.janny@gmail.com>
027 */
028package org.opends.server.extensions;
029
030import java.util.Arrays;
031import java.util.List;
032import java.util.Random;
033
034import org.forgerock.i18n.LocalizableMessage;
035import org.opends.server.admin.server.ConfigurationChangeListener;
036import org.opends.server.admin.std.server.CryptPasswordStorageSchemeCfg;
037import org.opends.server.admin.std.server.PasswordStorageSchemeCfg;
038import org.opends.server.api.PasswordStorageScheme;
039import org.forgerock.opendj.config.server.ConfigChangeResult;
040import org.forgerock.opendj.config.server.ConfigException;
041import org.opends.server.core.DirectoryServer;
042import org.opends.server.types.*;
043import org.forgerock.opendj.ldap.ResultCode;
044import org.forgerock.opendj.ldap.ByteString;
045import org.forgerock.opendj.ldap.ByteSequence;
046import org.opends.server.util.BSDMD5Crypt;
047import org.opends.server.util.Crypt;
048
049import static org.opends.messages.ExtensionMessages.*;
050import static org.opends.server.extensions.ExtensionsConstants.*;
051import static org.opends.server.util.StaticUtils.*;
052
053/**
054 * This class defines a Directory Server password storage scheme based on the
055 * UNIX Crypt algorithm.  This is a legacy one-way digest algorithm
056 * intended only for situations where passwords have not yet been
057 * updated to modern hashes such as SHA-1 and friends.  This
058 * implementation does perform weak salting, which means that it is more
059 * vulnerable to dictionary attacks than schemes with larger salts.
060 */
061public class CryptPasswordStorageScheme
062       extends PasswordStorageScheme<CryptPasswordStorageSchemeCfg>
063       implements ConfigurationChangeListener<CryptPasswordStorageSchemeCfg>
064{
065
066  /**
067   * The fully-qualified name of this class for debugging purposes.
068   */
069  private static final String CLASS_NAME =
070       "org.opends.server.extensions.CryptPasswordStorageScheme";
071
072  /**
073   * The current configuration for the CryptPasswordStorageScheme.
074   */
075  private CryptPasswordStorageSchemeCfg currentConfig;
076
077  /**
078   * An array of values that can be used to create salt characters
079   * when encoding new crypt hashes.
080   */
081  private static final byte[] SALT_CHARS =
082    ("./0123456789abcdefghijklmnopqrstuvwxyz"
083    +"ABCDEFGHIJKLMNOPQRSTUVWXYZ").getBytes();
084
085  private final Random randomSaltIndex = new Random();
086  private final Object saltLock = new Object();
087  private final Crypt crypt = new Crypt();
088
089
090  /**
091   * Creates a new instance of this password storage scheme.  Note that no
092   * initialization should be performed here, as all initialization should be
093   * done in the <CODE>initializePasswordStorageScheme</CODE> method.
094   */
095  public CryptPasswordStorageScheme()
096  {
097    super();
098  }
099
100
101  /** {@inheritDoc} */
102  @Override
103  public void initializePasswordStorageScheme(
104                   CryptPasswordStorageSchemeCfg configuration)
105         throws ConfigException, InitializationException {
106
107    configuration.addCryptChangeListener(this);
108
109    currentConfig = configuration;
110  }
111
112  /** {@inheritDoc} */
113  @Override
114  public String getStorageSchemeName()
115  {
116    return STORAGE_SCHEME_NAME_CRYPT;
117  }
118
119
120  /**
121   * Encrypt plaintext password with the Unix Crypt algorithm.
122   */
123  private ByteString unixCryptEncodePassword(ByteSequence plaintext)
124         throws DirectoryException
125  {
126    byte[] plaintextBytes = null;
127    byte[] digestBytes;
128
129    try
130    {
131      // TODO: can we avoid this copy?
132      plaintextBytes = plaintext.toByteArray();
133      digestBytes = crypt.crypt(plaintextBytes, randomSalt());
134    }
135    catch (Exception e)
136    {
137      LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
138          CLASS_NAME, stackTraceToSingleLineString(e));
139      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
140                                   message, e);
141    }
142    finally
143    {
144      if (plaintextBytes != null)
145      {
146        Arrays.fill(plaintextBytes, (byte) 0);
147      }
148    }
149
150    return ByteString.wrap(digestBytes);
151  }
152
153  /**
154   * Return a random 2-byte salt.
155   *
156   * @return a random 2-byte salt
157   */
158  private byte[] randomSalt() {
159    synchronized (saltLock)
160    {
161      int sb1 = randomSaltIndex.nextInt(SALT_CHARS.length);
162      int sb2 = randomSaltIndex.nextInt(SALT_CHARS.length);
163
164      return new byte[] {
165        SALT_CHARS[sb1],
166        SALT_CHARS[sb2],
167      };
168    }
169  }
170
171  private ByteString md5CryptEncodePassword(ByteSequence plaintext)
172         throws DirectoryException
173  {
174    String output;
175    try
176    {
177      output = BSDMD5Crypt.crypt(plaintext);
178    }
179    catch (Exception e)
180    {
181      LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
182          CLASS_NAME, stackTraceToSingleLineString(e));
183      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
184                                   message, e);
185    }
186    return ByteString.valueOfUtf8(output);
187  }
188
189  private ByteString sha256CryptEncodePassword(ByteSequence plaintext)
190      throws DirectoryException {
191    String output;
192    byte[] plaintextBytes = null;
193
194    try
195    {
196      plaintextBytes = plaintext.toByteArray();
197      output = Sha2Crypt.sha256Crypt(plaintextBytes);
198    }
199    catch (Exception e)
200    {
201      LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
202          CLASS_NAME, stackTraceToSingleLineString(e));
203      throw new DirectoryException(
204          DirectoryServer.getServerErrorResultCode(), message, e);
205    }
206    finally
207    {
208      if (plaintextBytes != null)
209      {
210        Arrays.fill(plaintextBytes, (byte) 0);
211      }
212    }
213    return ByteString.valueOfUtf8(output);
214  }
215
216  private ByteString sha512CryptEncodePassword(ByteSequence plaintext)
217      throws DirectoryException {
218    String output;
219    byte[] plaintextBytes = null;
220
221    try
222    {
223      plaintextBytes = plaintext.toByteArray();
224      output = Sha2Crypt.sha512Crypt(plaintextBytes);
225    }
226    catch (Exception e)
227    {
228      LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
229          CLASS_NAME, stackTraceToSingleLineString(e));
230      throw new DirectoryException(
231          DirectoryServer.getServerErrorResultCode(), message, e);
232    }
233    finally
234    {
235      if (plaintextBytes != null)
236      {
237        Arrays.fill(plaintextBytes, (byte) 0);
238      }
239    }
240    return ByteString.valueOfUtf8(output);
241  }
242
243  /** {@inheritDoc} */
244  @Override
245  public ByteString encodePassword(ByteSequence plaintext)
246         throws DirectoryException
247  {
248    ByteString bytes = null;
249    switch (currentConfig.getCryptPasswordStorageEncryptionAlgorithm())
250    {
251      case UNIX:
252        bytes = unixCryptEncodePassword(plaintext);
253        break;
254      case MD5:
255        bytes = md5CryptEncodePassword(plaintext);
256        break;
257      case SHA256:
258        bytes = sha256CryptEncodePassword(plaintext);
259        break;
260      case SHA512:
261        bytes = sha512CryptEncodePassword(plaintext);
262        break;
263    }
264    return bytes;
265  }
266
267
268  /** {@inheritDoc} */
269  @Override
270  public ByteString encodePasswordWithScheme(ByteSequence plaintext)
271         throws DirectoryException
272  {
273    StringBuilder buffer =
274      new StringBuilder(STORAGE_SCHEME_NAME_CRYPT.length()+12);
275    buffer.append('{');
276    buffer.append(STORAGE_SCHEME_NAME_CRYPT);
277    buffer.append('}');
278
279    buffer.append(encodePassword(plaintext));
280
281    return ByteString.valueOfUtf8(buffer);
282  }
283
284  /**
285   * Matches passwords encrypted with the Unix Crypt algorithm.
286   */
287  private boolean unixCryptPasswordMatches(ByteSequence plaintextPassword,
288                                 ByteSequence storedPassword)
289  {
290    // TODO: Can we avoid this copy?
291    byte[] plaintextPasswordBytes = null;
292
293    ByteString userPWDigestBytes;
294    try
295    {
296      plaintextPasswordBytes = plaintextPassword.toByteArray();
297      // The salt is stored as the first two bytes of the storedPassword
298      // value, and crypt.crypt() only looks at the first two bytes, so
299      // we can pass it in directly.
300      byte[] salt = storedPassword.copyTo(new byte[2]);
301      userPWDigestBytes =
302          ByteString.wrap(crypt.crypt(plaintextPasswordBytes, salt));
303    }
304    catch (Exception e)
305    {
306      return false;
307    }
308    finally
309    {
310      if (plaintextPasswordBytes != null)
311      {
312        Arrays.fill(plaintextPasswordBytes, (byte) 0);
313      }
314    }
315
316    return userPWDigestBytes.equals(storedPassword);
317  }
318
319  private boolean md5CryptPasswordMatches(ByteSequence plaintextPassword,
320                                 ByteSequence storedPassword)
321  {
322    String storedString = storedPassword.toString();
323    try
324    {
325      String userString   = BSDMD5Crypt.crypt(plaintextPassword,
326        storedString);
327      return userString.equals(storedString);
328    }
329    catch (Exception e)
330    {
331      return false;
332    }
333  }
334
335  private boolean sha256CryptPasswordMatches(ByteSequence plaintextPassword,
336      ByteSequence storedPassword) {
337    byte[] plaintextPasswordBytes = null;
338    String storedString = storedPassword.toString();
339    try
340    {
341      plaintextPasswordBytes = plaintextPassword.toByteArray();
342      String userString = Sha2Crypt.sha256Crypt(
343          plaintextPasswordBytes, storedString);
344      return userString.equals(storedString);
345    }
346    catch (Exception e)
347    {
348      return false;
349    }
350    finally
351    {
352      if (plaintextPasswordBytes != null)
353      {
354        Arrays.fill(plaintextPasswordBytes, (byte) 0);
355      }
356    }
357  }
358
359  private boolean sha512CryptPasswordMatches(ByteSequence plaintextPassword,
360      ByteSequence storedPassword) {
361    byte[] plaintextPasswordBytes = null;
362    String storedString = storedPassword.toString();
363    try
364    {
365      plaintextPasswordBytes = plaintextPassword.toByteArray();
366      String userString = Sha2Crypt.sha512Crypt(
367          plaintextPasswordBytes, storedString);
368      return userString.equals(storedString);
369    }
370    catch (Exception e)
371    {
372      return false;
373    }
374    finally
375    {
376      if (plaintextPasswordBytes != null)
377      {
378        Arrays.fill(plaintextPasswordBytes, (byte) 0);
379      }
380    }
381  }
382
383  /** {@inheritDoc} */
384  @Override
385  public boolean passwordMatches(ByteSequence plaintextPassword,
386                                 ByteSequence storedPassword)
387  {
388    String storedString = storedPassword.toString();
389    if (storedString.startsWith(BSDMD5Crypt.getMagicString()))
390    {
391      return md5CryptPasswordMatches(plaintextPassword, storedPassword);
392    }
393    else if (storedString.startsWith(Sha2Crypt.getMagicSHA256Prefix()))
394    {
395      return sha256CryptPasswordMatches(plaintextPassword, storedPassword);
396    }
397    else if (storedString.startsWith(Sha2Crypt.getMagicSHA512Prefix()))
398    {
399      return sha512CryptPasswordMatches(plaintextPassword, storedPassword);
400    }
401    else
402    {
403      return unixCryptPasswordMatches(plaintextPassword, storedPassword);
404    }
405  }
406
407  /** {@inheritDoc} */
408  @Override
409  public boolean supportsAuthPasswordSyntax()
410  {
411    // This storage scheme does not support the authentication password syntax.
412    return false;
413  }
414
415
416
417  /** {@inheritDoc} */
418  @Override
419  public ByteString encodeAuthPassword(ByteSequence plaintext)
420         throws DirectoryException
421  {
422    LocalizableMessage message =
423        ERR_PWSCHEME_DOES_NOT_SUPPORT_AUTH_PASSWORD.get(getStorageSchemeName());
424    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
425  }
426
427
428
429  /** {@inheritDoc} */
430  @Override
431  public boolean authPasswordMatches(ByteSequence plaintextPassword,
432                                     String authInfo, String authValue)
433  {
434    // This storage scheme does not support the authentication password syntax.
435    return false;
436  }
437
438
439
440  /** {@inheritDoc} */
441  @Override
442  public boolean isReversible()
443  {
444    return false;
445  }
446
447
448
449  /** {@inheritDoc} */
450  @Override
451  public ByteString getPlaintextValue(ByteSequence storedPassword)
452         throws DirectoryException
453  {
454    LocalizableMessage message =
455        ERR_PWSCHEME_NOT_REVERSIBLE.get(STORAGE_SCHEME_NAME_CRYPT);
456    throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
457  }
458
459
460
461  /** {@inheritDoc} */
462  @Override
463  public ByteString getAuthPasswordPlaintextValue(String authInfo,
464                                                  String authValue)
465         throws DirectoryException
466  {
467    LocalizableMessage message =
468      ERR_PWSCHEME_DOES_NOT_SUPPORT_AUTH_PASSWORD.get(getStorageSchemeName());
469    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
470  }
471
472
473
474  /** {@inheritDoc} */
475  @Override
476  public boolean isStorageSchemeSecure()
477  {
478    // FIXME:
479    // Technically, this isn't quite in keeping with the original spirit of
480    // this method, since the point was to determine whether the scheme could
481    // be trivially reversed.  I'm not sure I would put crypt into that
482    // category, but it's certainly a lot more vulnerable to lookup tables
483    // than most other algorithms.  I'd say we can keep it this way for now,
484    // but it might be something to reconsider later.
485    //
486    // Currently, this method is unused.  However, the intended purpose is
487    // eventually for use in issue #321, where we could do things like prevent
488    // even authorized users from seeing the password value over an insecure
489    // connection if it isn't considered secure.
490
491    return false;
492  }
493
494  /** {@inheritDoc} */
495  @Override
496  public boolean isConfigurationAcceptable(
497          PasswordStorageSchemeCfg configuration,
498          List<LocalizableMessage> unacceptableReasons)
499  {
500    CryptPasswordStorageSchemeCfg config =
501            (CryptPasswordStorageSchemeCfg) configuration;
502    return isConfigurationChangeAcceptable(config, unacceptableReasons);
503  }
504
505
506
507  /** {@inheritDoc} */
508  @Override
509  public boolean isConfigurationChangeAcceptable(
510                      CryptPasswordStorageSchemeCfg configuration,
511                      List<LocalizableMessage> unacceptableReasons)
512  {
513    // If we've gotten this far, then we'll accept the change.
514    return true;
515  }
516
517  /** {@inheritDoc} */
518  @Override
519  public ConfigChangeResult applyConfigurationChange(
520                      CryptPasswordStorageSchemeCfg configuration)
521  {
522    currentConfig = configuration;
523    return new ConfigChangeResult();
524  }
525}