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}