001/*
002 * Copyright 2023 the original author or authors.
003 * <p>
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * <p>
008 * https://www.apache.org/licenses/LICENSE-2.0
009 * <p>
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package de.cuioss.tools.net.ssl;
017
018import static de.cuioss.tools.base.Preconditions.checkState;
019import static de.cuioss.tools.string.MoreStrings.isEmpty;
020import static java.util.Objects.requireNonNull;
021
022import java.io.BufferedInputStream;
023import java.io.ByteArrayInputStream;
024import java.io.File;
025import java.io.FileInputStream;
026import java.io.IOException;
027import java.io.InputStream;
028import java.io.Serializable;
029import java.security.KeyStore;
030import java.security.KeyStoreException;
031import java.security.NoSuchAlgorithmException;
032import java.security.cert.CertificateException;
033import java.security.cert.CertificateFactory;
034import java.util.Collection;
035import java.util.Optional;
036
037import de.cuioss.tools.base.BooleanOperations;
038import de.cuioss.tools.io.MorePaths;
039import de.cuioss.tools.logging.CuiLogger;
040import lombok.Builder;
041import lombok.EqualsAndHashCode;
042import lombok.Getter;
043import lombok.NonNull;
044import lombok.Singular;
045import lombok.ToString;
046
047/**
048 * Provides instances of {@link KeyStore} defined by either given file /
049 * storePassword combination or one or more {@link KeyMaterialHolder} containing
050 * key-material as a byte-array.
051 * <h2>Some words on the String-representation of passwords</h2> <em>No</em> it
052 * is not (much) more secure to store them in a char[] because of not being part
053 * of the string-pool:
054 * <ul>
055 * <li>If an attacker is on your machine debugging the string-pool you are
056 * doomed anyway.</li>
057 * <li>In most frameworks / user-land code there are some places where input /
058 * configuration data is represented as String on the way to the more secure
059 * "give me a char[]" parts. So it is usually in the String pool anyway.</li>
060 * </ul>
061 * <p>
062 * So: In theory the statements made by the Java Cryptography Architecture guide
063 * ("<a href=
064 * "http://docs.oracle.com/javase/6/docs/technotes/guides/security/crypto/CryptoSpec.html#PBEEx">...</a>")
065 * are correct but in our scenarios they will increase security only a small
066 * amount and introduce potential bugs and will therefore be ignored for this
067 * keyStoreType.
068 * </p>
069 * <p>
070 * It is more important to avoid accidental printing on logs and such, what is
071 * handled by this keyStoreType.
072 * </p>
073 * Therefore, this class uses String-based handling of credentials, for
074 * simplification and provide shortcuts for creating char[], see
075 * {@link #getStorePasswordAsCharArray()} and
076 * {@link #getKeyPasswordAsCharArray()}
077 *
078 * @author Oliver Wolff
079 * @author Nikola Marijan
080 *
081 */
082@Builder
083@EqualsAndHashCode(of = { "keyStoreType", "location" }, doNotUseGetters = true)
084@ToString(of = { "keyStoreType", "location" }, doNotUseGetters = true)
085public class KeyStoreProvider implements Serializable {
086
087    private static final String UNABLE_TO_CREATE_KEYSTORE = "The creation of a KeyStore did not succeed";
088    private static final String UNABLE_TO_CREATE_CERTIFICATE = "The creation of a Certificate-Object did not succeed";
089
090    private static final CuiLogger log = new CuiLogger(KeyStoreProvider.class);
091
092    private static final long serialVersionUID = 496381186621534386L;
093
094    @NonNull
095    @Getter
096    private final KeyStoreType keyStoreType;
097
098    @Getter
099    // We can not use Path here, because it is not Serializable
100    private final File location;
101
102    /** The password for the keystore aka the storage. */
103    @Getter
104    private final String storePassword;
105
106    /**
107     * (Optional) password for the keystore-key. Due to its nature this is usually
108     * only necessary for {@link KeyStoreType#KEY_STORE}
109     */
110    @Getter
111    private final String keyPassword;
112
113    @Getter
114    @Singular
115    private final Collection<KeyMaterialHolder> keys;
116
117    /**
118     * Instantiates a {@link KeyStore} according to the given parameter. In case of
119     * {@link #getKeys()} and {@link #getLocation()} being present the
120     * {@link KeyStore} will <em>only</em> be created from the {@link #getKeys()}.
121     * The file will be ignored.
122     *
123     * @return an {@link Optional} on a {@link KeyStore} created from the configured
124     *         parameter. In case of {@link #getKeys} and {@link #getLocation()}
125     *         being {@code null} / empty it will return {@link Optional#empty()}
126     * @throws IllegalStateException in case the location-file is not null but not
127     *                               readable or of the key-store creation did fail.
128     */
129    public Optional<KeyStore> resolveKeyStore() {
130        if (BooleanOperations.areAllTrue(keys.isEmpty(), null == location)) {
131            log.debug("Neither file nor keyMaterial provided, returning Optional#empty");
132            return Optional.empty();
133        }
134        if (null != location) {
135            log.debug("Checking whether configured {} path is readable", location.getAbsolutePath());
136            checkState(MorePaths.checkReadablePath(location.toPath(), false, true),
137                    "'%s' is not readable check logs for reason", location.getAbsolutePath());
138        }
139        if (!keys.isEmpty()) {
140            return retrieveFromKeys();
141        }
142        return retrieveFromFile();
143    }
144
145    private Optional<KeyStore> retrieveFromFile() {
146        log.debug("Retrieving java.security.KeyStore from configured file '{}'", location);
147        try (InputStream input = new BufferedInputStream(new FileInputStream(location))) {
148            var keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
149            keyStore.load(input, getStorePasswordAsCharArray());
150            return Optional.of(keyStore);
151        } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) {
152            throw new IllegalStateException(UNABLE_TO_CREATE_KEYSTORE, e);
153        }
154    }
155
156    private Optional<KeyStore> retrieveFromKeys() {
157        log.debug("Retrieving java.security.KeyStore from configured keys");
158        var keyStore = createEmptyKeyStore();
159        for (KeyMaterialHolder key : keys) {
160            log.debug("Adding Key {}", key);
161            requireNonNull(key);
162            switch (key.getKeyHolderType()) {
163            case SINGLE_KEY:
164                // adds single certificate to the keyStore
165                addCertificateToKeyStore(key, keyStore);
166                break;
167            case KEY_STORE:
168                checkState(keys.size() == 1, "It is not allowed that there are multiple KeyStores");
169                keyStore = createKeyStoreFromByteArray(key);
170                break;
171            default:
172                throw new UnsupportedOperationException("KeyHolderType is not defined: " + key.getKeyHolderType());
173            }
174        }
175        return Optional.of(keyStore);
176    }
177
178    private static void addCertificateToKeyStore(KeyMaterialHolder key, KeyStore keyStore) {
179        CertificateFactory cf;
180        try {
181            cf = CertificateFactory.getInstance("X.509");
182        } catch (CertificateException e) {
183            throw new IllegalStateException("Unable to instantiate CertificateFactory", e);
184        }
185
186        try (InputStream certStream = new ByteArrayInputStream(key.getKeyMaterial())) {
187            var cert = cf.generateCertificate(certStream);
188            keyStore.setCertificateEntry(key.getKeyAlias(), cert);
189        } catch (KeyStoreException | CertificateException | IOException e) {
190            throw new IllegalStateException(UNABLE_TO_CREATE_CERTIFICATE, e);
191        }
192    }
193
194    private KeyStore createKeyStoreFromByteArray(KeyMaterialHolder key) {
195        KeyStore keyStore;
196        try {
197            keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
198        } catch (KeyStoreException e) {
199            throw new IllegalStateException("Unable to instantiate KeyStore", e);
200        }
201        try (InputStream keyStoreStream = new ByteArrayInputStream(key.getKeyMaterial())) {
202            keyStore.load(keyStoreStream, getStorePasswordAsCharArray());
203            return keyStore;
204        } catch (NoSuchAlgorithmException | CertificateException | IOException e) {
205            throw new IllegalStateException(UNABLE_TO_CREATE_KEYSTORE, e);
206        }
207    }
208
209    private KeyStore createEmptyKeyStore() {
210        try {
211            var keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
212            keyStore.load(null, getStorePasswordAsCharArray());
213            return keyStore;
214        } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) {
215            throw new IllegalStateException(UNABLE_TO_CREATE_KEYSTORE, e);
216        }
217    }
218
219    /**
220     * @return NPE-safe char-array representation of {@link #getStorePassword()}. If
221     *         storePassword is {@code null} or empty it returns an empty char[],
222     *         never {@code null}
223     */
224    public char[] getStorePasswordAsCharArray() {
225        return toCharArray(storePassword);
226    }
227
228    /**
229     * @return NPE-safe char-array representation of {@link #getKeyPassword()}. If
230     *         keyPassword is {@code null} or empty it returns an empty char[],
231     *         never {@code null}
232     */
233    public char[] getKeyPasswordAsCharArray() {
234        return toCharArray(keyPassword);
235    }
236
237    /**
238     * In case of accessing data on the {@link KeyStore} sometimes it is needed to
239     * access the defined key-password. If not present the api needs the
240     * store-password instead. This is method is a convenience method for dealing
241     * with that case.
242     *
243     * @return the keyPassword, if set or the store-password otherwise
244     */
245    public char[] getKeyOrStorePassword() {
246        if (isEmpty(keyPassword)) {
247            return getStorePasswordAsCharArray();
248        }
249        return getKeyPasswordAsCharArray();
250    }
251
252    /**
253     * @param password to be converted. May be {@code null} or empty
254     * @return NPE-safe char-array representation of given password. If password is
255     *         {@code null} or empty it returns an empty char[], never {@code null}
256     */
257    static final char[] toCharArray(String password) {
258        if (isEmpty(password)) {
259            return new char[0];
260        }
261        return password.toCharArray();
262    }
263}