KeyStoreBuilder.java
/*
* Copyright 2023 Emmanuel Bourg
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.jsign;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.Provider;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static net.jsign.KeyStoreType.*;
/**
* Keystore builder.
*
* <p>Example:</p>
*
* <pre>
* KeyStore keystore = new KeyStoreBuilder().storetype(PKCS12).keystore("keystore.p12").storepass("password").build();
* </pre>
*
* @since 5.0
*/
public class KeyStoreBuilder {
/** The name used to refer to a configuration parameter */
private String parameterName = "parameter";
private String keystore;
private String storepass;
private KeyStoreType storetype;
private String keypass;
private File keyfile;
private File certfile;
/** The base directory to resolve the relative paths */
private File basedir = new File("empty").getParentFile();
private Provider provider;
public KeyStoreBuilder() {
}
KeyStoreBuilder(String parameterName) {
this.parameterName = parameterName;
}
String parameterName() {
return parameterName;
}
/**
* Sets the file containing the keystore.
*/
public KeyStoreBuilder keystore(File keystore) {
return keystore(keystore.getPath());
}
/**
* Sets the name of the resource containing the keystore. Either the path of the keystore file,
* the SunPKCS11 configuration file or the cloud keystore name depending on the type of keystore.
*/
public KeyStoreBuilder keystore(String keystore) {
this.keystore = keystore;
return this;
}
String keystore() {
return keystore;
}
/**
* Sets the password to access the keystore. The password can be loaded from a file by using the <code>file:</code>
* prefix followed by the path of the file, or from an environment variable by using the <code>env:</code> prefix
* followed by the name of the variable.
*/
public KeyStoreBuilder storepass(String storepass) {
this.storepass = storepass;
return this;
}
String storepass() {
storepass = readPassword("storepass", storepass);
return storepass;
}
/**
* Sets the type of the keystore.
*/
public KeyStoreBuilder storetype(KeyStoreType storetype) {
this.storetype = storetype;
return this;
}
/**
* Sets the type of the keystore.
*
* @param storetype the type of the keystore
* @throws IllegalArgumentException if the type is not recognized
*/
public KeyStoreBuilder storetype(String storetype) {
try {
this.storetype = storetype != null ? KeyStoreType.valueOf(storetype.toUpperCase()) : null;
} catch (IllegalArgumentException e) {
String expectedTypes = Stream.of(KeyStoreType.values())
.filter(type -> type != NONE).map(KeyStoreType::name)
.collect(Collectors.joining(", "));
throw new IllegalArgumentException("Unknown keystore type '" + storetype + "' (expected types: " + expectedTypes + ")");
}
return this;
}
KeyStoreType storetype() {
if (storetype == null) {
if (keystore == null) {
// no keystore specified, keyfile and certfile are expected
storetype = NONE;
} else {
// the keystore type wasn't specified, let's try to guess it
File file = createFile(keystore);
if (!file.isFile()) {
throw new IllegalArgumentException("Keystore file '" + keystore + "' not found");
}
storetype = KeyStoreType.of(file);
if (storetype == null) {
throw new IllegalArgumentException("Keystore type of '" + keystore + "' not recognized");
}
}
}
return storetype;
}
/**
* Sets the password to access the private key. The password can be loaded from a file by using the <code>file:</code>
* prefix followed by the path of the file, or from an environment variable by using the <code>env:</code> prefix
* followed by the name of the variable.
*/
public KeyStoreBuilder keypass(String keypass) {
this.keypass = keypass;
return this;
}
String keypass() {
keypass = readPassword("keypass", keypass);
return keypass;
}
/**
* Sets the file containing the private key.
*/
public KeyStoreBuilder keyfile(String keyfile) {
return keyfile(createFile(keyfile));
}
/**
* Sets the file containing the private key.
*/
public KeyStoreBuilder keyfile(File keyfile) {
this.keyfile = keyfile;
return this;
}
File keyfile() {
return keyfile;
}
/**
* Sets the file containing the certificate chain.
*/
public KeyStoreBuilder certfile(String certfile) {
return certfile(createFile(certfile));
}
/**
* Sets the file containing the certificate chain.
*/
public KeyStoreBuilder certfile(File certfile) {
this.certfile = certfile;
return this;
}
File certfile() {
return certfile;
}
void setBaseDir(File basedir) {
this.basedir = basedir;
}
File createFile(String file) {
if (file == null) {
return null;
}
if (new File(file).isAbsolute()) {
return new File(file);
} else {
return new File(basedir, file);
}
}
/**
* Read the password from the specified value. If the value is prefixed with <code>file:</code>
* the password is loaded from a file. If the value is prefixed with <code>env:</code> the password
* is loaded from an environment variable. Otherwise the value is returned as is.
*
* @param name the name of the parameter
* @param value the value to parse
*/
private String readPassword(String name, String value) {
if (value != null) {
if (value.startsWith("file:")) {
String filename = value.substring("file:".length());
Path path = createFile(filename).toPath();
try {
value = String.join("\n", Files.readAllLines(path, StandardCharsets.UTF_8)).trim();
} catch (IOException e) {
throw new IllegalArgumentException("Failed to read the " + name + " " + parameterName + " from the file '" + filename + "'", e);
}
} else if (value.startsWith("env:")) {
String variable = value.substring("env:".length());
if (!System.getenv().containsKey(variable)) {
throw new IllegalArgumentException("Failed to read the " + name + " " + parameterName + ", the '" + variable + "' environment variable is not defined");
}
value = System.getenv(variable);
}
}
return value;
}
/**
* Validates the parameters.
*/
void validate() throws IllegalArgumentException {
// keystore or keyfile, but not both
if (keystore != null && keyfile != null) {
throw new IllegalArgumentException("keystore " + parameterName + " can't be mixed with keyfile");
}
if (keystore == null && keyfile == null && certfile == null && storetype == null) {
throw new IllegalArgumentException("Either keystore, or keyfile and certfile, or storetype " + parameterName + "s must be set");
}
storetype().validate(this);
}
/**
* Returns the provider used to sign with the keystore.
*/
public Provider provider() {
if (provider == null) {
provider = storetype().getProvider(this);
}
return provider;
}
/**
* Builds the keystore.
*
* @throws IllegalArgumentException if the parameters are invalid
* @throws KeyStoreException if the keystore can't be loaded
*/
public KeyStore build() throws KeyStoreException {
validate();
return storetype().getKeystore(this, provider());
}
/**
* Returns a java.security.KeyStore.Builder using the parameters of this builder.
*/
public KeyStore.Builder builder() {
return new KeyStore.Builder() {
@Override
public KeyStore getKeyStore() throws KeyStoreException {
return build();
}
@Override
public KeyStore.ProtectionParameter getProtectionParameter(String alias) {
String storepass = storepass();
return new KeyStore.PasswordProtection(storepass != null ? storepass.toCharArray() : null);
}
};
}
}