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}