001/*
002 * Copyright 2017-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2017-2024 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-2024 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.util.Arrays;
042import java.util.List;
043
044import com.unboundid.ldap.sdk.LDAPException;
045import com.unboundid.ldap.sdk.Modification;
046import com.unboundid.ldap.sdk.ReadOnlyEntry;
047import com.unboundid.ldap.sdk.ResultCode;
048import com.unboundid.util.NotNull;
049import com.unboundid.util.Nullable;
050import com.unboundid.util.ThreadLocalSecureRandom;
051import com.unboundid.util.ThreadSafety;
052import com.unboundid.util.ThreadSafetyLevel;
053import com.unboundid.util.Validator;
054
055import static com.unboundid.ldap.listener.ListenerMessages.*;
056
057
058
059/**
060 * This class provides an implementation of an in-memory directory server
061 * password encoder that uses a message digest to encode passwords.  Encoded
062 * passwords will also include some number of randomly generated bytes, called a
063 * salt, to ensure that encoding the same password multiple times will yield
064 * multiple different encoded representations.
065 */
066@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
067public final class SaltedMessageDigestInMemoryPasswordEncoder
068       extends InMemoryPasswordEncoder
069{
070  // Indicates whether the salt should go after or before the clear-text
071  // password when generating the message digest.
072  private final boolean saltAfterClearPassword;
073
074  // Indicates whether the salt should go after or before the digest bytes
075  // when generating the final encoded representation.
076  private final boolean saltAfterMessageDigest;
077
078  // The length of the generated message digest, in bytes.
079  private final int digestLengthBytes;
080
081  // The number of salt bytes to generate.
082  private final int numSaltBytes;
083
084  // The message digest instance tha will be used to actually perform the
085  // encoding.
086  @NotNull private final MessageDigest messageDigest;
087
088
089
090  /**
091   * Creates a new instance of this in-memory directory server password encoder
092   * with the provided information.
093   *
094   * @param  prefix                  The string that will appear at the
095   *                                 beginning of encoded passwords.  It must
096   *                                 not be {@code null} or empty.
097   * @param  outputFormatter         The output formatter that will be used to
098   *                                 format the encoded representation of
099   *                                 clear-text passwords.  It may be
100   *                                 {@code null} if no special formatting
101   *                                 should be applied to the raw bytes.
102   * @param  messageDigest           The message digest that will be used to
103   *                                 actually perform the encoding.  It must not
104   *                                 be {@code null}.
105   * @param  numSaltBytes            The number of salt bytes to generate when
106   *                                 encoding passwords.  It must be greater
107   *                                 than zero.
108   * @param  saltAfterClearPassword  Indicates whether the salt should be placed
109   *                                 after or before the clear-text password
110   *                                 when computing the message digest.  If this
111   *                                 is {@code true}, then the digest will be
112   *                                 computed from the concatenation of the
113   *                                 clear-text password and the salt, in that
114   *                                 order.  If this is {@code false}, then the
115   *                                 digest will be computed from the
116   *                                 concatenation of the salt and the
117   *                                 clear-text password.
118   * @param  saltAfterMessageDigest  Indicates whether the salt should be placed
119   *                                 after or before the computed digest when
120   *                                 creating the encoded representation.  If
121   *                                 this is {@code true}, then the encoded
122   *                                 password will consist of the concatenation
123   *                                 of the computed message digest and the
124   *                                 salt, in that order.  If this is
125   *                                 {@code false}, then the encoded password
126   *                                 will consist of the concatenation of the
127   *                                 salt and the message digest.
128   */
129  public SaltedMessageDigestInMemoryPasswordEncoder(
130              @NotNull final String prefix,
131              @Nullable final PasswordEncoderOutputFormatter outputFormatter,
132              @NotNull final MessageDigest messageDigest,
133              final int numSaltBytes, 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
154
155
156  /**
157   * Retrieves the digest algorithm that will be used when encoding passwords.
158   *
159   * @return  The message digest
160   */
161  @NotNull()
162  public String getDigestAlgorithm()
163  {
164    return messageDigest.getAlgorithm();
165  }
166
167
168
169  /**
170   * Retrieves the digest length, in bytes.
171   *
172   * @return  The digest length, in bytes.
173   */
174  public int getDigestLengthBytes()
175  {
176    return digestLengthBytes;
177  }
178
179
180
181  /**
182   * Retrieves the number of bytes of salt that will be generated when encoding
183   * a password.  Note that this is used only when encoding new clear-text
184   * passwords.  When comparing a clear-text password against an existing
185   * encoded representation, the number of salt bytes from the existing encoded
186   * password will be used.
187   *
188   * @return  The number of bytes of salt that will be generated when encoding a
189   *          password.
190   */
191  public int getNumSaltBytes()
192  {
193    return numSaltBytes;
194  }
195
196
197
198  /**
199   * Indicates whether the salt should be appended or prepended to the
200   * clear-text password when computing the message digest.
201   *
202   * @return  {@code true} if the salt should be appended to the clear-text
203   *          password when computing the message digest, or {@code false} if
204   *          the salt should be prepended to the clear-text password.
205   */
206  public boolean isSaltAfterClearPassword()
207  {
208    return saltAfterClearPassword;
209  }
210
211
212
213  /**
214   * Indicates whether the salt should be appended or prepended to the digest
215   * when generating the encoded representation for the password.
216   *
217   * @return  {@code true} if the salt should be appended to the digest when
218   *          generating the encoded representation for the password, or
219   *          {@code false} if the salt should be prepended to the digest.
220   */
221  public boolean isSaltAfterMessageDigest()
222  {
223    return saltAfterMessageDigest;
224  }
225
226
227
228  /**
229   * {@inheritDoc}
230   */
231  @Override()
232  @NotNull()
233  protected byte[] encodePassword(@NotNull final byte[] clearPassword,
234                        @NotNull final ReadOnlyEntry userEntry,
235                        @NotNull final List<Modification> modifications)
236            throws LDAPException
237  {
238    final byte[] salt = new byte[numSaltBytes];
239    ThreadLocalSecureRandom.get().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  @NotNull()
275  private static byte[] concatenate(@NotNull final byte[] b1,
276                                    @NotNull final byte[] b2)
277  {
278    final byte[] combined = new byte[b1.length + b2.length];
279    System.arraycopy(b1, 0, combined, 0, b1.length);
280    System.arraycopy(b2, 0, combined, b1.length, b2.length);
281    return combined;
282  }
283
284
285
286  /**
287   * {@inheritDoc}
288   */
289  @Override()
290  protected void ensurePreEncodedPasswordAppearsValid(
291       @NotNull final byte[] unPrefixedUnFormattedEncodedPasswordBytes,
292       @NotNull final ReadOnlyEntry userEntry,
293       @NotNull final List<Modification> modifications)
294       throws LDAPException
295  {
296    // Make sure that the encoded password is longer than the digest length
297    // so that there is room for some amount of salt.
298    if (unPrefixedUnFormattedEncodedPasswordBytes.length <= digestLengthBytes)
299    {
300      throw new LDAPException(ResultCode.PARAM_ERROR,
301           ERR_SALTED_DIGEST_PW_ENCODER_PRE_ENCODED_LENGTH_MISMATCH.get(
302                messageDigest.getAlgorithm(),
303                unPrefixedUnFormattedEncodedPasswordBytes.length,
304                (digestLengthBytes + 1)));
305    }
306  }
307
308
309
310  /**
311   * {@inheritDoc}
312   */
313  @Override()
314  protected boolean passwordMatches(@NotNull final byte[] clearPasswordBytes,
315       @NotNull final byte[] unPrefixedUnFormattedEncodedPasswordBytes,
316       @NotNull final ReadOnlyEntry userEntry)
317       throws LDAPException
318  {
319    // Subtract the digest length from the encoded password to get the number
320    // of salt bytes.  If the number of salt bytes is less than or equal to
321    // zero, then the password will not match.
322    final int numComputedSaltBytes =
323         unPrefixedUnFormattedEncodedPasswordBytes.length - digestLengthBytes;
324    if (numComputedSaltBytes <= 0)
325    {
326      return false;
327    }
328
329
330    // Separate the salt and the digest.
331    final byte[] salt = new byte[numComputedSaltBytes];
332    final byte[] digest = new byte[digestLengthBytes];
333    if (saltAfterMessageDigest)
334    {
335      System.arraycopy(unPrefixedUnFormattedEncodedPasswordBytes, 0, digest, 0,
336           digestLengthBytes);
337      System.arraycopy(unPrefixedUnFormattedEncodedPasswordBytes,
338           digestLengthBytes, salt, 0, salt.length);
339    }
340    else
341    {
342      System.arraycopy(unPrefixedUnFormattedEncodedPasswordBytes, 0, salt, 0,
343           salt.length);
344      System.arraycopy(unPrefixedUnFormattedEncodedPasswordBytes, salt.length,
345           digest, 0, digestLengthBytes);
346    }
347
348
349    // Now that we have the salt, combine it with the clear-text password in the
350    // proper order.
351    // Combine the clear-text password and the salt in the proper order.
352    final byte[] saltedPassword;
353    if (saltAfterClearPassword)
354    {
355      saltedPassword = concatenate(clearPasswordBytes, salt);
356    }
357    else
358    {
359      saltedPassword = concatenate(salt, clearPasswordBytes);
360    }
361
362
363    // Compute a digest of the salted password and see whether it matches the
364    // digest we extracted earlier.  If so, then the clear-text password
365    // matches.  If not, then it doesn't.
366    final byte[] computedDigest = messageDigest.digest(saltedPassword);
367    return Arrays.equals(computedDigest, digest);
368  }
369
370
371
372  /**
373   * {@inheritDoc}
374   */
375  @Override()
376  @NotNull()
377  protected byte[] extractClearPassword(
378       @NotNull final byte[] unPrefixedUnFormattedEncodedPasswordBytes,
379       @NotNull final ReadOnlyEntry userEntry)
380            throws LDAPException
381  {
382    throw new LDAPException(ResultCode.NOT_SUPPORTED,
383         ERR_SALTED_DIGEST_PW_ENCODER_NOT_REVERSIBLE.get());
384  }
385
386
387
388  /**
389   * {@inheritDoc}
390   */
391  @Override()
392  public void toString(@NotNull final StringBuilder buffer)
393  {
394    buffer.append("SaltedMessageDigestInMemoryPasswordEncoder(prefix='");
395    buffer.append(getPrefix());
396    buffer.append("', outputFormatter=");
397
398    final PasswordEncoderOutputFormatter outputFormatter =
399         getOutputFormatter();
400    if (outputFormatter == null)
401    {
402      buffer.append("null");
403    }
404    else
405    {
406      outputFormatter.toString(buffer);
407    }
408
409    buffer.append(", digestAlgorithm='");
410    buffer.append(messageDigest.getAlgorithm());
411    buffer.append("', digestLengthBytes=");
412    buffer.append(messageDigest.getDigestLength());
413    buffer.append(", numSaltBytes=");
414    buffer.append(numSaltBytes);
415    buffer.append(", saltAfterClearPassword=");
416    buffer.append(saltAfterClearPassword);
417    buffer.append(", saltAfterMessageDigest=");
418    buffer.append(saltAfterMessageDigest);
419    buffer.append(')');
420  }
421}