From 87bf6538cf56614eb7a358cf14daeb1dd7f27b68 Mon Sep 17 00:00:00 2001 From: Stefan van den Akker Date: Wed, 10 Apr 2024 11:56:08 +0200 Subject: [PATCH] feat: externalize config This commit adds the ability for a user to specify the algorithm/mode of operation/padding directly in their `application.yml`. This is pretty flexible and allows the user easy access to many JCA "transformations" without them needing to write any code. A new, incompatible format for the encrypted binary blob is introduced to achieve this. The versioned format allows us to make continuous improvements to it without rendering all previous outputs undecryptable. Provisions were made for version-1 outputs: these can still be decrypted. When migrating from version 1 to version 2, legacy key versions should be marked as such in the config. These key versions are then only allowed to decrypt: no new encryptions can be performed with them. --- README.md | 141 +++++++-- pom.xml | 34 ++- .../config/CryptVaultAutoConfiguration.java | 70 +++-- .../bol/crypt/CryptOperationException.java | 3 + src/main/java/com/bol/crypt/CryptVault.java | 280 ++++++++++-------- src/main/java/com/bol/crypt/CryptVersion.java | 19 -- src/main/java/com/bol/crypt/KeyVersion.java | 60 ++++ src/main/java/com/bol/crypt/KeyVersions.java | 114 +++++++ .../java/com/bol/crypt/CryptVaultTest.java | 127 ++++++-- .../EncryptionConfiguredSystemTest.java | 78 +++++ .../autoconfig/EncryptionConfiguredTest.java | 28 -- ...=> EncryptionNotConfiguredSystemTest.java} | 12 +- src/test/resources/application-autoconfig.yml | 17 +- 13 files changed, 714 insertions(+), 269 deletions(-) delete mode 100644 src/main/java/com/bol/crypt/CryptVersion.java create mode 100644 src/main/java/com/bol/crypt/KeyVersion.java create mode 100644 src/main/java/com/bol/crypt/KeyVersions.java create mode 100644 src/test/java/com/bol/system/autoconfig/EncryptionConfiguredSystemTest.java delete mode 100644 src/test/java/com/bol/system/autoconfig/EncryptionConfiguredTest.java rename src/test/java/com/bol/system/autoconfig/{EncryptionNotConfiguredTest.java => EncryptionNotConfiguredSystemTest.java} (59%) diff --git a/README.md b/README.md index 3b64139..f4c6082 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 is consistent with JCA parlance. + +## 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..0e40d77 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ cryptvault jar cryptvault - 3-1.0.2 + 3-2.0.0 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..b31e297 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,97 @@ 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=")); + public void keyVersionIsDerivedFromEncryptedBlob() { + var firstKeyVersion = cryptVault.keyVersions.get(1).orElseThrow(); + var secondKeyVersion = new KeyVersion( + 2, + "ChaCha20-Poly1305", + Base64.getDecoder().decode("IqWTpi549pJDZ1kuc9HppcMxtPfu2SP6Idlh+tz4LL4=")); + cryptVault.keyVersions.addVersion(secondKeyVersion); + + byte[] encryptedUnderFirstKeyBytes = cryptVault.encrypt(firstKeyVersion, plainBytes); + byte[] encryptedUnderSecondKeyBytes = cryptVault.encrypt(secondKeyVersion, plainBytes); + + assertThat(encryptedUnderFirstKeyBytes[1]).isEqualTo((byte) 1); + assertThat(encryptedUnderSecondKeyBytes[1]).isEqualTo((byte) 2); + + assertThat(cryptVault.decrypt(encryptedUnderFirstKeyBytes)).isEqualTo(plainBytes); + assertThat(cryptVault.decrypt(encryptedUnderSecondKeyBytes)).isEqualTo(plainBytes); + } + + @Test + public void differentKeyVersionsShouldLiveSideBySide() { + 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); - byte[] encryptedBytes = cryptVault.encrypt(1, plainBytes); + var algoParamSpec = new GCMParameterSpec(128, reusedIv); - byte[] encryptedBytes2 = cryptVault.encrypt(2, plainBytes); + byte[] firstEncryptedBlob = cryptVault.encrypt(aes256GcmVersion, plainBytes, algoParamSpec); + assertThat(firstEncryptedBlob[1]).isEqualTo((byte) 1); + assertThat(Arrays.copyOfRange(firstEncryptedBlob, 3, 3 + firstEncryptedBlob[2])).contains(reusedIv); - assertThat(fromSignedByte(encryptedBytes[0])).isEqualTo(1); - assertThat(fromSignedByte(encryptedBytes2[0])).isEqualTo(2); + 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=