/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at * docs/licenses/cddl.txt * or http://www.opensource.org/licenses/cddl1.php. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at * docs/licenses/cddl.txt. If applicable, * add the following below this CDDL HEADER, with the fields enclosed * by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Portions Copyright 2010-2023 Ping Identity Corporation */ package com.unboundid.directory.sdk.examples; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import com.unboundid.asn1.ASN1OctetString; import com.unboundid.directory.sdk.ds.api.PasswordStorageScheme; import com.unboundid.directory.sdk.ds.config.PasswordStorageSchemeConfig; import com.unboundid.directory.sdk.ds.types.DirectoryServerContext; import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.ResultCode; import com.unboundid.util.Base64; import com.unboundid.util.ByteString; import com.unboundid.util.StaticUtils; import com.unboundid.util.ByteStringBuffer; import com.unboundid.util.args.ArgumentException; import com.unboundid.util.args.ArgumentParser; import com.unboundid.util.args.StringArgument; /** * This class provides a simple example of a password storage scheme that will * use a simple XOR to obscure the provided password. This implementation is * only intended to be used for example purposes, as it is not secure. It has * one configuration argument: * <UL> * <LI>key -- The key that will be XORed with the provided password in order * to generate the encoded password.</LI> * </UL> */ public final class ExamplePasswordStorageScheme extends PasswordStorageScheme { /** * The name of the argument that will be used as the key to XOR with the * provided password. */ private static final String ARG_NAME_KEY = "key"; // The key to XOR with the provided password. private volatile byte[] key; // The server context for the server in which this extension is running. private DirectoryServerContext serverContext; /** * Creates a new instance of this password storage scheme. All password * storage scheme implementations must include a default constructor, but any * initialization should generally be done in the * {@code initializePasswordStorageScheme} method. */ public ExamplePasswordStorageScheme() { // No implementation required. } /** * Retrieves a human-readable name for this extension. * * @return A human-readable name for this extension. */ @Override() public String getExtensionName() { return "Example Password Storage Scheme"; } /** * Retrieves a human-readable description for this extension. Each element * of the array that is returned will be considered a separate paragraph in * generated documentation. * * @return A human-readable description for this extension, or {@code null} * or an empty array if no description should be available. */ @Override() public String[] getExtensionDescription() { return new String[] { "This password storage scheme serves an example that may be used to " + "demonstrate the process for creating a third-party password " + "storage scheme. It will essentially XOR the provided clear-text " + "password with a given key. It is not secure and should not " + "actually be used in production environments." }; } /** * Updates the provided argument parser to define any configuration arguments * which may be used by this password generator. The argument parser may also * be updated to define relationships between arguments (e.g., to specify * required, exclusive, or dependent argument sets). * * @param parser The argument parser to be updated with the configuration * arguments which may be used by this password generator. * * @throws ArgumentException If a problem is encountered while updating the * provided argument parser. */ @Override() public void defineConfigArguments(final ArgumentParser parser) throws ArgumentException { // Add an argument that allows you to specify the key for the encoded // passwords. Character shortIdentifier = null; String longIdentifier = ARG_NAME_KEY; boolean required = true; int maxOccurrences = 1; String placeholder = "{key}"; String description = "The key to XOR with provided passwords in " + "order to generate the encoded representation."; parser.addArgument(new StringArgument(shortIdentifier, longIdentifier, required, maxOccurrences, placeholder, description)); } /** * Initializes this password storage scheme. * * @param serverContext A handle to the server context for the server in * which this extension is running. * @param config The general configuration for this password storage * scheme. * @param parser The argument parser which has been initialized from * the configuration for this password storage scheme. * * @throws LDAPException If a problem occurs while initializing this * password storage scheme. */ @Override() public void initializePasswordStorageScheme( final DirectoryServerContext serverContext, final PasswordStorageSchemeConfig config, final ArgumentParser parser) throws LDAPException { serverContext.debugInfo("Beginning password storage scheme initialization"); this.serverContext = serverContext; final StringArgument keyArg = (StringArgument) parser.getNamedArgument(ARG_NAME_KEY); key = StaticUtils.getBytes(keyArg.getValue()); } /** * Indicates whether the configuration contained in the provided argument * parser represents a valid configuration for this extension. * * @param config The general configuration for this password * storage scheme. * @param parser The argument parser which has been initialized * with the proposed configuration. * @param unacceptableReasons A list that can be updated with reasons that * the proposed configuration is not acceptable. * * @return {@code true} if the proposed configuration is acceptable, or * {@code false} if not. */ @Override() public boolean isConfigurationAcceptable( final PasswordStorageSchemeConfig config, final ArgumentParser parser, final List<String> unacceptableReasons) { boolean acceptable = true; // The key must not be empty. final StringArgument keyArg = (StringArgument) parser.getNamedArgument(ARG_NAME_KEY); final String keyStr = keyArg.getValue(); if ((keyStr == null) || (keyStr.length() == 0)) { acceptable = false; unacceptableReasons.add("The XOR key must not be empty."); } return acceptable; } /** * Attempts to apply the configuration contained in the provided argument * parser. * * @param config The general configuration for this password * storage scheme. * @param parser The argument parser which has been * initialized with the new configuration. * @param adminActionsRequired A list that can be updated with information * about any administrative actions that may be * required before one or more of the * configuration changes will be applied. * @param messages A list that can be updated with information * about the result of applying the new * configuration. * * @return A result code that provides information about the result of * attempting to apply the configuration change. */ @Override() public ResultCode applyConfiguration(final PasswordStorageSchemeConfig config, final ArgumentParser parser, final List<String> adminActionsRequired, final List<String> messages) { final StringArgument keyArg = (StringArgument) parser.getNamedArgument(ARG_NAME_KEY); key = StaticUtils.getBytes(keyArg.getValue()); return ResultCode.SUCCESS; } /** * Performs any cleanup which may be necessary when this password storage * scheme is to be taken out of service. */ @Override() public void finalizePasswordStorageScheme() { // No finalization is required. } /** * Retrieves the name for this password storage scheme. This will be the * identifier which appears in curly braces at the beginning of the encoded * password. The name should not include curly braces. * * @return The name for this password storage scheme. */ @Override() public String getStorageSchemeName() { return "XOR"; } /** * Indicates whether this password storage scheme encodes passwords in a form * that allows the original plaintext value to be obtained from the encoded * representation. * * @return {@code true} if the original plaintext password may be obtained * from the encoded password, or {@code false} if not. */ @Override() public boolean isReversible() { return true; } /** * Indicates whether this password storage scheme encodes passwords in a form * that may be considered secure. A storage scheme should only be considered * secure if it is not possible to trivially determine a clear-text value * which may be used to generate a given encoded representation. * * @return {@code true} if this password storage scheme may be considered * secure, or {@code false} if not. */ @Override() public boolean isSecure() { // This scheme is definitely not secure. return false; } /** * Encodes the provided plaintext password. The encoded password should not * include the scheme name in curly braces. * * @param plaintext The plaintext password to be encoded. It must not be * {@code null}. Note that there is no guarantee that * password validators have yet been invoked for this * password, so this password storage scheme implementation * should not make any assumptions about the format of the * plaintext password or whether it will actually be * allowed for use in the entry. * * @return The encoded representation of the provided password. * * @throws LDAPException If a problem occurs while attempting to encode the * password. */ @Override() public ByteString encodePassword(final ByteString plaintext) throws LDAPException { // Cache the key locally in case the configuration changes during // processing. final byte[] k = key; // Create a buffer to use to hold the encoded password. final ByteStringBuffer buffer = new ByteStringBuffer(); // First, append the number of bytes in the key followed by a null byte. buffer.append(k.length); buffer.append((byte) 0x00); // Next, append the key itself. This is massively insecure, but it will // protect against existing encoded passwords becoming invalid if the key // changes. buffer.append(k); // Finally, iterate through the bytes of the provided password and XOR them // with the bytes in the key, looping through the key multiple times if // necessary. final byte[] plaintextBytes = plaintext.getValue(); for (int i=0; i < plaintextBytes.length; i++) { final byte plaintextByte = plaintextBytes[i]; final byte keyByte = k[i % k.length]; buffer.append((byte) (plaintextByte ^ keyByte)); } // Base64-encode the contents of the buffer before returning it to make // sure it's safe to represent in ASCII. return new ASN1OctetString(Base64.encode(buffer.toByteArray())); } /** * Indicates whether the provided plaintext password could have been used to * generate the given encoded password. * * @param plaintext The plaintext password for which to make the * determination. * @param encoded The encoded password for which to make the * determination. It will not include the scheme name. * * @return {@code true} if the provided clear-text password could have been * used to generate the encoded password, or {@code false} if not. */ @Override() public boolean passwordMatches(final ByteString plaintext, final ByteString encoded) { final ByteString decoded; try { decoded = getPlaintextValue(encoded); } catch (final Exception e) { // The provided encoded password was not valid. serverContext.debugCaught(e); return false; } // We don't want to use ByteString.equals in this case because they // might refer to ASN.1 elements with identical values but different types. return Arrays.equals(decoded.getValue(), plaintext.getValue()); } /** * Attempts to determine the plaintext password used to generate the provided * encoded password. This method should only be called if the * {@link #isReversible} method returns {@code true}. * * @param encoded The encoded password for which to obtain the original * plaintext password. It must not be {@code null} and will * not be prefixed with the scheme name. * * @return The plaintext password obtained from the given encoded password. * * @throws LDAPException If this password storage scheme is not reversible, * or if the provided value could not be decoded to * its plaintext representation. */ @Override() public ByteString getPlaintextValue(final ByteString encoded) throws LDAPException { // First, base64-decode the provided value. final byte[] encodedBytes; try { encodedBytes = Base64.decode(encoded.stringValue()); } catch (final Exception e) { throw new LDAPException(ResultCode.OTHER, "Unable to base64-encode the provided password: " + StaticUtils.getExceptionMessage(e), e); } // Find the index of the first null byte. int nullPos = -1; for (int i=0; i < encodedBytes.length; i++) { if (encodedBytes[i] == (byte) 0x00) { nullPos = i; break; } } if (nullPos <= 0) { throw new LDAPException(ResultCode.OTHER, "The password value cannot be decoded."); } try { // Get the key length. final byte[] keyLengthBytes = new byte[nullPos]; System.arraycopy(encodedBytes, 0, keyLengthBytes, 0, nullPos); final int keyLength = Integer.parseInt(StaticUtils.toUTF8String(keyLengthBytes)); // Get the key data. final byte[] keyBytes = new byte[keyLength]; System.arraycopy(encodedBytes, nullPos+1, keyBytes, 0, keyLength); // Get the password XORed with the key. final byte[] pwBytes = new byte[encodedBytes.length - (nullPos + 1 + keyLength)]; System.arraycopy(encodedBytes, nullPos+1+keyLength, pwBytes, 0, pwBytes.length); // Iterate through the encoded password bytes and XOR them with the same // key to get back the original value. final byte[] clearPWBytes = new byte[pwBytes.length]; for (int i=0; i < pwBytes.length; i++) { final byte encodedPWByte = pwBytes[i]; final byte keyByte = keyBytes[i % keyLength]; clearPWBytes[i] = (byte) (encodedPWByte ^ keyByte); } return new ASN1OctetString(clearPWBytes); } catch (final Exception e) { serverContext.debugCaught(e); throw new LDAPException(ResultCode.OTHER, "An error occurred while attempting to decode the provided " + "password: " + StaticUtils.getExceptionMessage(e), e); } } /** * Indicates whether this password storage scheme provides the ability to * encode passwords in the authentication password syntax as described in RFC * 3112. * * @return {@code true} if this password storage scheme supports the * authentication password syntax, or {@code false} if not. */ @Override() public boolean supportsAuthPasswordSyntax() { return true; } /** * Retrieves the name that should be used to identify this password storage * scheme when encoding passwords using the authentication password syntax as * described in RFC 3112. This should only be used if the * {@link #supportsAuthPasswordSyntax} method returns {@code true}. * * @return The name that should be used to identify this password storage * scheme when encoding passwords using the authentication password * syntax. */ @Override() public String getAuthPasswordSchemeName() { return "xor"; } /** * Encodes the provided plaintext password using the authentication password * syntax as defined in RFC 3112. This should only be used if the * {@link #supportsAuthPasswordSyntax} method returns {@code true}. * * @param plaintext The plaintext password to be encoded. It must not be * {@code null}. Note that there is no guarantee that * password validators have yet been invoked for this * password, so this password storage scheme implementation * should not make any assumptions about the format of the * plaintext password or whether it will actually be * allowed for use in the entry. * * @return The encoded representation of the provided password. * * @throws LDAPException If a problem occurs while encoding the provided * password, or if this password storage scheme does * not support the authentication password syntax. */ @Override() public ByteString encodeAuthPassword(final ByteString plaintext) throws LDAPException { final byte[] keyBytes = key; final ByteStringBuffer buffer = new ByteStringBuffer(); buffer.append("xor$"); Base64.encode(keyBytes, buffer); buffer.append('$'); final byte[] plaintextBytes = plaintext.getValue(); final byte[] encodedBytes = new byte[plaintextBytes.length]; for (int i=0; i < plaintextBytes.length; i++) { final byte plaintextByte = plaintextBytes[i]; final byte keyByte = keyBytes[i % keyBytes.length]; encodedBytes[i] = (byte) (plaintextByte ^ keyByte); } Base64.encode(encodedBytes, buffer); return buffer.toByteString(); } /** * Indicates whether the provided plaintext password may be used to generate * an encoded password with the given authInfo and authValue elements when * using the authentication password syntax as defined in RFC 3112. This * should only be used if the {@link #supportsAuthPasswordSyntax} method * returns {@code true}. * * @param plaintext The plaintext password for which to make the * determination. * @param authInfo The authInfo portion of the encoded password for which * to make the determination. * @param authValue The authValue portion of the encoded password for which * to make the determination. * * @return {@code true} if the provided plaintext password could be used to * generate an encoded password with the given authInfo and authValue * portions, or {@code false} if not. */ @Override() public boolean authPasswordMatches(final ByteString plaintext, final String authInfo, final String authValue) { final ByteString decoded; try { decoded = getAuthPasswordPlaintextValue(authInfo, authValue); } catch (final Exception e) { // The provided encoded password was not valid. serverContext.debugCaught(e); return false; } // We don't want to use ByteString.equals in this case because they // might refer to ASN.1 elements with identical values but different types. return Arrays.equals(decoded.getValue(), plaintext.getValue()); } /** * Obtains the plaintext password that was used to generate an encoded * password with the given authInfo and authValue elements when using the * authentication password syntax as described in RFC 3112. This should only * be used if both the {@link #supportsAuthPasswordSyntax} and * {@link #isReversible} methods return {@code true}. * * @param authInfo The authInfo portion of the encoded password for which * to retrieve the corresponding plaintext value. * @param authValue The authValue portion of the encoded password for which * to retrieve the corresponding plaintext value. * * @return The plaintext password that was used to generate the encoded * password. * * @throws LDAPException If this password storage scheme is not reversible, * if this password storage scheme does not support * the authentication password syntax, or if some * other problem is encountered while attempting to * determine the plaintext password. */ @Override() public ByteString getAuthPasswordPlaintextValue(final String authInfo, final String authValue) throws LDAPException { // The authInfo will be the base64-encoded key. final byte[] keyBytes; try { keyBytes = Base64.decode(authInfo); } catch (final Exception e) { throw new LDAPException(ResultCode.OTHER, "Unable to base64-decode the authInfo portion of the encoded " + "password: " + StaticUtils.getExceptionMessage(e), e); } // The authValue will be a base64-encoded representation of the key XORed // with the original password. final byte[] encodedPWBytes; try { encodedPWBytes = Base64.decode(authValue); } catch (final Exception e) { throw new LDAPException(ResultCode.OTHER, "Unable to base64-decode the authValue portion of the encoded " + "password: " + StaticUtils.getExceptionMessage(e), e); } final byte[] clearPWBytes = new byte[encodedPWBytes.length]; for (int i=0; i < encodedPWBytes.length; i++) { final byte encodedPWByte = encodedPWBytes[i]; final byte keyByte = keyBytes[i % keyBytes.length]; clearPWBytes[i] = (byte) (encodedPWByte ^ keyByte); } return new ASN1OctetString(clearPWBytes); } /** * Retrieves a map containing examples of configurations that may be used for * this extension. The map key should be a list of sample arguments, and the * corresponding value should be a description of the behavior that will be * exhibited by the extension when used with that configuration. * * @return A map containing examples of configurations that may be used for * this extension. It may be {@code null} or empty if there should * not be any example argument sets. */ @Override() public Map<List<String>,String> getExamplesArgumentSets() { final LinkedHashMap<List<String>,String> exampleMap = new LinkedHashMap<List<String>,String>(1); exampleMap.put( Arrays.asList(ARG_NAME_KEY + "=secret"), "Encode user passwords by XORing them with the key 'secret'."); return exampleMap; } }