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.NotNull; 050import com.unboundid.util.Nullable; 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 // The secure random number generator used for generating salts. 089 @NotNull private final SecureRandom random; 090 091 092 093 /** 094 * Creates a new instance of this in-memory directory server password encoder 095 * with the provided information. 096 * 097 * @param prefix The string that will appear at the 098 * beginning of encoded passwords. It must 099 * not be {@code null} or empty. 100 * @param outputFormatter The output formatter that will be used to 101 * format the encoded representation of 102 * clear-text passwords. It may be 103 * {@code null} if no special formatting 104 * should be applied to the raw bytes. 105 * @param messageDigest The message digest that will be used to 106 * actually perform the encoding. It must not 107 * be {@code null}. 108 * @param numSaltBytes The number of salt bytes to generate when 109 * encoding passwords. It must be greater 110 * than zero. 111 * @param saltAfterClearPassword Indicates whether the salt should be placed 112 * after or before the clear-text password 113 * when computing the message digest. If this 114 * is {@code true}, then the digest will be 115 * computed from the concatenation of the 116 * clear-text password and the salt, in that 117 * order. If this is {@code false}, then the 118 * digest will be computed from the 119 * concatenation of the salt and the 120 * clear-text password. 121 * @param saltAfterMessageDigest Indicates whether the salt should be placed 122 * after or before the computed digest when 123 * creating the encoded representation. If 124 * this is {@code true}, then the encoded 125 * password will consist of the concatenation 126 * of the computed message digest and the 127 * salt, in that order. If this is 128 * {@code false}, then the encoded password 129 * will consist of the concatenation of the 130 * salt and the message digest. 131 */ 132 public SaltedMessageDigestInMemoryPasswordEncoder( 133 @NotNull final String prefix, 134 @Nullable final PasswordEncoderOutputFormatter outputFormatter, 135 @NotNull final MessageDigest messageDigest, 136 final int numSaltBytes, final boolean saltAfterClearPassword, 137 final boolean saltAfterMessageDigest) 138 { 139 super(prefix, outputFormatter); 140 141 Validator.ensureNotNull(messageDigest); 142 this.messageDigest = messageDigest; 143 144 digestLengthBytes = messageDigest.getDigestLength(); 145 Validator.ensureTrue((digestLengthBytes > 0), 146 "The message digest use a fixed digest length, and that " + 147 "length must be greater than zero."); 148 149 this.numSaltBytes = numSaltBytes; 150 Validator.ensureTrue((numSaltBytes > 0), 151 "numSaltBytes must be greater than zero."); 152 153 this.saltAfterClearPassword = saltAfterClearPassword; 154 this.saltAfterMessageDigest = saltAfterMessageDigest; 155 156 random = new SecureRandom(); 157 } 158 159 160 161 /** 162 * Retrieves the digest algorithm that will be used when encoding passwords. 163 * 164 * @return The message digest 165 */ 166 @NotNull() 167 public String getDigestAlgorithm() 168 { 169 return messageDigest.getAlgorithm(); 170 } 171 172 173 174 /** 175 * Retrieves the digest length, in bytes. 176 * 177 * @return The digest length, in bytes. 178 */ 179 public int getDigestLengthBytes() 180 { 181 return digestLengthBytes; 182 } 183 184 185 186 /** 187 * Retrieves the number of bytes of salt that will be generated when encoding 188 * a password. Note that this is used only when encoding new clear-text 189 * passwords. When comparing a clear-text password against an existing 190 * encoded representation, the number of salt bytes from the existing encoded 191 * password will be used. 192 * 193 * @return The number of bytes of salt that will be generated when encoding a 194 * password. 195 */ 196 public int getNumSaltBytes() 197 { 198 return numSaltBytes; 199 } 200 201 202 203 /** 204 * Indicates whether the salt should be appended or prepended to the 205 * clear-text password when computing the message digest. 206 * 207 * @return {@code true} if the salt should be appended to the clear-text 208 * password when computing the message digest, or {@code false} if 209 * the salt should be prepended to the clear-text password. 210 */ 211 public boolean isSaltAfterClearPassword() 212 { 213 return saltAfterClearPassword; 214 } 215 216 217 218 /** 219 * Indicates whether the salt should be appended or prepended to the digest 220 * when generating the encoded representation for the password. 221 * 222 * @return {@code true} if the salt should be appended to the digest when 223 * generating the encoded representation for the password, or 224 * {@code false} if the salt should be prepended to the digest. 225 */ 226 public boolean isSaltAfterMessageDigest() 227 { 228 return saltAfterMessageDigest; 229 } 230 231 232 233 /** 234 * {@inheritDoc} 235 */ 236 @Override() 237 @NotNull() 238 protected byte[] encodePassword(@NotNull final byte[] clearPassword, 239 @NotNull final ReadOnlyEntry userEntry, 240 @NotNull final List<Modification> modifications) 241 throws LDAPException 242 { 243 final byte[] salt = new byte[numSaltBytes]; 244 random.nextBytes(salt); 245 246 final byte[] saltedPassword; 247 if (saltAfterClearPassword) 248 { 249 saltedPassword = concatenate(clearPassword, salt); 250 } 251 else 252 { 253 saltedPassword = concatenate(salt, clearPassword); 254 } 255 256 final byte[] digest = messageDigest.digest(saltedPassword); 257 258 if (saltAfterMessageDigest) 259 { 260 return concatenate(digest, salt); 261 } 262 else 263 { 264 return concatenate(salt, digest); 265 } 266 } 267 268 269 270 /** 271 * Creates a new byte array that is a concatenation of the provided byte 272 * arrays. 273 * 274 * @param b1 The byte array to appear first in the concatenation. 275 * @param b2 The byte array to appear second in the concatenation. 276 * 277 * @return A byte array containing the concatenation. 278 */ 279 @NotNull() 280 private static byte[] concatenate(@NotNull final byte[] b1, 281 @NotNull final byte[] b2) 282 { 283 final byte[] combined = new byte[b1.length + b2.length]; 284 System.arraycopy(b1, 0, combined, 0, b1.length); 285 System.arraycopy(b2, 0, combined, b1.length, b2.length); 286 return combined; 287 } 288 289 290 291 /** 292 * {@inheritDoc} 293 */ 294 @Override() 295 protected void ensurePreEncodedPasswordAppearsValid( 296 @NotNull final byte[] unPrefixedUnFormattedEncodedPasswordBytes, 297 @NotNull final ReadOnlyEntry userEntry, 298 @NotNull final List<Modification> modifications) 299 throws LDAPException 300 { 301 // Make sure that the encoded password is longer than the digest length 302 // so that there is room for some amount of salt. 303 if (unPrefixedUnFormattedEncodedPasswordBytes.length <= digestLengthBytes) 304 { 305 throw new LDAPException(ResultCode.PARAM_ERROR, 306 ERR_SALTED_DIGEST_PW_ENCODER_PRE_ENCODED_LENGTH_MISMATCH.get( 307 messageDigest.getAlgorithm(), 308 unPrefixedUnFormattedEncodedPasswordBytes.length, 309 (digestLengthBytes + 1))); 310 } 311 } 312 313 314 315 /** 316 * {@inheritDoc} 317 */ 318 @Override() 319 protected boolean passwordMatches(@NotNull final byte[] clearPasswordBytes, 320 @NotNull final byte[] unPrefixedUnFormattedEncodedPasswordBytes, 321 @NotNull final ReadOnlyEntry userEntry) 322 throws LDAPException 323 { 324 // Subtract the digest length from the encoded password to get the number 325 // of salt bytes. If the number of salt bytes is less than or equal to 326 // zero, then the password will not match. 327 final int numComputedSaltBytes = 328 unPrefixedUnFormattedEncodedPasswordBytes.length - digestLengthBytes; 329 if (numComputedSaltBytes <= 0) 330 { 331 return false; 332 } 333 334 335 // Separate the salt and the digest. 336 final byte[] salt = new byte[numComputedSaltBytes]; 337 final byte[] digest = new byte[digestLengthBytes]; 338 if (saltAfterMessageDigest) 339 { 340 System.arraycopy(unPrefixedUnFormattedEncodedPasswordBytes, 0, digest, 0, 341 digestLengthBytes); 342 System.arraycopy(unPrefixedUnFormattedEncodedPasswordBytes, 343 digestLengthBytes, salt, 0, salt.length); 344 } 345 else 346 { 347 System.arraycopy(unPrefixedUnFormattedEncodedPasswordBytes, 0, salt, 0, 348 salt.length); 349 System.arraycopy(unPrefixedUnFormattedEncodedPasswordBytes, salt.length, 350 digest, 0, digestLengthBytes); 351 } 352 353 354 // Now that we have the salt, combine it with the clear-text password in the 355 // proper order. 356 // Combine the clear-text password and the salt in the proper order. 357 final byte[] saltedPassword; 358 if (saltAfterClearPassword) 359 { 360 saltedPassword = concatenate(clearPasswordBytes, salt); 361 } 362 else 363 { 364 saltedPassword = concatenate(salt, clearPasswordBytes); 365 } 366 367 368 // Compute a digest of the salted password and see whether it matches the 369 // digest we extracted earlier. If so, then the clear-text password 370 // matches. If not, then it doesn't. 371 final byte[] computedDigest = messageDigest.digest(saltedPassword); 372 return Arrays.equals(computedDigest, digest); 373 } 374 375 376 377 /** 378 * {@inheritDoc} 379 */ 380 @Override() 381 @NotNull() 382 protected byte[] extractClearPassword( 383 @NotNull final byte[] unPrefixedUnFormattedEncodedPasswordBytes, 384 @NotNull final ReadOnlyEntry userEntry) 385 throws LDAPException 386 { 387 throw new LDAPException(ResultCode.NOT_SUPPORTED, 388 ERR_SALTED_DIGEST_PW_ENCODER_NOT_REVERSIBLE.get()); 389 } 390 391 392 393 /** 394 * {@inheritDoc} 395 */ 396 @Override() 397 public void toString(@NotNull final StringBuilder buffer) 398 { 399 buffer.append("SaltedMessageDigestInMemoryPasswordEncoder(prefix='"); 400 buffer.append(getPrefix()); 401 buffer.append("', outputFormatter="); 402 403 final PasswordEncoderOutputFormatter outputFormatter = 404 getOutputFormatter(); 405 if (outputFormatter == null) 406 { 407 buffer.append("null"); 408 } 409 else 410 { 411 outputFormatter.toString(buffer); 412 } 413 414 buffer.append(", digestAlgorithm='"); 415 buffer.append(messageDigest.getAlgorithm()); 416 buffer.append("', digestLengthBytes="); 417 buffer.append(messageDigest.getDigestLength()); 418 buffer.append(", numSaltBytes="); 419 buffer.append(numSaltBytes); 420 buffer.append(", saltAfterClearPassword="); 421 buffer.append(saltAfterClearPassword); 422 buffer.append(", saltAfterMessageDigest="); 423 buffer.append(saltAfterMessageDigest); 424 buffer.append(')'); 425 } 426}