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