![JAR search and dependency download from the Maven repository](/logo.png)
de.cuioss.tools.net.ssl.KeyStoreProvider Maven / Gradle / Ivy
/*
* Copyright 2023 the original author or authors.
*
* 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
*
* https://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 de.cuioss.tools.net.ssl;
import static de.cuioss.tools.base.Preconditions.checkState;
import static de.cuioss.tools.string.MoreStrings.isEmpty;
import static java.util.Objects.requireNonNull;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.Collection;
import java.util.Optional;
import de.cuioss.tools.base.BooleanOperations;
import de.cuioss.tools.io.MorePaths;
import de.cuioss.tools.logging.CuiLogger;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NonNull;
import lombok.Singular;
import lombok.ToString;
/**
* Provides instances of {@link KeyStore} defined by either given file /
* storePassword combination or one or more {@link KeyMaterialHolder} containing
* key-material as a byte-array.
*
Some words on the String-representation of passwords
No it
* is not (much) more secure to store them in a char[] because of not being part
* of the string-pool:
*
* - If an attacker is on your machine debugging the string-pool you are
* doomed anyway.
* - In most frameworks / user-land code there are some places where input /
* configuration data is represented as String on the way to the more secure
* "give me a char[]" parts. So it is usually in the String pool anyway.
*
*
* So: In theory the statements made by the Java Cryptography Architecture guide
* ("...")
* are correct but in our scenarios they will increase security only a small
* amount and introduce potential bugs and will therefore be ignored for this
* keyStoreType.
*
*
* It is more important to avoid accidental printing on logs and such, what is
* handled by this keyStoreType.
*
* Therefore, this class uses String-based handling of credentials, for
* simplification and provide shortcuts for creating char[], see
* {@link #getStorePasswordAsCharArray()} and
* {@link #getKeyPasswordAsCharArray()}
*
* @author Oliver Wolff
* @author Nikola Marijan
*
*/
@Builder
@EqualsAndHashCode(of = { "keyStoreType", "location" }, doNotUseGetters = true)
@ToString(of = { "keyStoreType", "location" }, doNotUseGetters = true)
public class KeyStoreProvider implements Serializable {
private static final String UNABLE_TO_CREATE_KEYSTORE = "The creation of a KeyStore did not succeed";
private static final String UNABLE_TO_CREATE_CERTIFICATE = "The creation of a Certificate-Object did not succeed";
private static final CuiLogger log = new CuiLogger(KeyStoreProvider.class);
private static final long serialVersionUID = 496381186621534386L;
@NonNull
@Getter
private final KeyStoreType keyStoreType;
@Getter
// We can not use Path here, because it is not Serializable
private final File location;
/** The password for the keystore aka the storage. */
@Getter
private final String storePassword;
/**
* (Optional) password for the keystore-key. Due to its nature this is usually
* only necessary for {@link KeyStoreType#KEY_STORE}
*/
@Getter
private final String keyPassword;
@Getter
@Singular
private final Collection keys;
/**
* Instantiates a {@link KeyStore} according to the given parameter. In case of
* {@link #getKeys()} and {@link #getLocation()} being present the
* {@link KeyStore} will only be created from the {@link #getKeys()}.
* The file will be ignored.
*
* @return an {@link Optional} on a {@link KeyStore} created from the configured
* parameter. In case of {@link #getKeys} and {@link #getLocation()}
* being {@code null} / empty it will return {@link Optional#empty()}
* @throws IllegalStateException in case the location-file is not null but not
* readable or of the key-store creation did fail.
*/
public Optional resolveKeyStore() {
if (BooleanOperations.areAllTrue(keys.isEmpty(), null == location)) {
log.debug("Neither file nor keyMaterial provided, returning Optional#empty");
return Optional.empty();
}
if (null != location) {
log.debug("Checking whether configured {} path is readable", location.getAbsolutePath());
checkState(MorePaths.checkReadablePath(location.toPath(), false, true),
"'%s' is not readable check logs for reason", location.getAbsolutePath());
}
if (!keys.isEmpty()) {
return retrieveFromKeys();
}
return retrieveFromFile();
}
private Optional retrieveFromFile() {
log.debug("Retrieving java.security.KeyStore from configured file '{}'", location);
try (InputStream input = new BufferedInputStream(new FileInputStream(location))) {
var keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(input, getStorePasswordAsCharArray());
return Optional.of(keyStore);
} catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) {
throw new IllegalStateException(UNABLE_TO_CREATE_KEYSTORE, e);
}
}
private Optional retrieveFromKeys() {
log.debug("Retrieving java.security.KeyStore from configured keys");
var keyStore = createEmptyKeyStore();
for (KeyMaterialHolder key : keys) {
log.debug("Adding Key {}", key);
requireNonNull(key);
switch (key.getKeyHolderType()) {
case SINGLE_KEY:
// adds single certificate to the keyStore
addCertificateToKeyStore(key, keyStore);
break;
case KEY_STORE:
checkState(keys.size() == 1, "It is not allowed that there are multiple KeyStores");
keyStore = createKeyStoreFromByteArray(key);
break;
default:
throw new UnsupportedOperationException("KeyHolderType is not defined: " + key.getKeyHolderType());
}
}
return Optional.of(keyStore);
}
private static void addCertificateToKeyStore(KeyMaterialHolder key, KeyStore keyStore) {
CertificateFactory cf;
try {
cf = CertificateFactory.getInstance("X.509");
} catch (CertificateException e) {
throw new IllegalStateException("Unable to instantiate CertificateFactory", e);
}
try (InputStream certStream = new ByteArrayInputStream(key.getKeyMaterial())) {
var cert = cf.generateCertificate(certStream);
keyStore.setCertificateEntry(key.getKeyAlias(), cert);
} catch (KeyStoreException | CertificateException | IOException e) {
throw new IllegalStateException(UNABLE_TO_CREATE_CERTIFICATE, e);
}
}
private KeyStore createKeyStoreFromByteArray(KeyMaterialHolder key) {
KeyStore keyStore;
try {
keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
} catch (KeyStoreException e) {
throw new IllegalStateException("Unable to instantiate KeyStore", e);
}
try (InputStream keyStoreStream = new ByteArrayInputStream(key.getKeyMaterial())) {
keyStore.load(keyStoreStream, getStorePasswordAsCharArray());
return keyStore;
} catch (NoSuchAlgorithmException | CertificateException | IOException e) {
throw new IllegalStateException(UNABLE_TO_CREATE_KEYSTORE, e);
}
}
private KeyStore createEmptyKeyStore() {
try {
var keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, getStorePasswordAsCharArray());
return keyStore;
} catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) {
throw new IllegalStateException(UNABLE_TO_CREATE_KEYSTORE, e);
}
}
/**
* @return NPE-safe char-array representation of {@link #getStorePassword()}. If
* storePassword is {@code null} or empty it returns an empty char[],
* never {@code null}
*/
public char[] getStorePasswordAsCharArray() {
return toCharArray(storePassword);
}
/**
* @return NPE-safe char-array representation of {@link #getKeyPassword()}. If
* keyPassword is {@code null} or empty it returns an empty char[],
* never {@code null}
*/
public char[] getKeyPasswordAsCharArray() {
return toCharArray(keyPassword);
}
/**
* In case of accessing data on the {@link KeyStore} sometimes it is needed to
* access the defined key-password. If not present the api needs the
* store-password instead. This is method is a convenience method for dealing
* with that case.
*
* @return the keyPassword, if set or the store-password otherwise
*/
public char[] getKeyOrStorePassword() {
if (isEmpty(keyPassword)) {
return getStorePasswordAsCharArray();
}
return getKeyPasswordAsCharArray();
}
/**
* @param password to be converted. May be {@code null} or empty
* @return NPE-safe char-array representation of given password. If password is
* {@code null} or empty it returns an empty char[], never {@code null}
*/
static final char[] toCharArray(String password) {
if (isEmpty(password)) {
return new char[0];
}
return password.toCharArray();
}
}