001/* 002 * Copyright 2017-2023 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2017-2023 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-2023 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.util.List; 041 042import com.unboundid.asn1.ASN1OctetString; 043import com.unboundid.ldap.sdk.LDAPException; 044import com.unboundid.ldap.sdk.Modification; 045import com.unboundid.ldap.sdk.ReadOnlyEntry; 046import com.unboundid.ldap.sdk.ResultCode; 047import com.unboundid.util.Extensible; 048import com.unboundid.util.NotNull; 049import com.unboundid.util.Nullable; 050import com.unboundid.util.StaticUtils; 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 defines an API that may be used to interact with clear-text 061 * passwords provided to the in-memory directory server. It can be used to 062 * ensure that clear-text passwords are encoded when storing them in the server, 063 * and to determine whether a provided clear-text password matches an encoded 064 * value. 065 */ 066@Extensible() 067@ThreadSafety(level=ThreadSafetyLevel.INTERFACE_NOT_THREADSAFE) 068public abstract class InMemoryPasswordEncoder 069{ 070 // The bytes that comprise the prefix. 071 @NotNull private final byte[] prefixBytes; 072 073 // The output formatter that will be used to format the encoded representation 074 // of clear-text passwords. 075 @Nullable private final PasswordEncoderOutputFormatter outputFormatter; 076 077 // The string that will appear at the beginning of encoded passwords. 078 @NotNull private final String prefix; 079 080 081 082 /** 083 * Creates a new instance of this in-memory directory server password encoder 084 * with the provided information. 085 * 086 * @param prefix The string that will appear at the beginning of 087 * encoded passwords. It must not be {@code null} or 088 * empty. 089 * @param outputFormatter The output formatter that will be used to format 090 * the encoded representation of clear-text 091 * passwords. It may be {@code null} if no 092 * special formatting should be applied to the raw 093 * bytes. 094 */ 095 protected InMemoryPasswordEncoder(@NotNull final String prefix, 096 @Nullable final PasswordEncoderOutputFormatter outputFormatter) 097 { 098 Validator.ensureNotNullOrEmpty(prefix, 099 "The password encoder prefix must not be null or empty."); 100 101 this.prefix = prefix; 102 this.outputFormatter = outputFormatter; 103 104 prefixBytes = StaticUtils.getBytes(prefix); 105 } 106 107 108 109 /** 110 * Retrieves the string that will appear at the beginning of encoded 111 * passwords. 112 * 113 * @return The string that will appear at the beginning of encoded passwords. 114 */ 115 @NotNull() 116 public final String getPrefix() 117 { 118 return prefix; 119 } 120 121 122 123 /** 124 * Retrieves the output formatter that will be used when generating the 125 * encoded representation of a password. 126 * 127 * @return The output formatter that will be used when generating the encoded 128 * representation of a password, or {@code nulL} if no output 129 * formatting will be applied. 130 */ 131 @Nullable() 132 public final PasswordEncoderOutputFormatter getOutputFormatter() 133 { 134 return outputFormatter; 135 } 136 137 138 139 /** 140 * Encodes the provided clear-text password for storage in the in-memory 141 * directory server. The encoded password that is returned will include the 142 * prefix, and any appropriate output formatting will have been applied. 143 * <BR><BR> 144 * This method will be invoked when adding data into the server, including 145 * through LDAP add operations or LDIF imports, and when modifying existing 146 * entries through LDAP modify operations. 147 * 148 * @param clearPassword The clear-text password to be encoded. It must not 149 * be {@code null} or empty, and it must not be 150 * pre-encoded. 151 * @param userEntry The entry in which the encoded password will appear. 152 * It must not be {@code null}. If the entry is in the 153 * process of being modified, then this will be a 154 * representation of the entry as it appeared before 155 * any changes have been applied. 156 * @param modifications A set of modifications to be applied to the user 157 * entry. It must not be [@code null}. It will be an 158 * empty list for entries created via LDAP add and LDIF 159 * import operations. It will be a non-empty list for 160 * LDAP modifications. 161 * 162 * @return The encoded representation of the provided clear-text password. 163 * It will include the prefix, and any appropriate output formatting 164 * will have been applied. 165 * 166 * @throws LDAPException If a problem is encountered while trying to encode 167 * the provided clear-text password. 168 */ 169 @NotNull() 170 public final ASN1OctetString encodePassword( 171 @NotNull final ASN1OctetString clearPassword, 172 @NotNull final ReadOnlyEntry userEntry, 173 @NotNull final List<Modification> modifications) 174 throws LDAPException 175 { 176 if (clearPassword.getValueLength() == 0) 177 { 178 throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM, 179 ERR_PW_ENCODER_ENCODE_PASSWORD_EMPTY.get()); 180 } 181 182 final byte[] clearPasswordBytes = clearPassword.getValue(); 183 final byte[] encodedPasswordBytes = 184 encodePassword(clearPasswordBytes, userEntry, modifications); 185 186 final byte[] formattedEncodedPasswordBytes; 187 if (outputFormatter == null) 188 { 189 formattedEncodedPasswordBytes = encodedPasswordBytes; 190 } 191 else 192 { 193 formattedEncodedPasswordBytes = 194 outputFormatter.format(encodedPasswordBytes); 195 } 196 197 final byte[] formattedPasswordBytesWithPrefix = 198 new byte[formattedEncodedPasswordBytes.length + prefixBytes.length]; 199 System.arraycopy(prefixBytes, 0, formattedPasswordBytesWithPrefix, 0, 200 prefixBytes.length); 201 System.arraycopy(formattedEncodedPasswordBytes, 0, 202 formattedPasswordBytesWithPrefix, prefixBytes.length, 203 formattedEncodedPasswordBytes.length); 204 205 return new ASN1OctetString(formattedPasswordBytesWithPrefix); 206 } 207 208 209 210 /** 211 * Encodes the provided clear-text password for storage in the in-memory 212 * directory server. The encoded password that is returned must not include 213 * the prefix, and no output formatting should have been applied. 214 * <BR><BR> 215 * This method will be invoked when adding data into the server, including 216 * through LDAP add operations or LDIF imports, and when modifying existing 217 * entries through LDAP modify operations. 218 * 219 * @param clearPassword The bytes that comprise the clear-text password to 220 * be encoded. It must not be {@code null} or empty. 221 * @param userEntry The entry in which the encoded password will appear. 222 * It must not be {@code null}. If the entry is in the 223 * process of being modified, then this will be a 224 * representation of the entry as it appeared before 225 * any changes have been applied. 226 * @param modifications A set of modifications to be applied to the user 227 * entry. It must not be [@code null}. It will be an 228 * empty list for entries created via LDAP add and LDIF 229 * import operations. It will be a non-empty list for 230 * LDAP modifications. 231 * 232 * @return The bytes that comprise encoded representation of the provided 233 * clear-text password, without the prefix, and without any output 234 * formatting applied. 235 * 236 * @throws LDAPException If a problem is encountered while trying to encode 237 * the provided clear-text password. 238 */ 239 @NotNull() 240 protected abstract byte[] encodePassword(@NotNull byte[] clearPassword, 241 @NotNull ReadOnlyEntry userEntry, 242 @NotNull List<Modification> modifications) 243 throws LDAPException; 244 245 246 247 /** 248 * Verifies that the provided pre-encoded password (including the prefix, and 249 * with any appropriate output formatting applied) is compatible with the 250 * validation performed by this password encoder. 251 * <BR><BR> 252 * This method will be invoked when adding data into the server, including 253 * through LDAP add operations or LDIF imports, and when modifying existing 254 * entries through LDAP modify operations. Any password included in any of 255 * these entries that starts with a prefix registered with the in-memory 256 * directory server will be validated with the encoder that corresponds to 257 * that password's prefix. 258 * 259 * @param prefixedFormattedEncodedPassword 260 * The pre-encoded password to validate. It must not be 261 * {@code null}, and it should include the prefix and any 262 * applicable output formatting. 263 * @param userEntry 264 * The entry in which the password will appear. It must not be 265 * {@code null}. If the entry is in the process of being 266 * modified, then this will be a representation of the entry 267 * as it appeared before any changes have been applied. 268 * @param modifications 269 * A set of modifications to be applied to the user entry. It 270 * must not be [@code null}. It will be an empty list for 271 * entries created via LDAP add and LDIF import operations. It 272 * will be a non-empty list for LDAP modifications. 273 * 274 * @throws LDAPException If the provided encoded password is not compatible 275 * with the validation performed by this password 276 * encoder, or if a problem is encountered while 277 * making the determination. 278 */ 279 public final void ensurePreEncodedPasswordAppearsValid( 280 @NotNull final ASN1OctetString prefixedFormattedEncodedPassword, 281 @NotNull final ReadOnlyEntry userEntry, 282 @NotNull final List<Modification> modifications) 283 throws LDAPException 284 { 285 // Strip the prefix off the encoded password. 286 final byte[] prefixedFormattedEncodedPasswordBytes = 287 prefixedFormattedEncodedPassword.getValue(); 288 if (! passwordStartsWithPrefix(prefixedFormattedEncodedPasswordBytes)) 289 { 290 throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM, 291 ERR_PW_ENCODER_VALIDATE_ENCODED_PW_MISSING_PREFIX.get( 292 getClass().getName(), prefix)); 293 } 294 295 final byte[] unPrefixedFormattedEncodedPasswordBytes = 296 new byte[prefixedFormattedEncodedPasswordBytes.length - 297 prefixBytes.length]; 298 System.arraycopy(prefixedFormattedEncodedPasswordBytes, prefixBytes.length, 299 unPrefixedFormattedEncodedPasswordBytes, 0, 300 unPrefixedFormattedEncodedPasswordBytes.length); 301 302 303 // If an output formatter is configured, then revert the output formatting. 304 final byte[] unPrefixedUnFormattedEncodedPasswordBytes; 305 if (outputFormatter == null) 306 { 307 unPrefixedUnFormattedEncodedPasswordBytes = 308 unPrefixedFormattedEncodedPasswordBytes; 309 } 310 else 311 { 312 unPrefixedUnFormattedEncodedPasswordBytes = 313 outputFormatter.unFormat(unPrefixedFormattedEncodedPasswordBytes); 314 } 315 316 317 // Validate the un-prefixed, un-formatted password. 318 ensurePreEncodedPasswordAppearsValid( 319 unPrefixedUnFormattedEncodedPasswordBytes, userEntry, modifications); 320 } 321 322 323 324 /** 325 * Verifies that the provided pre-encoded password (with the prefix removed 326 * and any output formatting reverted) is compatible with the validation 327 * performed by this password encoder. 328 * <BR><BR> 329 * Note that this method should return {@code true} if the provided 330 * {@code unPrefixedUnFormattedEncodedPasswordBytes} value could be used in 331 * conjunction with the {@link #passwordMatches} method, even if it does not 332 * exactly match the format of the output that would have been generated by 333 * the {@link #encodePassword} method. For example, if this password encoder 334 * uses a salt, then it may be desirable to accept passwords encoded with a 335 * salt that has a different length than the {@code encodePassword} method 336 * would use when encoding a clear-test password. This may allow the 337 * in-memory directory server to support pre-encoded passwords generated from 338 * other types of directory servers that may use different settings when 339 * encoding passwords, but still generates encoded passwords that are 340 * compatible with this password encoder. 341 * 342 * @param unPrefixedUnFormattedEncodedPasswordBytes 343 * The bytes that comprise the pre-encoded password to validate, 344 * with the prefix stripped off and the output formatting 345 * reverted. 346 * @param userEntry 347 * The entry in which the password will appear. It must not be 348 * {@code null}. If the entry is in the process of being 349 * modified, then this will be a representation of the entry 350 * as it appeared before any changes have been applied. 351 * @param modifications 352 * A set of modifications to be applied to the user entry. It 353 * must not be [@code null}. It will be an empty list for 354 * entries created via LDAP add and LDIF import operations. It 355 * will be a non-empty list for LDAP modifications. 356 * 357 * @throws LDAPException If the provided encoded password is not compatible 358 * with the validation performed by this password 359 * encoder, or if a problem is encountered while 360 * making the determination. 361 */ 362 protected abstract void ensurePreEncodedPasswordAppearsValid( 363 @NotNull byte[] unPrefixedUnFormattedEncodedPasswordBytes, 364 @NotNull ReadOnlyEntry userEntry, 365 @NotNull List<Modification> modifications) 366 throws LDAPException; 367 368 369 370 /** 371 * Indicates whether the provided clear-text password could have been used to 372 * generate the given encoded password. This method will be invoked when 373 * verifying a provided clear-text password during bind processing, or when 374 * removing an existing password in a modify operation. 375 * 376 * @param clearPassword 377 * The clear-text password to be compared against the encoded 378 * password. It must not be {@code null} or empty. 379 * @param prefixedFormattedEncodedPassword 380 * The encoded password to compare against the clear-text 381 * password. It must not be {@code null}, it must include the 382 * prefix, and any appropriate output formatting must have been 383 * applied. 384 * @param userEntry 385 * The entry in which the encoded password appears. It must not 386 * be {@code null}. 387 * 388 * @return {@code true} if the provided clear-text password could be used to 389 * generate the given encoded password, or {@code false} if not. 390 * 391 * @throws LDAPException If a problem is encountered while making the 392 * determination. 393 */ 394 public final boolean clearPasswordMatchesEncodedPassword( 395 @NotNull final ASN1OctetString clearPassword, 396 @NotNull final ASN1OctetString prefixedFormattedEncodedPassword, 397 @NotNull final ReadOnlyEntry userEntry) 398 throws LDAPException 399 { 400 // Make sure that the provided clear-text password is not null or empty. 401 final byte[] clearPasswordBytes = clearPassword.getValue(); 402 if (clearPasswordBytes.length == 0) 403 { 404 return false; 405 } 406 407 408 // If the password doesn't start with the right prefix, then it's not 409 // considered a match. If it does start with the right prefix, then strip 410 // it off. 411 final byte[] prefixedFormattedEncodedPasswordBytes = 412 prefixedFormattedEncodedPassword.getValue(); 413 if (! passwordStartsWithPrefix(prefixedFormattedEncodedPasswordBytes)) 414 { 415 return false; 416 } 417 418 final byte[] unPrefixedFormattedEncodedPasswordBytes = 419 new byte[prefixedFormattedEncodedPasswordBytes.length - 420 prefixBytes.length]; 421 System.arraycopy(prefixedFormattedEncodedPasswordBytes, prefixBytes.length, 422 unPrefixedFormattedEncodedPasswordBytes, 0, 423 unPrefixedFormattedEncodedPasswordBytes.length); 424 425 426 // If an output formatter is configured, then revert the output formatting. 427 final byte[] unPrefixedUnFormattedEncodedPasswordBytes; 428 if (outputFormatter == null) 429 { 430 unPrefixedUnFormattedEncodedPasswordBytes = 431 unPrefixedFormattedEncodedPasswordBytes; 432 } 433 else 434 { 435 unPrefixedUnFormattedEncodedPasswordBytes = 436 outputFormatter.unFormat(unPrefixedFormattedEncodedPasswordBytes); 437 } 438 439 440 // Make sure that the resulting un-prefixed, un-formatted password is not 441 // empty. 442 if (unPrefixedUnFormattedEncodedPasswordBytes.length == 0) 443 { 444 return false; 445 } 446 447 448 // Determine whether the provided clear-text password could have been used 449 // to generate the encoded representation. 450 return passwordMatches(clearPasswordBytes, 451 unPrefixedUnFormattedEncodedPasswordBytes, userEntry); 452 } 453 454 455 456 /** 457 * Indicates whether the provided clear-text password could have been used to 458 * generate the given encoded password. This method will be invoked when 459 * verifying a provided clear-text password during bind processing, or when 460 * removing an existing password in a modify operation. 461 * 462 * @param clearPasswordBytes 463 * The bytes that comprise the clear-text password to be 464 * compared against the encoded password. It must not be 465 * {@code null} or empty. 466 * @param unPrefixedUnFormattedEncodedPasswordBytes 467 * The bytes that comprise the encoded password, with the prefix 468 * stripped off and the output formatting reverted. 469 * @param userEntry 470 * The entry in which the encoded password appears. It must not 471 * be {@code null}. 472 * 473 * @return {@code true} if the provided clear-text password could have been 474 * used to generate the given encoded password, or {@code false} if 475 * not. 476 * 477 * @throws LDAPException If a problem is encountered while attempting to 478 * make the determination. 479 */ 480 protected abstract boolean passwordMatches( 481 @NotNull byte[] clearPasswordBytes, 482 @NotNull byte[] unPrefixedUnFormattedEncodedPasswordBytes, 483 @NotNull ReadOnlyEntry userEntry) 484 throws LDAPException; 485 486 487 488 /** 489 * Attempts to extract the clear-text password used to generate the provided 490 * encoded representation, if possible. Many password encoder implementations 491 * may use one-way encoding mechanisms, so it will often not be possible to 492 * obtain the original clear-text password from its encoded representation. 493 * 494 * @param prefixedFormattedEncodedPassword 495 * The encoded password from which to extract the clear-text 496 * password. It must not be {@code null}, it must include the 497 * prefix, and any appropriate output formatting must have been 498 * applied. 499 * @param userEntry 500 * The entry in which the encoded password appears. It must not 501 * be {@code null}. 502 * 503 * @return The clear-text password used to generate the provided encoded 504 * representation. 505 * 506 * @throws LDAPException If this password encoder is not reversible, or if a 507 * problem occurs while trying to extract the 508 * clear-text representation from the provided encoded 509 * password. 510 */ 511 @NotNull() 512 public final ASN1OctetString extractClearPasswordFromEncodedPassword( 513 @NotNull final ASN1OctetString prefixedFormattedEncodedPassword, 514 @NotNull final ReadOnlyEntry userEntry) 515 throws LDAPException 516 { 517 // Strip the prefix off the encoded password. 518 final byte[] prefixedFormattedEncodedPasswordBytes = 519 prefixedFormattedEncodedPassword.getValue(); 520 if (! passwordStartsWithPrefix(prefixedFormattedEncodedPasswordBytes)) 521 { 522 throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM, 523 ERR_PW_ENCODER_PW_MATCHES_ENCODED_PW_MISSING_PREFIX.get( 524 getClass().getName(), prefix)); 525 } 526 527 final byte[] unPrefixedFormattedEncodedPasswordBytes = 528 new byte[prefixedFormattedEncodedPasswordBytes.length - 529 prefixBytes.length]; 530 System.arraycopy(prefixedFormattedEncodedPasswordBytes, prefixBytes.length, 531 unPrefixedFormattedEncodedPasswordBytes, 0, 532 unPrefixedFormattedEncodedPasswordBytes.length); 533 534 535 // If an output formatter is configured, then revert the output formatting. 536 final byte[] unPrefixedUnFormattedEncodedPasswordBytes; 537 if (outputFormatter == null) 538 { 539 unPrefixedUnFormattedEncodedPasswordBytes = 540 unPrefixedFormattedEncodedPasswordBytes; 541 } 542 else 543 { 544 unPrefixedUnFormattedEncodedPasswordBytes = 545 outputFormatter.unFormat(unPrefixedFormattedEncodedPasswordBytes); 546 } 547 548 549 // Try to extract the clear-text password. 550 final byte[] clearPasswordBytes = extractClearPassword( 551 unPrefixedUnFormattedEncodedPasswordBytes, userEntry); 552 return new ASN1OctetString(clearPasswordBytes); 553 } 554 555 556 557 /** 558 * Attempts to extract the clear-text password used to generate the provided 559 * encoded representation, if possible. Many password encoder implementations 560 * may use one-way encoding mechanisms, so it will often not be possible to 561 * obtain the original clear-text password from its encoded representation. 562 * 563 * @param unPrefixedUnFormattedEncodedPasswordBytes 564 * The bytes that comprise the encoded password, with the prefix 565 * stripped off and the output formatting reverted. 566 * @param userEntry 567 * The entry in which the encoded password appears. It must not 568 * be {@code null}. 569 * 570 * @return The clear-text password used to generate the provided encoded 571 * representation. 572 * 573 * @throws LDAPException If this password encoder is not reversible, or if a 574 * problem occurs while trying to extract the 575 * clear-text representation from the provided encoded 576 * password. 577 */ 578 @NotNull() 579 protected abstract byte[] extractClearPassword( 580 @NotNull byte[] unPrefixedUnFormattedEncodedPasswordBytes, 581 @NotNull ReadOnlyEntry userEntry) 582 throws LDAPException; 583 584 585 586 /** 587 * Indicates whether the provided password starts with the encoded password 588 * prefix. 589 * 590 * @param password The password for which to make the determination. 591 * 592 * @return {@code true} if the provided password starts with the encoded 593 * password prefix, or {@code false} if not. 594 */ 595 public final boolean passwordStartsWithPrefix( 596 @NotNull final ASN1OctetString password) 597 { 598 return passwordStartsWithPrefix(password.getValue()); 599 } 600 601 602 603 /** 604 * Indicates whether the provided byte array starts with the encoded password 605 * prefix. 606 * 607 * @param b The byte array for which to make the determination. 608 * 609 * @return {@code true} if the provided byte array starts with the encoded 610 * password prefix, or {@code false} if not. 611 */ 612 private boolean passwordStartsWithPrefix(@NotNull final byte[] b) 613 { 614 if (b.length < prefixBytes.length) 615 { 616 return false; 617 } 618 619 for (int i=0; i < prefixBytes.length; i++) 620 { 621 if (b[i] != prefixBytes[i]) 622 { 623 return false; 624 } 625 } 626 627 return true; 628 } 629 630 631 632 /** 633 * Retrieves a string representation of this password encoder. 634 * 635 * @return A string representation of this password encoder. 636 */ 637 @Override() 638 @NotNull() 639 public final String toString() 640 { 641 final StringBuilder buffer = new StringBuilder(); 642 toString(buffer); 643 return buffer.toString(); 644 } 645 646 647 648 /** 649 * Appends a string representation of this password encoder to the provided 650 * buffer. 651 * 652 * @param buffer The buffer to which the information should be appended. 653 */ 654 public abstract void toString(@NotNull StringBuilder buffer); 655}