Skip to content

Commit 87bf653

Browse files
committed
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.
1 parent 97f5b5b commit 87bf653

13 files changed

Lines changed: 714 additions & 269 deletions

README.md

Lines changed: 121 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,28 @@
11
[![Maven Central](https://img.shields.io/maven-central/v/com.bol/cryptvault.svg)](http://search.maven.org/#search%7Cga%7C1%7Ccom.bol)
22
[![Build](https://github.com/bolcom/cryptvault/actions/workflows/maven.yml/badge.svg)](https://github.com/bolcom/cryptvault/actions)
33

4-
# Cryptvault
4+
# Cryptvault: versioned, secure, generic encryption/decryption in Java
55

6-
Allows for a versioned, secure generic crypt/decrypt in java.
7-
8-
Originally developed for [spring-data-mongodb-encrypt](https://github.com/bolcom/spring-data-mongodb-encrypt), it is now offered as a general use library.
6+
> When in doubt, encrypt. When not in doubt, be in doubt.
97
108
## Features
119

1210
- key versioning (to help migrating to new key without need to convert data)
1311
- uses 256-bit AES by default
14-
- supports any encryption available in Java (via JCE)
12+
- supports any encryption available in Java (via Java Cryptography Architecture
13+
or JCA)
1514
- simple
1615
- no dependencies
1716

18-
## Use
17+
## Usage
1918

2019
Add dependency:
2120

2221
```xml
2322
<dependency>
2423
<groupId>com.bol</groupId>
2524
<artifactId>cryptvault</artifactId>
26-
<version>1.0.2</version>
25+
<version>3-2.0.0</version>
2726
</dependency>
2827
```
2928

@@ -52,23 +51,26 @@ byte[] decrypted = cryptVault.decrypt(encrypted);
5251
new String(decrypted).equals("rock"); // true
5352
```
5453

55-
## Manual configuration
56-
57-
You can also configure `CryptVault` yourself. Look at [how spring autoconfig configures CryptVault](src/main/java/com/bol/config/CryptVaultAutoConfiguration.java) for details.
58-
5954
## Keys
6055

61-
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.
56+
This library uses the encryption keys specified in the configuration directly.
57+
Notably, it does not use any key-derivation. That means that you are responsible
58+
for providing a key from a high-entropy source.
6259

63-
To generate a key, you can use the following command line:
60+
The length of the key depends on the algorithm specified. When using AES-256,
61+
you need to provide a key that is 256 bits/32 bytes long. (For comparison, the
62+
weak DES uses 64-bit keys.)
6463

65-
```
66-
dd if=/dev/urandom bs=1 count=32 | base64
64+
To generate a key suitable for AES-256 bit, you can use the following command:
65+
66+
```console
67+
$ dd if=/dev/urandom bs=1 count=32 | base64
6768
```
6869

69-
## Exchange keys
70+
## Rotating keys
7071

71-
It is advisable to rotate your keys every now and then. To do so, define a new key version in `application.yml`:
72+
It is advisable to rotate your keys every now and then. To do so, define a new
73+
key version in `application.yml`:
7274

7375
```yaml
7476
cryptvault:
@@ -79,7 +81,13 @@ cryptvault:
7981
key: ge2L+MA9jLA8UiUJ4z5fUoK+Lgj2yddlL6EzYIBqb1Q=
8082
```
8183
82-
`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.
84+
CryptVault automatically uses the highest versioned key for encryption by
85+
default, but supports decryption using any of the keys. This allows you to
86+
deploy a new key, and either let old data slowly get phased out, or run a
87+
nightly load+save batch job to force key migration. Once all old keys are phased
88+
out, you may remove the old key from the configuration.
89+
90+
## Specify default key version
8391
8492
You can use
8593
@@ -88,9 +96,102 @@ cryptvault:
8896
default-key: 1
8997
```
9098
91-
to override which version of the defined keys is considered 'default'.
99+
to override which version of the defined keys is considered default.
100+
101+
## Specify encryption algorithm
102+
103+
Instead of using the default AES-256 in CBC mode, you can specify the algorithm,
104+
mode of operation and padding scheme directly in the configuration:
105+
106+
```yaml
107+
cryptvault:
108+
keys:
109+
version: 1
110+
key: Ifw/+pLuWBjn7a1mjuToQ8hpIh8DV0WLf9b4z7iinGs=
111+
transformation: AES/GCM/NoPadding
112+
```
113+
114+
You can use all the algorithms specified by JCA. Other valid transformations
115+
are, for example, "DES/CTR/NoPadding" and "ChaCha20-Poly1305". For a
116+
comprehensive list, see [Java Security Standard Algorithm Names][Java Security
117+
Standard Algorithm Names].
118+
119+
The YAML key is called "transformation" because it signifies more than just an
120+
algorithm, but rather a set of operations performed on an input to produce some
121+
output. Naming it this way is consistent with JCA parlance.
122+
123+
## Format of the encrypted blob
124+
125+
The encrypted blobs look like (numbers are bits):
126+
127+
```
128+
0 8 16 24
129+
+---------+---------+---------+--------------------+--------------------+
130+
|proto |key |param |params |ciphertext |
131+
|version |version |length | ... | ... |
132+
|8 |8 |8 |[0,255] |[16,inf) |
133+
+---------+---------+---------+--------------------+--------------------+
134+
```
92135

136+
* `proto version` is the protocol version of this blob. Having a version allows
137+
making improvements to this blob over time without having to decrypt all the
138+
old encryptions and encrypt it under a new (versionless) version.
139+
* `key version` is the user-controlled version of the key that was used to
140+
encrypt the data in this blob.
141+
* `param length` is the length of next field, the algorithm parameters
142+
* `params` are the algorithms parameters that that need to be known
143+
in order to decrypt the blob successfully. For example, when using
144+
AES/CBC/PKCS5Padding, this will (among some overhead) contain the 16-byte IV.
145+
See `java.security.AlgorithmParameters#getEncoded` for more information.
146+
* `ciphertext` contains the output of applying the specified transformation
147+
under the specified key to the input.
93148

94149
## Expected size of encrypted data
95150

96-
Depending on how much padding is used, you can expect 17..33 bytes for encryption overhead (salt + padding).
151+
Depending on the cipher, whether an IV or tag are used and the padding scheme
152+
you must expect some overhead for encryption. The default cipher, AES-256-CBC
153+
with PKCS #5 padding, requires an extra [22, 37] bytes: proto version (1) + key
154+
version (1) + param length (1) + algorithm parameters (18) + padding (best case:
155+
1, worst case: 16).
156+
157+
## Migrating from version 1 to version 2
158+
159+
### TL;DR:
160+
161+
1. Add `legacy: true` to keys that were in use under version 1.
162+
2. Create a new key version that will be used for new encryptions.
163+
164+
```yaml
165+
cryptvault:
166+
keys:
167+
# the legacy key version (can only decrypt!)
168+
- version: 1
169+
key: yaF4Gi13Gp+gF5Tm+jMkYbQKMO3c6KYZbQmMqXQyid0=
170+
legacy: true
171+
# the new version (can encrypt/decrypt as usual)
172+
- version: 2
173+
key: CqeKXVZuDbeMk0/h1zZrBG0Mul4qMnqShaGjkxWrlQ0=
174+
```
175+
176+
### More detail
177+
178+
Version 2 introduced a new format of the binary blob. This provides certain
179+
benefits (see under [Format of the encrypted blob,
180+
above](#format-of-the-encrypted-blob)). However, the old encrypted blobs have
181+
become incompatible as a result of this breaking change. You can still decrypt
182+
the blobs, however. Encrypting with these legacy key versions is not supported,
183+
however.
184+
185+
To migrate:
186+
187+
1. Add `legacy: true` to the legacy key version(s) in the config.
188+
2. Create a new key version that will be used for new encryptions.
189+
190+
Old encrypted blobs will not be updated automatically since this library does
191+
not handle persistence. There is little harm in keeping them around as they
192+
are still secure. However, should you wish to upgrade the stored blobs, decrypt
193+
them and then overwrite them with a fresh encrypted version under the new key
194+
version.
195+
196+
[Java Security Standard Algorithm Names]:
197+
<https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html>

pom.xml

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<artifactId>cryptvault</artifactId>
88
<packaging>jar</packaging>
99
<name>cryptvault</name>
10-
<version>3-1.0.2</version>
10+
<version>3-2.0.0</version>
1111
<description>Versioned crypto library</description>
1212
<url>https://github.com/bolcom/cryptvault</url>
1313

@@ -51,26 +51,20 @@
5151
<dependency>
5252
<groupId>org.springframework.boot</groupId>
5353
<artifactId>spring-boot-autoconfigure</artifactId>
54-
<version>3.2.3</version>
54+
<version>3.3.2</version>
5555
<scope>provided</scope>
5656
</dependency>
5757

5858
<dependency>
5959
<groupId>org.springframework.boot</groupId>
6060
<artifactId>spring-boot-starter-test</artifactId>
61-
<version>3.2.3</version>
62-
<scope>test</scope>
63-
</dependency>
64-
<dependency>
65-
<groupId>junit</groupId>
66-
<artifactId>junit</artifactId>
67-
<version>4.13.2</version>
61+
<version>3.3.2</version>
6862
<scope>test</scope>
6963
</dependency>
7064
<dependency>
7165
<groupId>org.assertj</groupId>
7266
<artifactId>assertj-core</artifactId>
73-
<version>3.25.3</version>
67+
<version>3.26.3</version>
7468
<scope>test</scope>
7569
</dependency>
7670
</dependencies>
@@ -81,8 +75,24 @@
8175
<artifactId>maven-compiler-plugin</artifactId>
8276
<version>3.6.1</version>
8377
<configuration>
84-
<source>1.8</source>
85-
<target>1.8</target>
78+
<source>17</source>
79+
<target>17</target>
80+
</configuration>
81+
</plugin>
82+
<plugin>
83+
<groupId>org.apache.maven.plugins</groupId>
84+
<artifactId>maven-failsafe-plugin</artifactId>
85+
<version>3.3.1</version>
86+
<executions>
87+
<execution>
88+
<goals>
89+
<goal>integration-test</goal>
90+
</goals>
91+
<phase>integration-test</phase>
92+
</execution>
93+
</executions>
94+
<configuration>
95+
<includes>*SystemTest.java</includes>
8696
</configuration>
8797
</plugin>
8898
</plugins>
Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,97 @@
11
package com.bol.config;
22

33
import com.bol.crypt.CryptVault;
4+
import com.bol.crypt.KeyVersion;
5+
import com.bol.crypt.KeyVersions;
46
import org.springframework.boot.autoconfigure.AutoConfiguration;
57
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
68
import org.springframework.boot.context.properties.ConfigurationProperties;
9+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
710
import org.springframework.context.annotation.Bean;
8-
import org.springframework.stereotype.Component;
911

10-
import java.util.Base64;
1112
import java.util.List;
13+
import java.util.Objects;
1214

1315
@AutoConfiguration
1416
@ConditionalOnProperty("cryptvault.keys[0].key")
17+
@EnableConfigurationProperties(value = {CryptVaultAutoConfiguration.CryptVaultConfigurationProperties.class})
1518
public class CryptVaultAutoConfiguration {
1619

1720
@Bean
1821
CryptVault cryptVault(CryptVaultConfigurationProperties properties) {
19-
CryptVault cryptVault = new CryptVault();
20-
if (properties.keys == null || properties.keys.isEmpty()) throw new IllegalArgumentException("property 'keys' is not set");
22+
if (properties.keys == null || properties.keys.isEmpty()) {
23+
throw new IllegalStateException("property 'keys' is not set");
24+
}
2125

22-
for (Key key : properties.keys) {
23-
byte[] secretKeyBytes = Base64.getDecoder().decode(key.key);
24-
cryptVault.with256BitAesCbcPkcs5PaddingAnd128BitSaltKey(key.version, secretKeyBytes);
26+
KeyVersions versions = new KeyVersions();
27+
for (KeyVersionProperties props : properties.keys) {
28+
Objects.requireNonNull(props.key, String.format("key version %d has a null key", props.version));
29+
if (props.version < 1 || props.version > 255) {
30+
throw new IllegalArgumentException(String.format("version should be [1, 255], got %d", props.version));
31+
}
32+
if (props.transformation == null) props.transformation = "AES/CBC/PKCS5Padding";
33+
versions.addVersion(new KeyVersion(props.version, props.transformation, props.key, props.legacy));
2534
}
2635

2736
if (properties.defaultKey != null) {
28-
cryptVault.withDefaultKeyVersion(properties.defaultKey);
37+
if (properties.defaultKey < 1 || properties.defaultKey > 255) {
38+
var msg = String.format("default key version should be in [1, 255], was %d", properties.defaultKey);
39+
throw new IllegalStateException(msg);
40+
}
41+
versions.get(properties.defaultKey).ifPresentOrElse(
42+
versions::setDefault,
43+
() -> {
44+
var msg = String.format("no version %d registered; cannot make default", properties.defaultKey);
45+
throw new IllegalStateException(msg);
46+
});
2947
}
3048

31-
return cryptVault;
49+
return CryptVault.of(versions);
3250
}
3351

34-
@Component
3552
@ConfigurationProperties("cryptvault")
3653
public static class CryptVaultConfigurationProperties {
37-
List<Key> keys;
54+
List<KeyVersionProperties> keys;
3855
Integer defaultKey;
3956

40-
public void setKeys(List<Key> keys) {
57+
public void setKeys(List<KeyVersionProperties> keys) {
4158
this.keys = keys;
4259
}
4360

4461
public void setDefaultKey(Integer defaultKey) {
4562
this.defaultKey = defaultKey;
4663
}
47-
48-
public List<Key> getKeys() {
49-
return keys;
50-
}
51-
52-
public Integer getDefaultKey() {
53-
return defaultKey;
54-
}
5564
}
5665

57-
public static class Key {
66+
public static class KeyVersionProperties {
5867
int version;
68+
String transformation;
5969
String key;
70+
boolean legacy;
6071

6172
public void setVersion(int version) {
6273
this.version = version;
6374
}
6475

76+
public void setTransformation(String transformation) {
77+
this.transformation = transformation;
78+
}
79+
6580
public void setKey(String key) {
6681
this.key = key;
6782
}
6883

69-
public int getVersion() {
70-
return version;
84+
public void setLegacy(boolean legacy) {
85+
this.legacy = legacy;
7186
}
7287

73-
public String getKey() {
74-
return key;
88+
@Override
89+
public String toString() {
90+
return "KeyVersionProperties{" +
91+
"version=" + version +
92+
", transformation='" + transformation + '\'' +
93+
", keyBase64='" + key + '\'' +
94+
'}';
7595
}
7696
}
7797
}

src/main/java/com/bol/crypt/CryptOperationException.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package com.bol.crypt;
22

3+
/**
4+
* Wraps different JCA exceptions under a single umbrella.
5+
*/
36
public class CryptOperationException extends RuntimeException {
47
public CryptOperationException(String s, Throwable e) {
58
super(s, e);

0 commit comments

Comments
 (0)