001/*
002 * Copyright 2017-2020 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2017-2020 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2017-2020 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.listener;
037
038
039
040import java.security.MessageDigest;
041import java.security.SecureRandom;
042import java.util.Arrays;
043import java.util.List;
044
045import com.unboundid.ldap.sdk.LDAPException;
046import com.unboundid.ldap.sdk.Modification;
047import com.unboundid.ldap.sdk.ReadOnlyEntry;
048import com.unboundid.ldap.sdk.ResultCode;
049import com.unboundid.util.ThreadSafety;
050import com.unboundid.util.ThreadSafetyLevel;
051import com.unboundid.util.Validator;
052
053import static com.unboundid.ldap.listener.ListenerMessages.*;
054
055
056
057/**
058 * This class provides an implementation of an in-memory directory server
059 * password encoder that uses a message digest to encode passwords.  Encoded
060 * passwords will also include some number of randomly generated bytes, called a
061 * salt, to ensure that encoding the same password multiple times will yield
062 * multiple different encoded representations.
063 */
064@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
065public final class SaltedMessageDigestInMemoryPasswordEncoder
066       extends InMemoryPasswordEncoder
067{
068  // Indicates whether the salt should go after or before the clear-text
069  // password when generating the message digest.
070  private final boolean saltAfterClearPassword;
071
072  // Indicates whether the salt should go after or before the digest bytes
073  // when generating the final encoded representation.
074  private final boolean saltAfterMessageDigest;
075
076  // The length of the generated message digest, in bytes.
077  private final int digestLengthBytes;
078
079  // The number of salt bytes to generate.
080  private final int numSaltBytes;
081
082  // The message digest instance tha will be used to actually perform the
083  // encoding.
084  private final MessageDigest messageDigest;
085
086  // The secure random number generator used for generating salts.
087  private final SecureRandom random;
088
089
090
091  /**
092   * Creates a new instance of this in-memory directory server password encoder
093   * with the provided information.
094   *
095   * @param  prefix                  The string that will appear at the
096   *                                 beginning of encoded passwords.  It must
097   *                                 not be {@code null} or empty.
098   * @param  outputFormatter         The output formatter that will be used to
099   *                                 format the encoded representation of
100   *                                 clear-text passwords.  It may be
101   *                                 {@code null} if no special formatting
102   *                                 should be applied to the raw bytes.
103   * @param  messageDigest           The message digest that will be used to
104   *                                 actually perform the encoding.  It must not
105   *                                 be {@code null}.
106   * @param  numSaltBytes            The number of salt bytes to generate when
107   *                                 encoding passwords.  It must be greater
108   *                                 than zero.
109   * @param  saltAfterClearPassword  Indicates whether the salt should be placed
110   *                                 after or before the clear-text password
111   *                                 when computing the message digest.  If this
112   *                                 is {@code true}, then the digest will be
113   *                                 computed from the concatenation of the
114   *                                 clear-text password and the salt, in that
115   *                                 order.  If this is {@code false}, then the
116   *                                 digest will be computed from the
117   *                                 concatenation of the salt and the
118   *                                 clear-text password.
119   * @param  saltAfterMessageDigest  Indicates whether the salt should be placed
120   *                                 after or before the computed digest when
121   *                                 creating the encoded representation.  If
122   *                                 this is {@code true}, then the encoded
123   *                                 password will consist of the concatenation
124   *                                 of the computed message digest and the
125   *                                 salt, in that order.  If this is
126   *                                 {@code false}, then the encoded password
127   *                                 will consist of the concatenation of the
128   *                                 salt and the message digest.
129   */
130  public SaltedMessageDigestInMemoryPasswordEncoder(final String prefix,
131              final PasswordEncoderOutputFormatter outputFormatter,
132              final MessageDigest messageDigest, final int numSaltBytes,
133              final boolean saltAfterClearPassword,
134              final boolean saltAfterMessageDigest)
135  {
136    super(prefix, outputFormatter);
137
138    Validator.ensureNotNull(messageDigest);
139    this.messageDigest = messageDigest;
140
141    digestLengthBytes = messageDigest.getDigestLength();
142    Validator.ensureTrue((digestLengthBytes > 0),
143         "The message digest use a fixed digest length, and that " +
144              "length must be greater than zero.");
145
146    this.numSaltBytes = numSaltBytes;
147    Validator.ensureTrue((numSaltBytes > 0),
148         "numSaltBytes must be greater than zero.");
149
150    this.saltAfterClearPassword = saltAfterClearPassword;
151    this.saltAfterMessageDigest = saltAfterMessageDigest;
152
153    random = new SecureRandom();
154  }
155
156
157
158  /**
159   * Retrieves the digest algorithm that will be used when encoding passwords.
160   *
161   * @return  The message digest
162   */
163  public String getDigestAlgorithm()
164  {
165    return messageDigest.getAlgorithm();
166  }
167
168
169
170  /**
171   * Retrieves the digest length, in bytes.
172   *
173   * @return  The digest length, in bytes.
174   */
175  public int getDigestLengthBytes()
176  {
177    return digestLengthBytes;
178  }
179
180
181
182  /**
183   * Retrieves the number of bytes of salt that will be generated when encoding
184   * a password.  Note that this is used only when encoding new clear-text
185   * passwords.  When comparing a clear-text password against an existing
186   * encoded representation, the number of salt bytes from the existing encoded
187   * password will be used.
188   *
189   * @return  The number of bytes of salt that will be generated when encoding a
190   *          password.
191   */
192  public int getNumSaltBytes()
193  {
194    return numSaltBytes;
195  }
196
197
198
199  /**
200   * Indicates whether the salt should be appended or prepended to the
201   * clear-text password when computing the message digest.
202   *
203   * @return  {@code true} if the salt should be appended to the clear-text
204   *          password when computing the message digest, or {@code false} if
205   *          the salt should be prepended to the clear-text password.
206   */
207  public boolean isSaltAfterClearPassword()
208  {
209    return saltAfterClearPassword;
210  }
211
212
213
214  /**
215   * Indicates whether the salt should be appended or prepended to the digest
216   * when generating the encoded representation for the password.
217   *
218   * @return  {@code true} if the salt should be appended to the digest when
219   *          generating the encoded representation for the password, or
220   *          {@code false} if the salt should be prepended to the digest.
221   */
222  public boolean isSaltAfterMessageDigest()
223  {
224    return saltAfterMessageDigest;
225  }
226
227
228
229  /**
230   * {@inheritDoc}
231   */
232  @Override()
233  protected byte[] encodePassword(final byte[] clearPassword,
234                                  final ReadOnlyEntry userEntry,
235                                  final List<Modification> modifications)
236            throws LDAPException
237  {
238    final byte[] salt = new byte[numSaltBytes];
239    random.nextBytes(salt);
240
241    final byte[] saltedPassword;
242    if (saltAfterClearPassword)
243    {
244      saltedPassword = concatenate(clearPassword, salt);
245    }
246    else
247    {
248      saltedPassword = concatenate(salt, clearPassword);
249    }
250
251    final byte[] digest = messageDigest.digest(saltedPassword);
252
253    if (saltAfterMessageDigest)
254    {
255      return concatenate(digest, salt);
256    }
257    else
258    {
259      return concatenate(salt, digest);
260    }
261  }
262
263
264
265  /**
266   * Creates a new byte array that is a concatenation of the provided byte
267   * arrays.
268   *
269   * @param  b1  The byte array to appear first in the concatenation.
270   * @param  b2  The byte array to appear second in the concatenation.
271   *
272   * @return  A byte array containing the concatenation.
273   */
274  private static byte[] concatenate(final byte[] b1, final byte[] b2)
275  {
276    final byte[] combined = new byte[b1.length + b2.length];
277    System.arraycopy(b1, 0, combined, 0, b1.length);
278    System.arraycopy(b2, 0, combined, b1.length, b2.length);
279    return combined;
280  }
281
282
283
284  /**
285   * {@inheritDoc}
286   */
287  @Override()
288  protected void ensurePreEncodedPasswordAppearsValid(
289                      final byte[] unPrefixedUnFormattedEncodedPasswordBytes,
290                      final ReadOnlyEntry userEntry,
291                      final List<Modification> modifications)
292            throws LDAPException
293  {
294    // Make sure that the encoded password is longer than the digest length
295    // so that there is room for some amount of salt.
296    if (unPrefixedUnFormattedEncodedPasswordBytes.length <= digestLengthBytes)
297    {
298      throw new LDAPException(ResultCode.PARAM_ERROR,
299           ERR_SALTED_DIGEST_PW_ENCODER_PRE_ENCODED_LENGTH_MISMATCH.get(
300                messageDigest.getAlgorithm(),
301                unPrefixedUnFormattedEncodedPasswordBytes.length,
302                (digestLengthBytes + 1)));
303    }
304  }
305
306
307
308  /**
309   * {@inheritDoc}
310   */
311  @Override()
312  protected boolean passwordMatches(final byte[] clearPasswordBytes,
313                         final byte[] unPrefixedUnFormattedEncodedPasswordBytes,
314                         final ReadOnlyEntry userEntry)
315            throws LDAPException
316  {
317    // Subtract the digest length from the encoded password to get the number
318    // of salt bytes.  If the number of salt bytes is less than or equal to
319    // zero, then the password will not match.
320    final int numComputedSaltBytes =
321         unPrefixedUnFormattedEncodedPasswordBytes.length - digestLengthBytes;
322    if (numComputedSaltBytes <= 0)
323    {
324      return false;
325    }
326
327
328    // Separate the salt and the digest.
329    final byte[] salt = new byte[numComputedSaltBytes];
330    final byte[] digest = new byte[digestLengthBytes];
331    if (saltAfterMessageDigest)
332    {
333      System.arraycopy(unPrefixedUnFormattedEncodedPasswordBytes, 0, digest, 0,
334           digestLengthBytes);
335      System.arraycopy(unPrefixedUnFormattedEncodedPasswordBytes,
336           digestLengthBytes, salt, 0, salt.length);
337    }
338    else
339    {
340      System.arraycopy(unPrefixedUnFormattedEncodedPasswordBytes, 0, salt, 0,
341           salt.length);
342      System.arraycopy(unPrefixedUnFormattedEncodedPasswordBytes, salt.length,
343           digest, 0, digestLengthBytes);
344    }
345
346
347    // Now that we have the salt, combine it with the clear-text password in the
348    // proper order.
349    // Combine the clear-text password and the salt in the proper order.
350    final byte[] saltedPassword;
351    if (saltAfterClearPassword)
352    {
353      saltedPassword = concatenate(clearPasswordBytes, salt);
354    }
355    else
356    {
357      saltedPassword = concatenate(salt, clearPasswordBytes);
358    }
359
360
361    // Compute a digest of the salted password and see whether it matches the
362    // digest we extracted earlier.  If so, then the clear-text password
363    // matches.  If not, then it doesn't.
364    final byte[] computedDigest = messageDigest.digest(saltedPassword);
365    return Arrays.equals(computedDigest, digest);
366  }
367
368
369
370  /**
371   * {@inheritDoc}
372   */
373  @Override()
374  protected byte[] extractClearPassword(
375                 final byte[] unPrefixedUnFormattedEncodedPasswordBytes,
376                 final ReadOnlyEntry userEntry)
377            throws LDAPException
378  {
379    throw new LDAPException(ResultCode.NOT_SUPPORTED,
380         ERR_SALTED_DIGEST_PW_ENCODER_NOT_REVERSIBLE.get());
381  }
382
383
384
385  /**
386   * {@inheritDoc}
387   */
388  @Override()
389  public void toString(final StringBuilder buffer)
390  {
391    buffer.append("SaltedMessageDigestInMemoryPasswordEncoder(prefix='");
392    buffer.append(getPrefix());
393    buffer.append("', outputFormatter=");
394
395    final PasswordEncoderOutputFormatter outputFormatter =
396         getOutputFormatter();
397    if (outputFormatter == null)
398    {
399      buffer.append("null");
400    }
401    else
402    {
403      outputFormatter.toString(buffer);
404    }
405
406    buffer.append(", digestAlgorithm='");
407    buffer.append(messageDigest.getAlgorithm());
408    buffer.append("', digestLengthBytes=");
409    buffer.append(messageDigest.getDigestLength());
410    buffer.append(", numSaltBytes=");
411    buffer.append(numSaltBytes);
412    buffer.append(", saltAfterClearPassword=");
413    buffer.append(saltAfterClearPassword);
414    buffer.append(", saltAfterMessageDigest=");
415    buffer.append(saltAfterMessageDigest);
416    buffer.append(')');
417  }
418}