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}