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}