KeyStoreConfiguration.java
/*
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2017 ForgeRock AS.
*/
package org.forgerock.security.keystore;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Holds the configuration required for initializing a Keystore. This includes things
* like the keystore type (JKS, JCEKS), paths to keystore files, the Keystore provider class, etc.
* <p>
* This class is usually de-serialized from json using Jackson. It is immutable once created.
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class KeyStoreConfiguration {
private String keyStorePasswordFile;
private String keyPasswordFile;
private String keyStoreType;
private String keyStoreFile;
private String providerClass;
private String providerArg;
private String providerName;
private Map<String, Object> parameters;
private static final String[] NEWLINE_TYPES = {
"\r\n",
"\n",
};
// Private no arg constructor is provided for Jackson
private KeyStoreConfiguration() {
}
/**
* Create an Immutable KeyStoreConfiguration that holds keystore configuration parameters.
* Note that depending on the KeyStoreProvider, some or all of these arguments may be
* optional, in which case you can pass null.
* <p>
* Creating instances via json de-serialization is the recommended approach.
*
* @param keyStorePasswordFile path name of the .storepass file
* @param keyPasswordFile path name of the .keypass file
* @param keyStoreType The type of keystore (JKS, JCEKS, etc.)
* @param keyStoreFile The path name of the keystore file ( keystore.jceks)
* @param providerClass The name of the KeyStoreProvider class (org.acme.CustomKeystoreProvider)
* @param providerArg Optional argument used to instantiate a KeyStoreProvider. The interpretation
* is left to the provider
* @param providerName The name of the registered keystore provider instance ("LDAP", "JKS", etc.)
* @param parameters optional key/value map used to create loadstore parameters for keystore
* initialization
*/
public KeyStoreConfiguration(String keyStorePasswordFile,
String keyPasswordFile,
String keyStoreType,
String keyStoreFile,
String providerClass,
String providerArg,
String providerName,
Map<String, Object> parameters) {
this.keyStorePasswordFile = keyStorePasswordFile;
this.keyPasswordFile = keyPasswordFile;
this.keyStoreType = keyStoreType;
this.keyStoreFile = keyStoreFile;
this.providerClass = providerClass;
this.providerArg = providerArg;
this.providerName = providerName;
this.parameters = parameters;
}
/**
* The provider string name (LDAP, JKS, etc.).
*
* @return The provider name as a string
*/
public String getProviderName() {
return providerName;
}
/**
* Get the path to the file that contains the password/pin used to unlock the keystore.
*
* @return Path to file that holds the password to unlock the keystore
*/
public String getKeyStorePasswordFile() {
return keyStorePasswordFile;
}
/**
* Get the path to file that holds the password to unlock individual keys. Frequently not used.
*
* @return path to file that contains password to unlock individual key entries
*/
public String getKeyPasswordFile() {
return keyPasswordFile;
}
/**
* Get the keystore type.
*
* @return the keystore type (JKS, JCEKS , etc. )
*/
public String getKeyStoreType() {
return keyStoreType;
}
/**
* Get the path to the keystore.
*
* @return The keystore file (example: /tmp/keystore.jceks )
*/
public String getKeyStoreFile() {
return keyStoreFile;
}
/**
* Get the provider class name string. This is optional for providers.
*
* @return The provider class
*/
public String getProviderClass() {
return providerClass;
}
/**
* Get the provider generic argument as a string. The Java keystore SPI provides
* a single string argument that the provider interprets as it wishes.
*
* @return optional provider argument
*/
public String getProviderArg() {
return providerArg;
}
/**
* Get the optional parameter map used to initialize a keystore. This is
* for providers that support a LoadStoreParameter argument.
*
* @return parameter map used to configure the keystore.
*/
public Map<String, Object> getParameters() {
return parameters;
}
/**
* Get the keystore password. This results in the store password file being opened and read into
* memory.
*
* <p>If the keystore password file ends with a new line, the new line is automatically stripped
* from the password before it is returned. This ensures that a password file edited with a
* POSIX-compliant editor continues to function as it did before editing. Note that only the
* last, platform-dependent newline is stripped from the password; whitespace or multiple
* newlines at the end of the password file are left intact.
*
* @param pathPrefix
* The path prefix where files will be opened relative to. This can be null or "" in which
* case the current directory is assumed. This will not be applied to any files that start
* with a file separator.
*
* @return The keystore password as a char array
*
* @throws IOException
* If the keystore password file cannot be opened
*/
@JsonIgnore
public char[] getKeyStorePassword(final String pathPrefix) throws IOException {
final String fileName = prefix(pathPrefix, getKeyStorePasswordFile()),
fileContents = new String(Files.readAllBytes(Paths.get(fileName)), UTF_8);
String trimmedContents = fileContents;
for (String newLine : NEWLINE_TYPES) {
trimmedContents = trimFromEnd(trimmedContents, newLine);
if (!trimmedContents.equals(fileContents)) {
// Only replace a single newline
break;
}
}
return trimmedContents.toCharArray();
}
/**
* Get the key password used to unlock individual key entries.The results in the
* file being opened and read into memory.
*
* @param pathPrefix The path prefix where files will be opened relative to. This can be null or ""
* in which case the current directory is assumed. This will not be applied
* to any files that start with a file separator.
* @return The key password as a char array
* @throws IOException If the key password file can not be opened
*/
@JsonIgnore
public char[] getKeyPassword(final String pathPrefix) throws IOException {
String fileName = prefix(pathPrefix, getKeyPasswordFile());
return new String(Files.readAllBytes(Paths.get(fileName)), UTF_8).toCharArray();
}
/**
* Initialize and load the keystore described by this configuration
*
* There are a number of possible exceptions that can be generated - they are consolidated
* to a single KeyStoreException and the underlying exception is wrapped. If dynamic
* classloading is required to load the KeyStoreProvider, the current ClassLoader is used.
*
* @param pathPrefix The path prefix where files will be opened relative to. This can be null or ""
* in which case the current directory is assumed. This will not be applied
* to any files that start with a file separator.
* @return the opened KeyStore
* @throws KeyStoreException if the keystore can not be opened or initialized.
*/
public KeyStore loadKeyStore(final String pathPrefix) throws KeyStoreException {
return loadKeyStore(pathPrefix, Thread.currentThread().getContextClassLoader());
}
/**
* Initialize and load the keystore described by this configuration
*
* There are a number of possible exceptions that can be generated - they are consolidated
* to a single KeyStoreException and the underlying exception is wrapped.
*
* @param pathPrefix The path prefix where files will be opened relative to. This can be null or ""
* in which case the current directory is assumed. This will not be applied
* to any files that start with a file separator.
* @param classLoader The classloader to use for dynamic classloading of the KeyStore Provider
* @return the opened KeyStore
* @throws KeyStoreException if the keystore can not be opened or initialized.
*/
public KeyStore loadKeyStore(final String pathPrefix, final ClassLoader classLoader) throws KeyStoreException {
KeyStore ks = null;
try {
KeyStoreBuilder builder = new KeyStoreBuilder();
String file = getKeyStoreFile();
// If keystore file is supplied, this is a keystore on disk type of keystore...
if (file != null) {
builder = builder.withKeyStoreFile(prefix(pathPrefix, file))
.withKeyStoreType(getKeyStoreType())
.withPassword(getKeyStorePassword(pathPrefix));
} else if (getProviderClass() != null) {
builder = builder
.withProviderClass(getProviderClass(), classLoader)
.withKeyStoreType(getKeyStoreType());
if (getProviderArg() != null) {
builder = builder.withProviderArgument(getProviderArg());
}
if (getParameters() != null) {
final MapKeyStoreParameters params = new MapKeyStoreParameters(getParameters());
builder = builder.withLoadStoreParameter(params);
}
} else {
throw new KeyStoreException("Could not initialize keystore - the configuration file is incomplete");
}
ks = builder.build();
} catch (IOException e) {
throw new KeyStoreException("Could not initialize the Keystore. Cause: " + e.getMessage(), e);
}
return ks;
}
/**
* Apply the prefix to the string.
*
* @param pathPrefix The path prefix where files will be opened relative to.
* @param file name string
* @return the file name prefixed by that path prefix. If the file name starts with /, it is not modified
*/
private static String prefix(final String pathPrefix, final String file) {
if (pathPrefix == null || pathPrefix.equals("") || file.startsWith(File.separator)) {
return file;
}
return pathPrefix + (pathPrefix.endsWith(File.separator) ? "" : File.separator) + file;
}
/**
* Trims the specified pattern from the end of the provided value.
*
* <p>If the provided value does not end with the specified pattern, the value is returned
* as-is.
*
* @param value
* The value that needs trimming.
* @param pattern
* The pattern to remove from the end of the value, if it exists.
*
* @return
* The trimmed string (if the pattern was present); or the original string (if the pattern was
* not present).
*/
private static String trimFromEnd(final String value, final String pattern) {
final String trimmedValue;
if (value.endsWith(pattern)) {
final int valueLength = value.length(),
patternLength = pattern.length(),
trimmedLength = valueLength - patternLength;
trimmedValue = value.substring(0, trimmedLength);
} else {
trimmedValue = value;
}
return trimmedValue;
}
}