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.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}