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