Skip to content

Commit

Permalink
feat: externalize config
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
svandenakker committed Jul 30, 2024
1 parent 97f5b5b commit 1a8f1af
Show file tree
Hide file tree
Showing 13 changed files with 706 additions and 268 deletions.
141 changes: 121 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,28 @@
[![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:

```xml
<dependency>
<groupId>com.bol</groupId>
<artifactId>cryptvault</artifactId>
<version>1.0.2</version>
<version>3-2.0.0</version>
</dependency>
```

Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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]:
<https://docs.oracle.com/en/java/javase/21/docs/specs/security/standard-names.html>
34 changes: 22 additions & 12 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<artifactId>cryptvault</artifactId>
<packaging>jar</packaging>
<name>cryptvault</name>
<version>3-1.0.2</version>
<version>3-2.0.0-SNAPSHOT</version>
<description>Versioned crypto library</description>
<url>https://github.com/bolcom/cryptvault</url>

Expand Down Expand Up @@ -51,26 +51,20 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>3.2.3</version>
<version>3.3.2</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>3.2.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<version>3.3.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.25.3</version>
<version>3.26.3</version>
<scope>test</scope>
</dependency>
</dependencies>
Expand All @@ -81,8 +75,24 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.3.1</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
</goals>
<phase>integration-test</phase>
</execution>
</executions>
<configuration>
<includes>*SystemTest.java</includes>
</configuration>
</plugin>
</plugins>
Expand Down
70 changes: 45 additions & 25 deletions src/main/java/com/bol/config/CryptVaultAutoConfiguration.java
Original file line number Diff line number Diff line change
@@ -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<Key> keys;
List<KeyVersionProperties> keys;
Integer defaultKey;

public void setKeys(List<Key> keys) {
public void setKeys(List<KeyVersionProperties> keys) {
this.keys = keys;
}

public void setDefaultKey(Integer defaultKey) {
this.defaultKey = defaultKey;
}

public List<Key> 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 + '\'' +
'}';
}
}
}
3 changes: 3 additions & 0 deletions src/main/java/com/bol/crypt/CryptOperationException.java
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
Loading

0 comments on commit 1a8f1af

Please sign in to comment.