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}