Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: externalize config #4

Merged
merged 1 commit into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 121 additions & 20 deletions README.md
Nonius marked this conversation as resolved.
Show resolved Hide resolved
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`:
Nonius marked this conversation as resolved.
Show resolved Hide resolved

```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
Nonius marked this conversation as resolved.
Show resolved Hide resolved

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:
svandenakker marked this conversation as resolved.
Show resolved Hide resolved

```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
Nonius marked this conversation as resolved.
Show resolved Hide resolved
# the new version (can encrypt/decrypt as usual)
- version: 2
key: CqeKXVZuDbeMk0/h1zZrBG0Mul4qMnqShaGjkxWrlQ0=
```
Nonius marked this conversation as resolved.
Show resolved Hide resolved

### 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/17/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</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
Loading