diff --git a/README.md b/README.md index 3b64139..8361069 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,20 @@ [![Maven Central](https://img.shields.io/maven-central/v/com.bol/cryptvault.svg)](http://search.maven.org/#search%7Cga%7C1%7Ccom.bol) [![Build](https://github.com/bolcom/cryptvault/actions/workflows/maven.yml/badge.svg)](https://github.com/bolcom/cryptvault/actions) -# Cryptvault +# Cryptvault: versioned, secure, generic encryption/decryption in Java -Allows for a versioned, secure generic crypt/decrypt in java. - -Originally developed for [spring-data-mongodb-encrypt](https://github.com/bolcom/spring-data-mongodb-encrypt), it is now offered as a general use library. +> When in doubt, encrypt. When not in doubt, be in doubt. ## Features - key versioning (to help migrating to new key without need to convert data) - uses 256-bit AES by default -- supports any encryption available in Java (via JCE) +- supports any encryption available in Java (via Java Cryptography Architecture + or JCA) - simple - no dependencies -## Use +## Usage Add dependency: @@ -23,7 +22,7 @@ Add dependency: com.bol cryptvault - 1.0.2 + 3-2.0.0 ``` @@ -52,23 +51,26 @@ byte[] decrypted = cryptVault.decrypt(encrypted); new String(decrypted).equals("rock"); // true ``` -## Manual configuration - -You can also configure `CryptVault` yourself. Look at [how spring autoconfig configures CryptVault](src/main/java/com/bol/config/CryptVaultAutoConfiguration.java) for details. - ## Keys -This library supports AES 256 bit keys out of the box. It's possible to extend this, check the source code (`CryptVault` specifically) on how to do so. +This library uses the encryption keys specified in the configuration directly. +Notably, it does not use any key-derivation. That means that you are responsible +for providing a key from a high-entropy source. -To generate a key, you can use the following command line: +The length of the key depends on the algorithm specified. When using AES-256, +you need to provide a key that is 256 bits/32 bytes long. (For comparison, the +weak DES uses 64-bit keys.) -``` -dd if=/dev/urandom bs=1 count=32 | base64 +To generate a key suitable for AES-256 bit, you can use the following command: + +```console +$ dd if=/dev/urandom bs=1 count=32 | base64 ``` -## Exchange keys +## Rotating keys -It is advisable to rotate your keys every now and then. To do so, define a new key version in `application.yml`: +It is advisable to rotate your keys every now and then. To do so, define a new +key version in `application.yml`: ```yaml cryptvault: @@ -79,7 +81,13 @@ cryptvault: key: ge2L+MA9jLA8UiUJ4z5fUoK+Lgj2yddlL6EzYIBqb1Q= ``` -`spring-data-mongodb-encrypt` would automatically use the highest versioned key for encryption by default, but supports decryption using any of the keys. This allows you to deploy a new key, and either let old data slowly get phased out, or run a nightly load+save batch job to force key migration. Once all old keys are phased out, you may remove the old key from the configuration. +CryptVault automatically uses the highest versioned key for encryption by +default, but supports decryption using any of the keys. This allows you to +deploy a new key, and either let old data slowly get phased out, or run a +nightly load+save batch job to force key migration. Once all old keys are phased +out, you may remove the old key from the configuration. + +## Specify default key version You can use @@ -88,9 +96,102 @@ cryptvault: default-key: 1 ``` -to override which version of the defined keys is considered 'default'. +to override which version of the defined keys is considered default. + +## Specify encryption algorithm + +Instead of using the default AES-256 in CBC mode, you can specify the algorithm, +mode of operation and padding scheme directly in the configuration: + +```yaml +cryptvault: + keys: + version: 1 + key: Ifw/+pLuWBjn7a1mjuToQ8hpIh8DV0WLf9b4z7iinGs= + transformation: AES/GCM/NoPadding +``` + +You can use all the algorithms specified by JCA. Other valid transformations +are, for example, "DES/CTR/NoPadding" and "ChaCha20-Poly1305". For a +comprehensive list, see [Java Security Standard Algorithm Names][Java Security +Standard Algorithm Names]. + +The YAML key is called "transformation" because it signifies more than just an +algorithm, but rather a set of operations performed on an input to produce some +output. Naming it this way keeps vocabulary consistent with the JCA. + +## Format of the encrypted blob + +The encrypted blobs look like (numbers are bits): + +``` +0 8 16 24 ++---------+---------+---------+--------------------+--------------------+ +|proto |key |param |params |ciphertext | +|version |version |length | ... | ... | +|8 |8 |8 |[0,255] |[16,inf) | ++---------+---------+---------+--------------------+--------------------+ +``` +* `proto version` is the protocol version of this blob. Having a version allows + making improvements to this blob over time without having to decrypt all the + old encryptions and encrypt it under a new (versionless) version. +* `key version` is the user-controlled version of the key that was used to + encrypt the data in this blob. +* `param length` is the length of next field, the algorithm parameters +* `params` are the algorithms parameters that that need to be known + in order to decrypt the blob successfully. For example, when using + AES/CBC/PKCS5Padding, this will (among some overhead) contain the 16-byte IV. + See `java.security.AlgorithmParameters#getEncoded` for more information. +* `ciphertext` contains the output of applying the specified transformation + under the specified key to the input. ## Expected size of encrypted data -Depending on how much padding is used, you can expect 17..33 bytes for encryption overhead (salt + padding). +Depending on the cipher, whether an IV or tag are used and the padding scheme +you must expect some overhead for encryption. The default cipher, AES-256-CBC +with PKCS #5 padding, requires an extra [22, 37] bytes: proto version (1) + key +version (1) + param length (1) + algorithm parameters (18) + padding (best case: +1, worst case: 16). + +## Migrating from version 1 to version 2 + +### TL;DR: + +1. Add `legacy: true` to keys that were in use under version 1. +2. Create a new key version that will be used for new encryptions. + +```yaml +cryptvault: + keys: + # the legacy key version (can only decrypt!) + - version: 1 + key: yaF4Gi13Gp+gF5Tm+jMkYbQKMO3c6KYZbQmMqXQyid0= + legacy: true + # the new version (can encrypt/decrypt as usual) + - version: 2 + key: CqeKXVZuDbeMk0/h1zZrBG0Mul4qMnqShaGjkxWrlQ0= +``` + +### More detail + +Version 2 introduced a new format of the binary blob. This provides certain +benefits (see under [Format of the encrypted blob, +above](#format-of-the-encrypted-blob)). However, the old encrypted blobs have +become incompatible as a result of this breaking change. You can still decrypt +the blobs, however. Encrypting with these legacy key versions is not supported, +however. + +To migrate: + +1. Add `legacy: true` to the legacy key version(s) in the config. +2. Create a new key version that will be used for new encryptions. + +Old encrypted blobs will not be updated automatically since this library does +not handle persistence. There is little harm in keeping them around as they +are still secure. However, should you wish to upgrade the stored blobs, decrypt +them and then overwrite them with a fresh encrypted version under the new key +version. + +[Java Security Standard Algorithm Names]: + diff --git a/pom.xml b/pom.xml index d365b98..051a4f7 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ cryptvault jar cryptvault - 3-1.0.2 + 3-2.0.0-SNAPSHOT Versioned crypto library https://github.com/bolcom/cryptvault @@ -51,26 +51,20 @@ org.springframework.boot spring-boot-autoconfigure - 3.2.3 + 3.3.2 provided org.springframework.boot spring-boot-starter-test - 3.2.3 - test - - - junit - junit - 4.13.2 + 3.3.2 test org.assertj assertj-core - 3.25.3 + 3.26.3 test @@ -81,8 +75,24 @@ maven-compiler-plugin 3.6.1 - 1.8 - 1.8 + 17 + 17 + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.3.1 + + + + integration-test + + integration-test + + + + *SystemTest.java diff --git a/src/main/java/com/bol/config/CryptVaultAutoConfiguration.java b/src/main/java/com/bol/config/CryptVaultAutoConfiguration.java index 2bf3b72..7cf826b 100644 --- a/src/main/java/com/bol/config/CryptVaultAutoConfiguration.java +++ b/src/main/java/com/bol/config/CryptVaultAutoConfiguration.java @@ -1,77 +1,97 @@ package com.bol.config; import com.bol.crypt.CryptVault; +import com.bol.crypt.KeyVersion; +import com.bol.crypt.KeyVersions; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; -import org.springframework.stereotype.Component; -import java.util.Base64; import java.util.List; +import java.util.Objects; @AutoConfiguration @ConditionalOnProperty("cryptvault.keys[0].key") +@EnableConfigurationProperties(value = {CryptVaultAutoConfiguration.CryptVaultConfigurationProperties.class}) public class CryptVaultAutoConfiguration { @Bean CryptVault cryptVault(CryptVaultConfigurationProperties properties) { - CryptVault cryptVault = new CryptVault(); - if (properties.keys == null || properties.keys.isEmpty()) throw new IllegalArgumentException("property 'keys' is not set"); + if (properties.keys == null || properties.keys.isEmpty()) { + throw new IllegalStateException("property 'keys' is not set"); + } - for (Key key : properties.keys) { - byte[] secretKeyBytes = Base64.getDecoder().decode(key.key); - cryptVault.with256BitAesCbcPkcs5PaddingAnd128BitSaltKey(key.version, secretKeyBytes); + KeyVersions versions = new KeyVersions(); + for (KeyVersionProperties props : properties.keys) { + Objects.requireNonNull(props.key, String.format("key version %d has a null key", props.version)); + if (props.version < 1 || props.version > 255) { + throw new IllegalArgumentException(String.format("version should be [1, 255], got %d", props.version)); + } + if (props.transformation == null) props.transformation = "AES/CBC/PKCS5Padding"; + versions.addVersion(new KeyVersion(props.version, props.transformation, props.key, props.legacy)); } if (properties.defaultKey != null) { - cryptVault.withDefaultKeyVersion(properties.defaultKey); + if (properties.defaultKey < 1 || properties.defaultKey > 255) { + var msg = String.format("default key version should be in [1, 255], was %d", properties.defaultKey); + throw new IllegalStateException(msg); + } + versions.get(properties.defaultKey).ifPresentOrElse( + versions::setDefault, + () -> { + var msg = String.format("no version %d registered; cannot make default", properties.defaultKey); + throw new IllegalStateException(msg); + }); } - return cryptVault; + return CryptVault.of(versions); } - @Component @ConfigurationProperties("cryptvault") public static class CryptVaultConfigurationProperties { - List keys; + List keys; Integer defaultKey; - public void setKeys(List keys) { + public void setKeys(List keys) { this.keys = keys; } public void setDefaultKey(Integer defaultKey) { this.defaultKey = defaultKey; } - - public List getKeys() { - return keys; - } - - public Integer getDefaultKey() { - return defaultKey; - } } - public static class Key { + public static class KeyVersionProperties { int version; + String transformation; String key; + boolean legacy; public void setVersion(int version) { this.version = version; } + public void setTransformation(String transformation) { + this.transformation = transformation; + } + public void setKey(String key) { this.key = key; } - public int getVersion() { - return version; + public void setLegacy(boolean legacy) { + this.legacy = legacy; } - public String getKey() { - return key; + @Override + public String toString() { + return "KeyVersionProperties{" + + "version=" + version + + ", transformation='" + transformation + '\'' + + ", keyBase64='" + key + '\'' + + '}'; } } } diff --git a/src/main/java/com/bol/crypt/CryptOperationException.java b/src/main/java/com/bol/crypt/CryptOperationException.java index 20b4bec..a73e077 100644 --- a/src/main/java/com/bol/crypt/CryptOperationException.java +++ b/src/main/java/com/bol/crypt/CryptOperationException.java @@ -1,5 +1,8 @@ package com.bol.crypt; +/** + * Wraps different JCA exceptions under a single umbrella. + */ public class CryptOperationException extends RuntimeException { public CryptOperationException(String s, Throwable e) { super(s, e); diff --git a/src/main/java/com/bol/crypt/CryptVault.java b/src/main/java/com/bol/crypt/CryptVault.java index 372c2f3..71efe1d 100644 --- a/src/main/java/com/bol/crypt/CryptVault.java +++ b/src/main/java/com/bol/crypt/CryptVault.java @@ -1,179 +1,197 @@ package com.bol.crypt; -import com.bol.util.JCEPolicy; -import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.lang.Nullable; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; -import javax.crypto.ShortBufferException; +import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; +import java.io.IOException; +import java.security.AlgorithmParameters; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; -import java.security.Key; -import java.security.SecureRandom; -import java.util.function.Function; - +import java.security.NoSuchAlgorithmException; +import java.security.spec.AlgorithmParameterSpec; + +/** + * The main encryptor and decryptor class. + *

+ * The binary blobs produced by this library look like (numbers are bits): + *

+ * 0         8         16        24
+ * +---------+---------+---------+--------------------+--------------------+
+ * |proto    |key      |param    |params              |ciphertext          |
+ * |version  |version  |length   |         ...        |            ...     |
+ * |8        |8        |8        |[0,255]             |[16,inf)            |
+ * +---------+---------+---------+--------------------+--------------------+
+ */
 public class CryptVault {
-    static final String DEFAULT_CIPHER = "AES/CBC/PKCS5Padding";
-    static final String DEFAULT_ALGORITHM = "AES";
-    static final int DEFAULT_SALT_LENGTH = 16;
-
-    private final CryptVersion[] cryptVersions = new CryptVersion[256];
-    int defaultVersion = -1;
-
     /**
-     * Helper method for the most used case.
-     * If you even need to change this, or need backwards compatibility, use the more advanced constructor instead.
+     * All the key versions as configured in the external configuration.
      */
-    public CryptVault with256BitAesCbcPkcs5PaddingAnd128BitSaltKey(int version, byte[] secret) {
-        if (secret.length != 32) throw new IllegalArgumentException("invalid AES key size; should be 256 bits!");
+    public KeyVersions keyVersions;
 
-        Key key = new SecretKeySpec(secret, DEFAULT_ALGORITHM);
-        CryptVersion cryptVersion = new CryptVersion(DEFAULT_SALT_LENGTH, DEFAULT_CIPHER, key, AESLengthCalculator);
-        return withKey(version, cryptVersion);
+    private CryptVault() {
     }
 
-    public CryptVault withKey(int version, CryptVersion cryptVersion) {
-        if (version < 0 || version > 255) throw new IllegalArgumentException("version must be a byte");
-        if (cryptVersions[version] != null) throw new IllegalArgumentException("version " + version + " is already defined");
-
-        cryptVersions[version] = cryptVersion;
-        if (version > defaultVersion) defaultVersion = version;
-        return this;
+    /**
+     * Create a new instance initialized with the provided KeyVersions.
+     *
+     * @param keyVersions
+     * @return A new instance.
+     */
+    public static CryptVault of(KeyVersions keyVersions) {
+        CryptVault cryptVault = new CryptVault();
+        cryptVault.keyVersions = keyVersions;
+        return cryptVault;
     }
 
     /**
-     * specifies the version used in encrypting new data. default is highest version number.
+     * Encrypts the given binary blob under the transformation given by the
+     * default key version. Default encryption parameters are used.
+     * 

+ * Legacy key versions are only allowed to decrypt, not encrypt. + * + * @param cleartext Bytes to be encrypted. + * @return A self-contained, encrypted binary blob. + * @throws CryptOperationException */ - public CryptVault withDefaultKeyVersion(int defaultVersion) { - if (defaultVersion < 0 || defaultVersion > 255) throw new IllegalArgumentException("version must be a byte"); - if (cryptVersions[defaultVersion] == null) throw new IllegalArgumentException("version " + defaultVersion + " is undefined"); - - this.defaultVersion = defaultVersion; - return this; + public byte[] encrypt(byte[] cleartext) throws CryptOperationException { + return encrypt(keyVersions.getDefault(), cleartext); } - // FIXME: have a pool of ciphers (with locks & so), cipher init seems to be very costly (jmh it!) - Cipher cipher(String cipher) { - try { - return Cipher.getInstance(cipher); - } catch (Exception e) { - throw new IllegalStateException("init failed for cipher " + cipher, e); - } + /** + * Encrypts the given binary blob under the transformation defined in the + * given key version. Default encryption parameters are used. + *

+ * Legacy key versions are only allowed to decrypt, not encrypt. + * + * @param keyVersion The key version to encrypt the blob under. + * @param cleartext Bytes to be encrypted. + * @return A self-contained, encrypted binary blob. + * @throws CryptOperationException + */ + public byte[] encrypt(KeyVersion keyVersion, byte[] cleartext) throws CryptOperationException { + return encrypt(keyVersion, cleartext, null); } - private SecureRandom SECURE_RANDOM = new SecureRandom(); + /** + * Encrypts the given binary blob under the transformation defined in the + * given key version. Algorithm parameters can be tweaked by passing a + * custom {@code AlgorithmParameterSpec}. + *

+ * Legacy key versions are only allowed to decrypt, not encrypt. + * + * @param keyVersion The key version to encrypt the blob under. + * @param cleartext Bytes to be encrypted. + * @param algoParamSpec The encryption parameters. Can be null, in which case the defaults for the given algorithm will be used. + * @return A self-contained, encrypted binary blob. + * @throws CryptOperationException + */ + public byte[] encrypt(KeyVersion keyVersion, byte[] cleartext, @Nullable AlgorithmParameterSpec algoParamSpec) throws CryptOperationException { + if (keyVersion.legacy) + throw new CryptOperationException("cannot encrypt with legacy key version; hint: create new key version"); - // depending on securerandom implementation (that differs per platform and jvm), this might or might not be necessary. - @Scheduled(initialDelay = 3_600_000, fixedDelay = 3_600_000) - public void reinitSecureRandomHourly() { - SECURE_RANDOM = new SecureRandom(); - } + try { + Cipher cipher = Cipher.getInstance(keyVersion.transformation); - byte[] randomBytes(int numBytes) { - byte[] bytes = new byte[numBytes]; - SECURE_RANDOM.nextBytes(bytes); - return bytes; - } + String algorithm = keyVersion.transformation.split("/", 2)[0]; + SecretKeySpec aesKeySpec = new SecretKeySpec(keyVersion.key, algorithm); - public byte[] encrypt(byte[] data) { - return encrypt(defaultVersion, data); - } + cipher.init(Cipher.ENCRYPT_MODE, aesKeySpec, algoParamSpec); - public byte[] encrypt(int version, byte[] data) { - CryptVersion cryptVersion = cryptVersion(version); - try { - int cryptedLength = cryptVersion.encryptedLength.apply(data.length); - byte[] result = new byte[cryptedLength + cryptVersion.saltLength + 1]; - result[0] = toSignedByte(version); + byte[] ciphertext = cipher.doFinal(cleartext); - byte[] random = randomBytes(cryptVersion.saltLength); - IvParameterSpec iv_spec = new IvParameterSpec(random); - System.arraycopy(random, 0, result, 1, cryptVersion.saltLength); + byte[] encodedParams = (cipher.getParameters() == null) ? new byte[0] : cipher.getParameters().getEncoded(); - Cipher cipher = cipher(cryptVersion.cipher); - cipher.init(Cipher.ENCRYPT_MODE, cryptVersion.key, iv_spec); - int len = cipher.doFinal(data, 0, data.length, result, cryptVersion.saltLength + 1); + byte[] blob = new byte[1 + 1 + 1 + encodedParams.length + ciphertext.length]; + blob[0] = (byte) 0x0; // proto version + blob[1] = (byte) keyVersion.version; // key version (also defines transformation) + blob[2] = (byte) encodedParams.length; // paramLen + System.arraycopy(encodedParams, 0, blob, 3, encodedParams.length); + System.arraycopy(ciphertext, 0, blob, 3 + encodedParams.length, ciphertext.length); - return result; - } catch (ShortBufferException | IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException | InvalidKeyException e) { + return blob; + } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | + BadPaddingException | IOException | InvalidAlgorithmParameterException e) { // wrap checked exception for easy use - throw new CryptOperationException("JCE exception caught while encrypting with version " + version, e); + throw new CryptOperationException("JCA exception caught while encrypting with key version " + keyVersion.version, e); } } - public byte[] decrypt(byte[] data) { - int version = fromSignedByte(data[0]); - CryptVersion cryptVersion = cryptVersion(version); - - try { - byte[] random = new byte[cryptVersion.saltLength]; - System.arraycopy(data, 1, random, 0, cryptVersion.saltLength); - IvParameterSpec iv_spec = new IvParameterSpec(random); - - Cipher cipher = cipher(cryptVersions[version].cipher); - cipher.init(Cipher.DECRYPT_MODE, cryptVersions[version].key, iv_spec); - return cipher.doFinal(data, cryptVersion.saltLength + 1, data.length - cryptVersion.saltLength - 1); - } catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) { - // wrap checked exception for easy use - throw new CryptOperationException("JCE exception caught while decrypting with key version " + version, e); + /** + * Decrypts a previously-encrypted, self-contained binary blob. To achieve + * compatibility with previous versions of this library, if the first byte + * is not a recognized protocol version (currently 00), a "legacy + * decryption" is attempted: the blob will be decrypted according to the + * legacy decryption process. + * + * @param blob The previously-encrypted binary blob. + * @return The recovered cleartext. + * @throws CryptOperationException + */ + public byte[] decrypt(byte[] blob) throws CryptOperationException { + int protoVersion = blob[0] & 0xFF; + if (protoVersion != 0) { + if (keyVersions.isLegacyVersion(blob[0])) { + return attemptLegacyDecrypt(blob); + } + throw new CryptOperationException("cryptvault protocol version in encrypted blob is unknown: " + protoVersion); } - } - public int expectedCryptedLength(int serializedLength) { - return expectedCryptedLength(defaultVersion, serializedLength); - } + int blobKeyVersion = blob[1] & 0xFF; + KeyVersion keyVersion = keyVersions.get(blobKeyVersion).orElseThrow( + () -> new CryptOperationException("key version in encrypted blob is unknown: " + blobKeyVersion)); - public int expectedCryptedLength(int version, int serializedLength) { - CryptVersion cryptVersion = cryptVersion(version); - return cryptVersion.saltLength + 1 + cryptVersion.encryptedLength.apply(serializedLength); - } + int paramLen = blob[2] & 0xFF; + byte[] paramsAsBytes = new byte[paramLen]; + System.arraycopy(blob, 3, paramsAsBytes, 0, paramLen); - private CryptVersion cryptVersion(int version) { try { - CryptVersion result = cryptVersions[version]; - if (result == null) throw new CryptOperationException("version " + version + " undefined"); - return result; - } catch (IndexOutOfBoundsException e) { - if (version < 0) throw new CryptOperationException("encryption keys are not initialized"); - throw new CryptOperationException("version must be a byte (0-255)"); - } - } + Cipher decryptionCipher = Cipher.getInstance(keyVersion.transformation); - /** - * amount of keys defined in this CryptVault - */ - public int size() { - int size = 0; - for (int i = 0; i < cryptVersions.length; i++) { - if (cryptVersions[i] != null) size++; - } - return size; - } + AlgorithmParameters algoParams = decryptionCipher.getParameters(); + AlgorithmParameters storedParams = null; + if (algoParams != null) { + storedParams = AlgorithmParameters.getInstance(algoParams.getAlgorithm()); + storedParams.init(paramsAsBytes); + } - /** - * AES simply pads to 128 bits - */ - static final Function AESLengthCalculator = i -> (i | 0xf) + 1; + String algorithm = keyVersion.transformation.split("/", 2)[0]; + SecretKeySpec keySpec = new SecretKeySpec(keyVersion.key, algorithm); - /** - * because, you know... java - */ - public static byte toSignedByte(int val) { - return (byte) (val + Byte.MIN_VALUE); - } + decryptionCipher.init(Cipher.DECRYPT_MODE, keySpec, storedParams); - /** - * because, you know... java - */ - public static int fromSignedByte(byte val) { - return ((int) val - Byte.MIN_VALUE); + return decryptionCipher.doFinal( + blob, 3 + paramLen, blob.length - 3 - paramLen); + } catch (InvalidAlgorithmParameterException | NoSuchPaddingException | IllegalBlockSizeException | + NoSuchAlgorithmException | IOException | BadPaddingException | InvalidKeyException e) { + throw new CryptOperationException("JCA exception caught while decrypting with key version " + keyVersion.version, e); + } } - static { - // stupid JCE - JCEPolicy.allowUnlimitedStrength(); + byte[] attemptLegacyDecrypt(byte[] blob) throws RuntimeException { + int version = (int) blob[0] - Byte.MIN_VALUE; + var legacyKeyVersion = keyVersions.get(version).orElseThrow( + () -> new CryptOperationException(String.format("legacy version %d not registered", version)) + ); + + int keyVersionLength = 1; + int ivLength = 16; + byte[] ivBytes = new byte[ivLength]; + System.arraycopy(blob, keyVersionLength, ivBytes, 0, ivLength); + try { + var ivParamSpec = new IvParameterSpec(ivBytes); + + var cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + var key = new SecretKeySpec(legacyKeyVersion.key, "AES"); + cipher.init(Cipher.DECRYPT_MODE, key, ivParamSpec); + return cipher.doFinal(blob, keyVersionLength + ivLength, blob.length - keyVersionLength - ivLength); + } catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | + BadPaddingException | NoSuchPaddingException | NoSuchAlgorithmException e) { + throw new CryptOperationException("JCA exception caught while attempting legacy decryption with key version " + version, e); + } } } diff --git a/src/main/java/com/bol/crypt/CryptVersion.java b/src/main/java/com/bol/crypt/CryptVersion.java deleted file mode 100644 index 6a50334..0000000 --- a/src/main/java/com/bol/crypt/CryptVersion.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.bol.crypt; - -import java.security.Key; -import java.util.function.Function; - -/** Immutable data class */ -public class CryptVersion { - public final int saltLength; - public final String cipher; - public final Key key; - public final Function encryptedLength; - - public CryptVersion(int saltLength, String cipher, Key key, Function encryptedLength) { - this.saltLength = saltLength; - this.cipher = cipher; - this.key = key; - this.encryptedLength = encryptedLength; - } -} diff --git a/src/main/java/com/bol/crypt/KeyVersion.java b/src/main/java/com/bol/crypt/KeyVersion.java new file mode 100644 index 0000000..8781460 --- /dev/null +++ b/src/main/java/com/bol/crypt/KeyVersion.java @@ -0,0 +1,60 @@ +package com.bol.crypt; + +import java.util.Arrays; +import java.util.Base64; + +public class KeyVersion { + /** + * The version. Can be no bigger than a byte, so only values [0, 256) are + * acceptable. + */ + public final int version; + /** + * A JCA "transformation" consisting of "algorithm/mode of + * operation/padding". E.g. {@code "AES/CBC/PKCS5Padding"}. + * Case-insensitive. + */ + public final String transformation; + /** + * The actual key used in encryption, i.e. no key derivation is performed + * on this key. The size depends on the algorithm used. E.g. in AES-256, + * the key size should be 256 bits/32 bytes. An incorrect key size will + * result in exceptions. + */ + public final byte[] key; + /** + * Whether this key was used with version 1 of this library. In that case, + * this key version can only be used for decryption. New encryptions should + * be done with a new key. + */ + public final boolean legacy; + + public KeyVersion(int version, String transformation, byte[] key, boolean legacy) { + this.version = version; + this.transformation = transformation; + this.key = key; + this.legacy = legacy; + } + + public KeyVersion(int version, String transformation, byte[] key) { + this(version, transformation, key, false); + } + + public KeyVersion(int version, String transformation, String keyBase64, boolean legacy) { + this(version, transformation, Base64.getDecoder().decode(keyBase64), legacy); + } + + public KeyVersion(int version, String transformation, String keyBase64) { + this(version, transformation, keyBase64, false); + } + + @Override + public String toString() { + return "KeyVersion{" + + "version=" + version + + ", transformation='" + transformation + '\'' + + ", key=" + Arrays.toString(key) + + ", legacy=" + legacy + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/bol/crypt/KeyVersions.java b/src/main/java/com/bol/crypt/KeyVersions.java new file mode 100644 index 0000000..849929b --- /dev/null +++ b/src/main/java/com/bol/crypt/KeyVersions.java @@ -0,0 +1,114 @@ +package com.bol.crypt; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +/** + * Stores different versioned configurations containing transformations and keys. + */ +public class KeyVersions { + private final List keyVersions = new ArrayList<>(); + private KeyVersion defaultVersion = null; + + /** + * Creates a new instance of this class initialized with the provided key + * versions. + * + * @param versions Key versions. + * @return A new instance. + */ + public static KeyVersions of(KeyVersion... versions) { + var instance = new KeyVersions(); + instance.addVersions(List.of(versions)); + return instance; + } + + /** + * Amount of keys defined. + */ + public int size() { + return keyVersions.size(); + } + + /** + * Get the key version indicated by {@code version} if it exists. + * @param version The version. Should be [1, 255] or will throw otherwise. + * @return The key version, if it was registered. An empty {@code Optional} + * otherwise. + * @throws IllegalArgumentException when version is not in [1, 255] + */ + public Optional get(int version) { + if (version < 1 || version > 255) throw new IllegalArgumentException("versions must be in range [1, 255]"); + return keyVersions.stream().filter((v) -> v.version == version).findFirst(); + } + + /** + * Gets the default key version. + * @return The default key version. + * @throws IllegalStateException when no default key version was previously set. + */ + public KeyVersion getDefault() { + if (defaultVersion == null) throw new IllegalStateException("no default version set"); + return defaultVersion; + } + + /** + * Adds a version. If the added version has a higher version number than the + * existing default version, the default becomes the newly-added version. + * This is compatible with earlier versions of this library. If you want to + * use a default version that is not the latest version, make sure to + * invoke {@code setDefault} after calling this method. + * + * @param keyVersion The version to add. + */ + public void addVersion(KeyVersion keyVersion) { + if (keyVersion.version < 0 || keyVersion.version > 255) { + throw new IllegalArgumentException("version must fit in a byte"); + } + if (get(keyVersion.version).isPresent()) { + throw new IllegalArgumentException("version " + keyVersion.version + " is already registered"); + } + + keyVersions.add(keyVersion); + + if (defaultVersion == null || keyVersion.version > defaultVersion.version) { + defaultVersion = keyVersion; + } + } + + /** + * Adds multiple versions in one swoop. + * + * @param versions The versions to add. + */ + public void addVersions(Collection versions) { + versions.forEach(this::addVersion); + } + + /** + * Set the default version. This is the version that is used in unqualified + * calls to {@code CryptVault#encrypt}. + * + * @param defaultVersion The new default. + */ + public void setDefault(KeyVersion defaultVersion) { + this.defaultVersion = defaultVersion; + } + + /** + * Reports whether the given version number is registered as being a legacy + * key version (< version 2 of this library). A legacy version can still be + * decrypted if it used the default algorithm and parameters. + * @param version A version number. It's a byte for convenience when taking + * it from a binary blob. + * @return Whether {@code version} is a recognized (i.e. registered) legacy + * key version. + */ + public boolean isLegacyVersion(byte version) { + // in legacy version, 0x80 (-128) was version 0, 0x81 (-127) was version 1, etc. + var legacyVersion = (int) version - Byte.MIN_VALUE; + return get(legacyVersion).map((kv) -> kv.legacy).orElse(false); + } +} diff --git a/src/test/java/com/bol/crypt/CryptVaultTest.java b/src/test/java/com/bol/crypt/CryptVaultTest.java index 6c16c8f..1012f12 100644 --- a/src/test/java/com/bol/crypt/CryptVaultTest.java +++ b/src/test/java/com/bol/crypt/CryptVaultTest.java @@ -3,31 +3,33 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import javax.crypto.spec.GCMParameterSpec; import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.Arrays; import java.util.Base64; -import static com.bol.crypt.CryptVault.fromSignedByte; -import static com.bol.crypt.CryptVault.toSignedByte; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; public class CryptVaultTest { - private static final byte[] KEY = "VGltVGhlSW5jcmVkaWJsZURldmVsb3BlclNlY3JldCE=".getBytes(); - private static final String plainText = "The quick brown fox jumps over the lazy dog"; - private static final byte[] plainBytes = plainText.getBytes(StandardCharsets.UTF_8); + private static final String keyBase64 = "VGltVGhlSW5jcmVkaWJsZURldmVsb3BlclNlY3JldCE="; + private static final String plaintext = "The quick brown fox jumps over the lazy dog"; + private static final byte[] plainBytes = plaintext.getBytes(StandardCharsets.UTF_8); private CryptVault cryptVault; @BeforeEach public void setup() { - byte[] secretKeyBytes = Base64.getDecoder().decode(KEY); - cryptVault = new CryptVault() - .with256BitAesCbcPkcs5PaddingAnd128BitSaltKey(1, secretKeyBytes); + var keyVersions = new KeyVersions(); + keyVersions.addVersion(new KeyVersion(1, "AES/CBC/PKCS5Padding", keyBase64, false)); + cryptVault = CryptVault.of(keyVersions); } @Test public void consecutiveEncryptsDifferentResults() { - byte[] cryptedSecret1 = cryptVault.encrypt(1, plainBytes); - byte[] cryptedSecret2 = cryptVault.encrypt(1, plainBytes); + KeyVersion firstVersion = cryptVault.keyVersions.get(1).orElseThrow(); + byte[] cryptedSecret1 = cryptVault.encrypt(firstVersion, plainBytes); + byte[] cryptedSecret2 = cryptVault.encrypt(firstVersion, plainBytes); assertThat(cryptedSecret1.length).isEqualTo(cryptedSecret2.length); // version @@ -49,16 +51,17 @@ public void decryptionUndoesEncryption() { byte[] decryptedBytes = cryptVault.decrypt(encryptedBytes); String decryptedString = new String(decryptedBytes, StandardCharsets.UTF_8); - assertThat(decryptedString).isEqualTo(plainText); + assertThat(decryptedString).isEqualTo(plaintext); } @Test public void wrongKeyDecryptionFailure() { byte[] encryptedBytes = cryptVault.encrypt(plainBytes); - byte[] keyBytes = Base64.getDecoder().decode("VGhpcyBpcyB0aGUgd3Jvbmcga2V5LCBJJ20gc29ycnk="); - CryptVault otherVault = new CryptVault() - .with256BitAesCbcPkcs5PaddingAnd128BitSaltKey(1, keyBytes); + byte[] otherKeyBytes = Base64.getDecoder().decode("VGhpcyBpcyB0aGUgd3Jvbmcga2V5LCBJJ20gc29ycnk="); + var otherKeyVersions = new KeyVersions(); + otherKeyVersions.addVersion(new KeyVersion(1, "AES/CBC/PKCS5Padding", otherKeyBytes)); + CryptVault otherVault = CryptVault.of(otherKeyVersions); assertThrows(CryptOperationException.class, () -> otherVault.decrypt(encryptedBytes)); } @@ -66,7 +69,7 @@ public void wrongKeyDecryptionFailure() { @Test public void missingKeyVersionsDecryptionFailure() { byte[] encryptedBytes = cryptVault.encrypt(plainBytes); - encryptedBytes[0] = toSignedByte('2'); + encryptedBytes[1] = (byte) 2; assertThrows(CryptOperationException.class, () -> cryptVault.decrypt(encryptedBytes)); } @@ -75,25 +78,90 @@ public void missingKeyVersionsDecryptionFailure() { public void highestKeyVersionIsDefaultKey() { byte[] encryptedBytes = cryptVault.encrypt(plainBytes); - cryptVault.with256BitAesCbcPkcs5PaddingAnd128BitSaltKey(2, Base64.getDecoder().decode("IqWTpi549pJDZ1kuc9HppcMxtPfu2SP6Idlh+tz4LL4=")); + var secondKeyVersion = new KeyVersion(2, "AES/CBC/PKCS5Padding", Base64.getDecoder().decode("IqWTpi549pJDZ1kuc9HppcMxtPfu2SP6Idlh+tz4LL4=")); + cryptVault.keyVersions.addVersion(secondKeyVersion); byte[] encryptedBytes2 = cryptVault.encrypt(plainBytes); - assertThat(fromSignedByte(encryptedBytes[0])).isEqualTo(1); - assertThat(fromSignedByte(encryptedBytes2[0])).isEqualTo(2); + assertThat(encryptedBytes[1]).isEqualTo((byte) 1); + assertThat(encryptedBytes2[1]).isEqualTo((byte) 2); } @Test public void keyVersionIsDerivedFromCipher() { - cryptVault.with256BitAesCbcPkcs5PaddingAnd128BitSaltKey(2, Base64.getDecoder().decode("IqWTpi549pJDZ1kuc9HppcMxtPfu2SP6Idlh+tz4LL4=")); + var firstKeyVersion = cryptVault.keyVersions.get(1).orElseThrow(); + var secondKeyVersion = new KeyVersion(2, "ChaCha20-Poly1305", Base64.getDecoder().decode("IqWTpi549pJDZ1kuc9HppcMxtPfu2SP6Idlh+tz4LL4=")); + cryptVault.keyVersions.addVersion(secondKeyVersion); - byte[] encryptedBytes = cryptVault.encrypt(1, plainBytes); + byte[] encryptedUnderFirstKeyBytes = cryptVault.encrypt(firstKeyVersion, plainBytes); + byte[] encryptedUnderSecondKeyBytes = cryptVault.encrypt(secondKeyVersion, plainBytes); - byte[] encryptedBytes2 = cryptVault.encrypt(2, plainBytes); + assertThat(encryptedUnderFirstKeyBytes[1]).isEqualTo((byte) 1); + assertThat(encryptedUnderSecondKeyBytes[1]).isEqualTo((byte) 2); - assertThat(fromSignedByte(encryptedBytes[0])).isEqualTo(1); - assertThat(fromSignedByte(encryptedBytes2[0])).isEqualTo(2); + assertThat(cryptVault.decrypt(encryptedUnderFirstKeyBytes)).isEqualTo(plainBytes); + assertThat(cryptVault.decrypt(encryptedUnderSecondKeyBytes)).isEqualTo(plainBytes); + } + + @Test + public void differentCipherVersionsShouldLiveSideBySide() { + byte[] aesKey = "2~_J2#Kb=_xV3!wMmX3}LAny0fie7:hT".getBytes(StandardCharsets.UTF_8); + var aes256CtrTransformation = "AES/CTR/NoPadding"; + var aes256CtrVersion = new KeyVersion(1, aes256CtrTransformation, aesKey); + + var desKey = "jcs&@IwY".getBytes(); + var desCbcTransformation = "DES/CBC/PKCS5Padding"; + var desCbcVersion = new KeyVersion(2, desCbcTransformation, desKey); + + var cryptVault = CryptVault.of(KeyVersions.of(aes256CtrVersion, desCbcVersion)); + + byte[] aes256CtrBlob = cryptVault.encrypt(aes256CtrVersion, plainBytes); + assertThat(aes256CtrBlob[1]).isEqualTo((byte) 1); + assertThat(aes256CtrBlob.length).isEqualTo(1 + 1 + 1 + aes256CtrBlob[2] + plaintext.length()); + + byte[] chacha20Poly1305Blob = cryptVault.encrypt(desCbcVersion, plainBytes); + assertThat(chacha20Poly1305Blob[1]).isEqualTo((byte) 2); + int paddingLength = plaintext.length() % 16 == 0 ? 16 : 16 - plaintext.length() % 16; + assertThat(chacha20Poly1305Blob.length).isEqualTo(1 + 1 + 1 + chacha20Poly1305Blob[2] + plaintext.length() + paddingLength); + } + + @Test + public void ecbWithoutIv() { + byte[] key = "2~_J2#Kb=_xV3!wMmX3}LAny0fie7:hT".getBytes(StandardCharsets.UTF_8); + + String aes256EcbTransformation = "AES/ECB/Pkcs5Padding"; + var aes256EcbVersion = new KeyVersion(1, aes256EcbTransformation, key); + var cryptVersions = KeyVersions.of(aes256EcbVersion); + var vault = CryptVault.of(cryptVersions); + + byte[] ciphertext = vault.encrypt(plainBytes); + String decryptedText = new String(vault.decrypt(ciphertext)); + + assertThat(decryptedText).isEqualTo(plaintext); + } + + @Test + public void gcmWithSameIvEveryTime() { + byte[] key = "2~_J2#Kb=_xV3!wMmX3}LAny0fie7:hT".getBytes(StandardCharsets.UTF_8); + + var aes256GcmTransformation = "AES/GCM/NoPadding"; + var aes256GcmVersion = new KeyVersion(1, aes256GcmTransformation, key); + var keyVersions = KeyVersions.of(aes256GcmVersion); + var cryptVault = CryptVault.of(keyVersions); + + byte[] reusedIv = new byte[16]; + new SecureRandom().nextBytes(reusedIv); + + var algoParamSpec = new GCMParameterSpec(128, reusedIv); + + byte[] firstEncryptedBlob = cryptVault.encrypt(aes256GcmVersion, plainBytes, algoParamSpec); + assertThat(firstEncryptedBlob[1]).isEqualTo((byte) 1); + assertThat(Arrays.copyOfRange(firstEncryptedBlob, 3, 3 + firstEncryptedBlob[2])).contains(reusedIv); + + byte[] secondEncryptedBlob = cryptVault.encrypt(aes256GcmVersion, plainBytes, algoParamSpec); + assertThat(secondEncryptedBlob[1]).isEqualTo((byte) 1); + assertThat(Arrays.copyOfRange(secondEncryptedBlob, 1, 3 + secondEncryptedBlob[2])).contains(reusedIv); - assertThat(cryptVault.decrypt(encryptedBytes)).isEqualTo(plainBytes); - assertThat(cryptVault.decrypt(encryptedBytes2)).isEqualTo(plainBytes); + assertThat(new String(cryptVault.decrypt(firstEncryptedBlob))).isEqualTo(plaintext); + assertThat(new String(cryptVault.decrypt(secondEncryptedBlob))).isEqualTo(plaintext); } } diff --git a/src/test/java/com/bol/system/autoconfig/EncryptionConfiguredSystemTest.java b/src/test/java/com/bol/system/autoconfig/EncryptionConfiguredSystemTest.java new file mode 100644 index 0000000..2c40978 --- /dev/null +++ b/src/test/java/com/bol/system/autoconfig/EncryptionConfiguredSystemTest.java @@ -0,0 +1,78 @@ +package com.bol.system.autoconfig; + +import com.bol.config.CryptVaultAutoConfiguration; +import com.bol.crypt.CryptOperationException; +import com.bol.crypt.CryptVault; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import java.util.Base64; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@ActiveProfiles("autoconfig") +@EnableAutoConfiguration +@SpringBootTest(classes = {EncryptionConfiguredSystemTest.class, CryptVaultAutoConfiguration.class}) +public class EncryptionConfiguredSystemTest { + private static final byte[] cleartext = ("Lorem ipsum dolor sit amet, " + + "consectetur adipiscing elit, sed do eiusmod tempor incididunt ut " + + "labore et dolore magna aliqua.").getBytes(); + + @Autowired(required = false) + CryptVault cryptVault; + + @Test + public void sanityTest() { + assertThat(cryptVault).isNotNull(); + assertThat(cryptVault.keyVersions.size()).isEqualTo(6); + } + + @Test + public void specifyCipherInExternalConfig() { + var secondKeyVersion = cryptVault.keyVersions.get(2).orElseThrow(); + byte[] encryptedBlob = cryptVault.encrypt(secondKeyVersion, cleartext); + + // version + len(cleartext) w/o padding + assertThat(encryptedBlob.length).isEqualTo(1 + 1 + 1 + encryptedBlob[2] + cleartext.length); + } + + @Test + public void legacyKeyVersionShouldThrowWhenUsedForNewEncryption() { + // CryptVault 1 did not specify a CryptVault protocol version in the encrypted blob + var legacyVersion = cryptVault.keyVersions.get(1).orElseThrow(); + var t = assertThrows( + CryptOperationException.class, + () -> cryptVault.encrypt(legacyVersion, cleartext) + ); + + assertThat(t.getMessage()).startsWith("cannot encrypt with legacy key version"); + } + + @Test + public void legacyKeyVersionShouldBeAbleToDecryptLegacyEncryption() { + var legacyBlob = Base64.getDecoder().decode("gV4dQBm9mYJ1JC3DDs7Wj4cdbJKJALhIPktD4AT2sq4/"); + // in legacy version, 0x80 (-128) was version 0, 0x81 (-127) was version 1, etc. + assertThat(legacyBlob[0]).isEqualTo((byte) 0x81); + byte[] recoveredCleartextAsBytes = cryptVault.decrypt(legacyBlob); + assertThat(new String(recoveredCleartextAsBytes)).isEqualTo("lorem ipsum"); + } + + @Test + public void blobNotMarkedAsLegacyShouldFailDecryption() { + var legacyBlob = Base64.getDecoder().decode("gl4dQBm9mYJ1JC3DDs7Wj4cdbJKJALhIPktD4AT2sq4/"); + assertThat(legacyBlob[0]).isEqualTo((byte) 0x82); + var t = assertThrows(CryptOperationException.class, () -> cryptVault.decrypt(legacyBlob)); + assertThat(t.getMessage()).startsWith("cryptvault protocol version in encrypted blob is unknown:"); + } + + @Test + public void noTransformationSpecifiedShouldFallBackToAesCbcPkcs5Padding() { + var keyVersionWithoutTransformation = cryptVault.keyVersions.get(6).orElseThrow(); + byte[] recoveredCleartext = cryptVault.decrypt(cryptVault.encrypt(keyVersionWithoutTransformation, cleartext)); + assertThat(recoveredCleartext).isEqualTo(cleartext); + } +} diff --git a/src/test/java/com/bol/system/autoconfig/EncryptionConfiguredTest.java b/src/test/java/com/bol/system/autoconfig/EncryptionConfiguredTest.java deleted file mode 100644 index 1608932..0000000 --- a/src/test/java/com/bol/system/autoconfig/EncryptionConfiguredTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.bol.system.autoconfig; - -import com.bol.config.CryptVaultAutoConfiguration; -import com.bol.crypt.CryptVault; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit4.SpringRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -@ActiveProfiles("autoconfig") -@RunWith(SpringRunner.class) -@EnableAutoConfiguration -@SpringBootTest(classes = {EncryptionConfiguredTest.class, CryptVaultAutoConfiguration.class}) -public class EncryptionConfiguredTest { - - @Autowired(required = false) CryptVault cryptVault; - - @Test - public void sanityTest() { - assertThat(cryptVault).isNotNull(); - assertThat(cryptVault.size()).isEqualTo(1); - } -} diff --git a/src/test/java/com/bol/system/autoconfig/EncryptionNotConfiguredTest.java b/src/test/java/com/bol/system/autoconfig/EncryptionNotConfiguredSystemTest.java similarity index 59% rename from src/test/java/com/bol/system/autoconfig/EncryptionNotConfiguredTest.java rename to src/test/java/com/bol/system/autoconfig/EncryptionNotConfiguredSystemTest.java index 5ca4437..61b731f 100644 --- a/src/test/java/com/bol/system/autoconfig/EncryptionNotConfiguredTest.java +++ b/src/test/java/com/bol/system/autoconfig/EncryptionNotConfiguredSystemTest.java @@ -1,21 +1,19 @@ package com.bol.system.autoconfig; import com.bol.crypt.CryptVault; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) @EnableAutoConfiguration -@SpringBootTest(classes = EncryptionNotConfiguredTest.class) -public class EncryptionNotConfiguredTest { +@SpringBootTest(classes = EncryptionNotConfiguredSystemTest.class) +public class EncryptionNotConfiguredSystemTest { - @Autowired(required = false) CryptVault cryptVault; + @Autowired(required = false) + CryptVault cryptVault; @Test public void sanityTest() { diff --git a/src/test/resources/application-autoconfig.yml b/src/test/resources/application-autoconfig.yml index 482a509..c0095be 100644 --- a/src/test/resources/application-autoconfig.yml +++ b/src/test/resources/application-autoconfig.yml @@ -2,4 +2,19 @@ cryptvault: default-key: 1 keys: - version: 1 - key: hqHKBLV83LpCqzKpf8OvutbCs+O5wX5BPu3btWpEvXA= + key: 5DAYjqpqKHK8tzyS6IJEXtsQM/ZQAZ8BOJPgCsQe1sM= + legacy: yes + - version: 2 + key: h7giVR4xH4RDYj4VLSCkSUVyxQQoqEgvjWNIStjE0oM= + transformation: AES/CTR/NoPadding + - version: 3 + key: X89VgG8A9F2DNZCBUezWBetujpQfvG6bJxytTNWrWM8= + transformation: AES/ECB/Pkcs5Padding + - version: 4 + key: VWSMXzwVOT5S887/abibix0zMKghSDFswm2C2SLkFrk= + transformation: ChaCha20-Poly1305 + - version: 5 + key: j+kZRwQUSu0nseG6LUPecHUoHEintYn9u673rLAPU+s= + transformation: AES/GCM/NoPadding + - version: 6 + key: 2FHtWpob9UPrnz7FCT5LnlgmZ6ZqB5U8oQIBJkk+dAc=