001// Generated by delombok at Sat Feb 25 17:58:34 UTC 2023
002package io.cui.tools.net.ssl;
003
004import static io.cui.tools.base.Preconditions.checkState;
005import static io.cui.tools.string.MoreStrings.isEmpty;
006import static java.util.Objects.requireNonNull;
007import java.io.BufferedInputStream;
008import java.io.ByteArrayInputStream;
009import java.io.File;
010import java.io.FileInputStream;
011import java.io.IOException;
012import java.io.InputStream;
013import java.io.Serializable;
014import java.security.KeyStore;
015import java.security.KeyStoreException;
016import java.security.NoSuchAlgorithmException;
017import java.security.cert.CertificateException;
018import java.security.cert.CertificateFactory;
019import java.util.Collection;
020import java.util.Optional;
021import io.cui.tools.base.BooleanOperations;
022import io.cui.tools.io.MorePaths;
023import io.cui.tools.logging.CuiLogger;
024import lombok.NonNull;
025
026/**
027 * Provides instances of {@link KeyStore} defined by either given file / storePassword combination
028 * or one or more {@link KeyMaterialHolder} containing key-material as a byte-array.
029 * <h2>Some words on the String-representation of passwords</h2>
030 * <em>No</em> it is not (much) more secure to store them in a char[] because of not being part of
031 * the string-pool:
032 * <ul>
033 * <li>If an attacker is on your machine debugging the string-pool you are doomed anyway.</li>
034 * <li>In most frameworks / user-land code there are some places where input / configuration data is
035 * represented as String on the way to the more secure "give me a char[]" parts. So it is usually in
036 * the String pool anyway.</li>
037 * </ul>
038 * <p>
039 * So: In theory the statements made by the Java Cryptography Architecture guide
040 * ("<a href=
041 * "http://docs.oracle.com/javase/6/docs/technotes/guides/security/crypto/CryptoSpec.html#PBEEx">...</a>")
042 * are correct but in our scenarios they will increase security only a small amount and introduce
043 * potential bugs and will therefore be ignored for this keyStoreType.
044 * </p>
045 * <p>
046 * It is more important to avoid accidental printing on logs and such, what is handled by this
047 * keyStoreType.
048 * </p>
049 * Therefore, this class uses String-based handling of credentials, for simplification and provide
050 * shortcuts for creating char[], see {@link #getStorePasswordAsCharArray()} and
051 * {@link #getKeyPasswordAsCharArray()}
052 *
053 * @author Oliver Wolff
054 * @author Nikola Marijan
055 */
056public class KeyStoreProvider implements Serializable {
057    private static final String UNABLE_TO_CREATE_KEYSTORE = "The creation of a KeyStore did not succeed";
058    private static final String UNABLE_TO_CREATE_CERTIFICATE = "The creation of a Certificate-Object did not succeed";
059    private static final CuiLogger log = new CuiLogger(KeyStoreProvider.class);
060    private static final long serialVersionUID = 496381186621534386L;
061    @NonNull
062    private final KeyStoreType keyStoreType;
063    // We can not use Path here, because it is not Serializable
064    private final File location;
065    /**
066     * The password for the keystore aka the storage.
067     */
068    private final String storePassword;
069    /**
070     * (Optional) password for the keystore-key. Due to its nature this is usually only necessary
071     * for {@link KeyStoreType#KEY_STORE}
072     */
073    private final String keyPassword;
074    private final Collection<KeyMaterialHolder> keys;
075
076    /**
077     * Instantiates a {@link KeyStore} according to the given parameter. In case of
078     * {@link #getKeys()} and {@link #getLocation()} being present the {@link KeyStore} will
079     * <em>only</em> be created from the {@link #getKeys()}. The file will be ignored.
080     *
081     * @return an {@link Optional} on a {@link KeyStore} created from the configured parameter. In
082     *         case of {@link #getKeys} and {@link #getLocation()} being {@code null} / empty it
083     *         will return {@link Optional#empty()}
084     * @throws IllegalStateException in case the location-file is not null but not readable or of
085     *             the key-store creation did fail.
086     */
087    public Optional<KeyStore> resolveKeyStore() {
088        if (BooleanOperations.areAllTrue(keys.isEmpty(), null == location)) {
089            log.debug("Neither file nor keyMaterial provided, returning Optional#empty");
090            return Optional.empty();
091        }
092        if (null != location) {
093            log.debug("Checking whether configured {} path is readable", location.getAbsolutePath());
094            checkState(MorePaths.checkReadablePath(location.toPath(), false, true), "\'%s\' is not readable check logs for reason", location.getAbsolutePath());
095        }
096        if (!keys.isEmpty()) {
097            return retrieveFromKeys();
098        }
099        return retrieveFromFile();
100    }
101
102    private Optional<KeyStore> retrieveFromFile() {
103        log.debug("Retrieving java.security.KeyStore from configured file \'{}\'", location);
104        try (InputStream input = new BufferedInputStream(new FileInputStream(location))) {
105            var keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
106            keyStore.load(input, getStorePasswordAsCharArray());
107            return Optional.of(keyStore);
108        } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) {
109            throw new IllegalStateException(UNABLE_TO_CREATE_KEYSTORE, e);
110        }
111    }
112
113    private Optional<KeyStore> retrieveFromKeys() {
114        log.debug("Retrieving java.security.KeyStore from configured keys");
115        var keyStore = createEmptyKeyStore();
116        for (KeyMaterialHolder key : keys) {
117            log.debug("Adding Key {}", key);
118            requireNonNull(key);
119            switch (key.getKeyHolderType()) {
120            case SINGLE_KEY: 
121                // adds single certificate to the keyStore
122                addCertificateToKeyStore(key, keyStore);
123                break;
124            case KEY_STORE: 
125                checkState(keys.size() == 1, "It is not allowed that there are multiple KeyStores");
126                keyStore = createKeyStoreFromByteArray(key);
127                break;
128            default: 
129                throw new UnsupportedOperationException("KeyHolderType is not defined: " + key.getKeyHolderType());
130            }
131        }
132        return Optional.of(keyStore);
133    }
134
135    private static void addCertificateToKeyStore(KeyMaterialHolder key, KeyStore keyStore) {
136        CertificateFactory cf;
137        try {
138            cf = CertificateFactory.getInstance("X.509");
139        } catch (CertificateException e) {
140            throw new IllegalStateException("Unable to instantiate CertificateFactory", e);
141        }
142        try (InputStream certStream = new ByteArrayInputStream(key.getKeyMaterial())) {
143            var cert = cf.generateCertificate(certStream);
144            keyStore.setCertificateEntry(key.getKeyAlias(), cert);
145        } catch (KeyStoreException | CertificateException | IOException e) {
146            throw new IllegalStateException(UNABLE_TO_CREATE_CERTIFICATE, e);
147        }
148    }
149
150    private KeyStore createKeyStoreFromByteArray(KeyMaterialHolder key) {
151        KeyStore keyStore;
152        try {
153            keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
154        } catch (KeyStoreException e) {
155            throw new IllegalStateException("Unable to instantiate KeyStore", e);
156        }
157        try (InputStream keyStoreStream = new ByteArrayInputStream(key.getKeyMaterial())) {
158            keyStore.load(keyStoreStream, getStorePasswordAsCharArray());
159            return keyStore;
160        } catch (NoSuchAlgorithmException | CertificateException | IOException e) {
161            throw new IllegalStateException(UNABLE_TO_CREATE_KEYSTORE, e);
162        }
163    }
164
165    private KeyStore createEmptyKeyStore() {
166        try {
167            var keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
168            keyStore.load(null, getStorePasswordAsCharArray());
169            return keyStore;
170        } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) {
171            throw new IllegalStateException(UNABLE_TO_CREATE_KEYSTORE, e);
172        }
173    }
174
175    /**
176     * @return NPE-safe char-array representation of {@link #getStorePassword()}. If storePassword
177     *         is {@code null} or empty it returns an empty char[], never {@code null}
178     */
179    public char[] getStorePasswordAsCharArray() {
180        return toCharArray(storePassword);
181    }
182
183    /**
184     * @return NPE-safe char-array representation of {@link #getKeyPassword()}. If keyPassword is
185     *         {@code null} or empty it returns an empty char[], never {@code null}
186     */
187    public char[] getKeyPasswordAsCharArray() {
188        return toCharArray(keyPassword);
189    }
190
191    /**
192     * In case of accessing data on the {@link KeyStore} sometimes it is needed to access the
193     * defined key-password. If not present the api needs the store-password instead. This is method
194     * is a convenience method for dealing with that case.
195     *
196     * @return the keyPassword, if set or the store-password otherwise
197     */
198    public char[] getKeyOrStorePassword() {
199        if (isEmpty(keyPassword)) {
200            return getStorePasswordAsCharArray();
201        }
202        return getKeyPasswordAsCharArray();
203    }
204
205    /**
206     * @param password to be converted. May be {@code null} or empty
207     * @return NPE-safe char-array representation of given password. If password is
208     *         {@code null} or empty it returns an empty char[], never {@code null}
209     */
210    static final char[] toCharArray(String password) {
211        if (isEmpty(password)) {
212            return new char[0];
213        }
214        return password.toCharArray();
215    }
216
217    @java.lang.SuppressWarnings("all")
218    @lombok.Generated
219    KeyStoreProvider(@NonNull final KeyStoreType keyStoreType, final File location, final String storePassword, final String keyPassword, final Collection<KeyMaterialHolder> keys) {
220        if (keyStoreType == null) {
221            throw new java.lang.NullPointerException("keyStoreType is marked non-null but is null");
222        }
223        this.keyStoreType = keyStoreType;
224        this.location = location;
225        this.storePassword = storePassword;
226        this.keyPassword = keyPassword;
227        this.keys = keys;
228    }
229
230
231    @java.lang.SuppressWarnings("all")
232    @lombok.Generated
233    public static class KeyStoreProviderBuilder {
234        @java.lang.SuppressWarnings("all")
235        @lombok.Generated
236        private KeyStoreType keyStoreType;
237        @java.lang.SuppressWarnings("all")
238        @lombok.Generated
239        private File location;
240        @java.lang.SuppressWarnings("all")
241        @lombok.Generated
242        private String storePassword;
243        @java.lang.SuppressWarnings("all")
244        @lombok.Generated
245        private String keyPassword;
246        @java.lang.SuppressWarnings("all")
247        @lombok.Generated
248        private java.util.ArrayList<KeyMaterialHolder> keys;
249
250        @java.lang.SuppressWarnings("all")
251        @lombok.Generated
252        KeyStoreProviderBuilder() {
253        }
254
255        /**
256         * @return {@code this}.
257         */
258        @java.lang.SuppressWarnings("all")
259        @lombok.Generated
260        public KeyStoreProvider.KeyStoreProviderBuilder keyStoreType(@NonNull final KeyStoreType keyStoreType) {
261            if (keyStoreType == null) {
262                throw new java.lang.NullPointerException("keyStoreType is marked non-null but is null");
263            }
264            this.keyStoreType = keyStoreType;
265            return this;
266        }
267
268        /**
269         * @return {@code this}.
270         */
271        @java.lang.SuppressWarnings("all")
272        @lombok.Generated
273        public KeyStoreProvider.KeyStoreProviderBuilder location(final File location) {
274            this.location = location;
275            return this;
276        }
277
278        /**
279         * The password for the keystore aka the storage.
280         * @return {@code this}.
281         */
282        @java.lang.SuppressWarnings("all")
283        @lombok.Generated
284        public KeyStoreProvider.KeyStoreProviderBuilder storePassword(final String storePassword) {
285            this.storePassword = storePassword;
286            return this;
287        }
288
289        /**
290         * (Optional) password for the keystore-key. Due to its nature this is usually only necessary
291         * for {@link KeyStoreType#KEY_STORE}
292         * @return {@code this}.
293         */
294        @java.lang.SuppressWarnings("all")
295        @lombok.Generated
296        public KeyStoreProvider.KeyStoreProviderBuilder keyPassword(final String keyPassword) {
297            this.keyPassword = keyPassword;
298            return this;
299        }
300
301        @java.lang.SuppressWarnings("all")
302        @lombok.Generated
303        public KeyStoreProvider.KeyStoreProviderBuilder key(final KeyMaterialHolder key) {
304            if (this.keys == null) this.keys = new java.util.ArrayList<KeyMaterialHolder>();
305            this.keys.add(key);
306            return this;
307        }
308
309        @java.lang.SuppressWarnings("all")
310        @lombok.Generated
311        public KeyStoreProvider.KeyStoreProviderBuilder keys(final java.util.Collection<? extends KeyMaterialHolder> keys) {
312            if (keys == null) {
313                throw new java.lang.NullPointerException("keys cannot be null");
314            }
315            if (this.keys == null) this.keys = new java.util.ArrayList<KeyMaterialHolder>();
316            this.keys.addAll(keys);
317            return this;
318        }
319
320        @java.lang.SuppressWarnings("all")
321        @lombok.Generated
322        public KeyStoreProvider.KeyStoreProviderBuilder clearKeys() {
323            if (this.keys != null) this.keys.clear();
324            return this;
325        }
326
327        @java.lang.SuppressWarnings("all")
328        @lombok.Generated
329        public KeyStoreProvider build() {
330            java.util.Collection<KeyMaterialHolder> keys;
331            switch (this.keys == null ? 0 : this.keys.size()) {
332            case 0: 
333                keys = java.util.Collections.emptyList();
334                break;
335            case 1: 
336                keys = java.util.Collections.singletonList(this.keys.get(0));
337                break;
338            default: 
339                keys = java.util.Collections.unmodifiableList(new java.util.ArrayList<KeyMaterialHolder>(this.keys));
340            }
341            return new KeyStoreProvider(this.keyStoreType, this.location, this.storePassword, this.keyPassword, keys);
342        }
343
344        @java.lang.Override
345        @java.lang.SuppressWarnings("all")
346        @lombok.Generated
347        public java.lang.String toString() {
348            return "KeyStoreProvider.KeyStoreProviderBuilder(keyStoreType=" + this.keyStoreType + ", location=" + this.location + ", storePassword=" + this.storePassword + ", keyPassword=" + this.keyPassword + ", keys=" + this.keys + ")";
349        }
350    }
351
352    @java.lang.SuppressWarnings("all")
353    @lombok.Generated
354    public static KeyStoreProvider.KeyStoreProviderBuilder builder() {
355        return new KeyStoreProvider.KeyStoreProviderBuilder();
356    }
357
358    @java.lang.Override
359    @java.lang.SuppressWarnings("all")
360    @lombok.Generated
361    public boolean equals(final java.lang.Object o) {
362        if (o == this) return true;
363        if (!(o instanceof KeyStoreProvider)) return false;
364        final KeyStoreProvider other = (KeyStoreProvider) o;
365        if (!other.canEqual((java.lang.Object) this)) return false;
366        final java.lang.Object this$keyStoreType = this.keyStoreType;
367        final java.lang.Object other$keyStoreType = other.keyStoreType;
368        if (this$keyStoreType == null ? other$keyStoreType != null : !this$keyStoreType.equals(other$keyStoreType)) return false;
369        final java.lang.Object this$location = this.location;
370        final java.lang.Object other$location = other.location;
371        if (this$location == null ? other$location != null : !this$location.equals(other$location)) return false;
372        return true;
373    }
374
375    @java.lang.SuppressWarnings("all")
376    @lombok.Generated
377    protected boolean canEqual(final java.lang.Object other) {
378        return other instanceof KeyStoreProvider;
379    }
380
381    @java.lang.Override
382    @java.lang.SuppressWarnings("all")
383    @lombok.Generated
384    public int hashCode() {
385        final int PRIME = 59;
386        int result = 1;
387        final java.lang.Object $keyStoreType = this.keyStoreType;
388        result = result * PRIME + ($keyStoreType == null ? 43 : $keyStoreType.hashCode());
389        final java.lang.Object $location = this.location;
390        result = result * PRIME + ($location == null ? 43 : $location.hashCode());
391        return result;
392    }
393
394    @java.lang.Override
395    @java.lang.SuppressWarnings("all")
396    @lombok.Generated
397    public java.lang.String toString() {
398        return "KeyStoreProvider(keyStoreType=" + this.keyStoreType + ", location=" + this.location + ")";
399    }
400
401    @NonNull
402    @java.lang.SuppressWarnings("all")
403    @lombok.Generated
404    public KeyStoreType getKeyStoreType() {
405        return this.keyStoreType;
406    }
407
408    @java.lang.SuppressWarnings("all")
409    @lombok.Generated
410    public File getLocation() {
411        return this.location;
412    }
413
414    /**
415     * The password for the keystore aka the storage.
416     */
417    @java.lang.SuppressWarnings("all")
418    @lombok.Generated
419    public String getStorePassword() {
420        return this.storePassword;
421    }
422
423    /**
424     * (Optional) password for the keystore-key. Due to its nature this is usually only necessary
425     * for {@link KeyStoreType#KEY_STORE}
426     */
427    @java.lang.SuppressWarnings("all")
428    @lombok.Generated
429    public String getKeyPassword() {
430        return this.keyPassword;
431    }
432
433    @java.lang.SuppressWarnings("all")
434    @lombok.Generated
435    public Collection<KeyMaterialHolder> getKeys() {
436        return this.keys;
437    }
438}