/* * 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 2011-2024 Ping Identity Corporation */ package com.unboundid.directory.sdk.examples; import java.io.InputStream; import java.io.OutputStream; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.crypto.Cipher; import javax.crypto.CipherInputStream; import javax.crypto.CipherOutputStream; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; import com.unboundid.directory.sdk.ds.api.CipherStreamProvider; import com.unboundid.directory.sdk.ds.config.CipherStreamProviderConfig; import com.unboundid.directory.sdk.ds.types.DirectoryServerContext; import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.ResultCode; import com.unboundid.util.StaticUtils; 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 cipher stream provider which will * use a password (provided as an argument to this extension) to generate the * key to use in order to perform encryption and decryption using a 128-bit AES * cipher. It has a single configuration argument: * <UL> * <LI>key-password -- The password to use to generate the encryption * key.</LI> * </UL> */ public final class ExampleCipherStreamProvider extends CipherStreamProvider { /** * The name of the argument that will be used to specify the password to use * when generating the encryption key. */ private static final String ARG_NAME_KEY_PASSWORD = "key-password"; // The characters that comprise the key password. private volatile char[] keyPassword; // The server context for the server in which this extension is running. private DirectoryServerContext serverContext; /** * Creates a new instance of this cipher stream provider. All cipher stream * provider implementations must include a default constructor, but any * initialization should generally be done in the * {@code initializeCipherStreamProvider} method. */ public ExampleCipherStreamProvider() { // 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 Cipher Stream Provider"; } /** * 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 cipher stream provider uses a password provided in the " + "key-password argument in order to generate the encryption key " + "to use for encrypting and decrypting data using a 128-bit AES " + "cipher." }; } /** * Updates the provided argument parser to define any configuration arguments * which may be used by this cipher stream provider. 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 cipher stream provider. * * @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 subject attribute. final Character shortIdentifier = null; final String longIdentifier = ARG_NAME_KEY_PASSWORD; final boolean required = true; final int maxOccurrences = 1; final String placeholder = "{password}"; final String description = "The password to use to generate the " + "key to use for performing 128-bit AES encryption and decryption."; parser.addArgument(new StringArgument(shortIdentifier, longIdentifier, required, maxOccurrences, placeholder, description)); } /** * Initializes this cipher stream provider. * * @param serverContext A handle to the server context for the server in * which this extension is running. * @param config The general configuration for this cipher stream * provider. * @param parser The argument parser which has been initialized from * the configuration for this cipher stream provider. * * @throws LDAPException If a problem occurs while initializing this cipher * stream provider. */ @Override() public void initializeCipherStreamProvider( final DirectoryServerContext serverContext, final CipherStreamProviderConfig config, final ArgumentParser parser) throws LDAPException { serverContext.debugInfo("Beginning cipher stream provider initialization"); this.serverContext = serverContext; // Get the key password. final StringArgument pwArg = (StringArgument) parser.getNamedArgument(ARG_NAME_KEY_PASSWORD); keyPassword = pwArg.getValue().toCharArray(); } /** * Indicates whether the configuration contained in the provided argument * parser represents a valid configuration for this extension. * * @param config The general configuration for this cipher * stream provider. * @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 CipherStreamProviderConfig config, final ArgumentParser parser, final List<String> unacceptableReasons) { // This is a special case, because we don't want the password to be changed // once it is set. If the password is changed in the same configuration // entry (rather than defining a new configuration entry with the new // password), then it may no longer be possible to decrypt any data that was // previously encrypted with the old key. As a result, we will only allow // the password change if there is no current configuration (which would be // the case when first initializing this cipher stream provider), or if the // new configuration has the same password as the previous one. if (keyPassword == null) { return true; } final StringArgument pwArg = (StringArgument) parser.getNamedArgument(ARG_NAME_KEY_PASSWORD); final String newPW = pwArg.getValue(); if (newPW.equals(new String(keyPassword))) { return true; } else { unacceptableReasons.add("The password used for the cipher stream " + "provider must not be changed once it has been set, because it " + "can prevent previously-encrypted data from being decrypted. " + "Instead, a new cipher stream provider instance should be " + "created and configured for use."); return false; } } /** * Attempts to apply the configuration contained in the provided argument * parser. * * @param config The general configuration for this cipher * stream provider. * @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 CipherStreamProviderConfig config, final ArgumentParser parser, final List<String> adminActionsRequired, final List<String> messages) { // We will not apply any configuration change, so just return success // without doing anything. return ResultCode.SUCCESS; } /** * Performs any cleanup which may be necessary when this cipher stream * provider is to be taken out of service. */ @Override() public void finalizeCipherStreamProvider() { // No finalization is required. } /** * Wraps the provided input stream in a cipher input stream that can be used * to decrypt data read from the given stream. * * @param source The input stream to be wrapped with a cipher input stream. * * @return The cipher input stream which wraps the provided input stream. * * @throws LDAPException If a problem occurs while creating the cipher input * stream. */ @Override() public CipherInputStream createCipherInputStream(final InputStream source) throws LDAPException { final Cipher cipher; try { final SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); final PBEKeySpec keySpec = new PBEKeySpec(keyPassword, new byte[8], 1024, 128); final SecretKey encryptionKey = new SecretKeySpec(keyFactory.generateSecret(keySpec).getEncoded(), "AES"); cipher = Cipher.getInstance("AES"); cipher.init(Cipher.DECRYPT_MODE, encryptionKey); } catch (final Exception e) { serverContext.debugCaught(e); throw new LDAPException(ResultCode.OTHER, "An error occurred while attempting to create the encryption " + "cipher: " + StaticUtils.getExceptionMessage(e), e); } return new CipherInputStream(source, cipher); } /** * Wraps the provided output stream in a cipher output stream that can be used * to encrypt data written to the given stream. * * @param target The output stream to be wrapped with a cipher output * stream. * * @return The cipher output stream which wraps the provided output stream. * * @throws LDAPException If a problem occurs while creating the cipher * output stream. */ @Override() public CipherOutputStream createCipherOutputStream(final OutputStream target) throws LDAPException { final Cipher cipher; try { final SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); final PBEKeySpec keySpec = new PBEKeySpec(keyPassword, new byte[8], 1024, 128); final SecretKey encryptionKey = new SecretKeySpec(keyFactory.generateSecret(keySpec).getEncoded(), "AES"); cipher = Cipher.getInstance("AES"); cipher.init(Cipher.ENCRYPT_MODE, encryptionKey); } catch (final Exception e) { serverContext.debugCaught(e); throw new LDAPException(ResultCode.OTHER, "An error occurred while attempting to create the decryption " + "cipher: " + StaticUtils.getExceptionMessage(e), e); } return new CipherOutputStream(target, cipher); } /** * 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_PASSWORD + "=secret123"), "Use the password 'secret123' to generate the key to use when " + "encrypting and decrypting data using a 128-bit AES cipher."); return exampleMap; } }