Add VaultBytesEncryptor and VaultBytesKeyGenerator.

We now provide a Spring Security integration for Vault transit-based BytesEncryptor and BytesKeyGenerator.

VaultOperations operations = …;
VaultBytesKeyGenerator generator = new VaultBytesKeyGenerator(operations);

byte[] key = generator.generateKey();

VaultTransitOperations transit = …;
VaultBytesEncryptor encryptor = new VaultBytesEncryptor(transit, "my-key-name");

byte[] ciphertext = encryptor.encrypt(plaintext);
byte[] decrypted = encryptor.decrypt(ciphertext);

Closes gh-187.
This commit is contained in:
Mark Paluch
2017-10-25 15:39:34 +02:00
parent 3e6fbe7581
commit 28ff17d4f8
10 changed files with 352 additions and 0 deletions

10
pom.xml
View File

@@ -130,6 +130,16 @@
<scope>import</scope>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-bom</artifactId>
<version>5.0.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Testing -->
<dependency>

View File

@@ -83,6 +83,14 @@
<optional>true</optional>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
<optional>true</optional>
</dependency>
<!-- HTTP clients -->
<dependency>

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.vault.security;
import org.springframework.security.crypto.codec.Utf8;
import org.springframework.security.crypto.encrypt.BytesEncryptor;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.vault.core.VaultTransitOperations;
import org.springframework.vault.support.Ciphertext;
import org.springframework.vault.support.Plaintext;
/**
* Vault-based {@link BytesEncryptor} using Vault's {@literal transit} backend.
* Encryption/decryption is bound to a particular key that must support encryption and
* decryption.
*
* @author Mark Paluch
* @since 2.0
*/
public class VaultBytesEncryptor implements BytesEncryptor {
private final VaultTransitOperations transitOperations;
private final String keyName;
/**
* Create a new {@link VaultBytesEncryptor} given {@link VaultTransitOperations} and
* {@code keyName}.
*
* @param transitOperations must not be {@literal null}.
* @param keyName must not be {@literal null} or empty.
*/
public VaultBytesEncryptor(VaultTransitOperations transitOperations, String keyName) {
Assert.notNull(transitOperations, "VaultTransitOperations must not be null");
Assert.hasText(keyName, "Key name must not be null or empty");
this.transitOperations = transitOperations;
this.keyName = keyName;
}
@Override
public byte[] encrypt(byte[] plaintext) {
Assert.notNull(plaintext, "Plaintext must not be null");
Assert.isTrue(!ObjectUtils.isEmpty(plaintext), "Plaintext must not be empty");
Ciphertext ciphertext = transitOperations.encrypt(keyName,
Plaintext.of(plaintext));
return Utf8.encode(ciphertext.getCiphertext());
}
@Override
public byte[] decrypt(byte[] ciphertext) {
Assert.notNull(ciphertext, "Ciphertext must not be null");
Assert.isTrue(!ObjectUtils.isEmpty(ciphertext), "Ciphertext must not be empty");
Plaintext plaintext = transitOperations.decrypt(keyName,
Ciphertext.of(Utf8.decode(ciphertext)));
return plaintext.getPlaintext();
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.vault.security;
import java.util.Collections;
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
import org.springframework.util.Assert;
import org.springframework.util.Base64Utils;
import org.springframework.vault.core.VaultOperations;
import org.springframework.vault.support.VaultResponse;
/**
* Random byte generator using Vault's {@code transit} backend to generate high-quality
* random bytes of the configured length.
* <p>
* Using Vault ensures to use a high-entropy source preventing to consume entropy of the
* local machine.
*
* @author Mark Paluch
* @since 2.0
*/
public class VaultBytesKeyGenerator implements BytesKeyGenerator {
private final VaultOperations vaultOperations;
private final int length;
private String transitPath;
/**
* Creates a new {@link VaultBytesKeyGenerator} initialized to generate {@link 32}
* random bytes using {@code transit} for transit mount path.
*
* @param vaultOperations must not be {@literal null}.
*/
public VaultBytesKeyGenerator(VaultOperations vaultOperations) {
this(vaultOperations, "transit", 32);
}
/**
* Creates a new {@link VaultBytesKeyGenerator} initialized to generate {@code length}
* random bytes.
*
* @param vaultOperations must not be {@literal null}.
* @param transitPath path of the transit backend, must not be {@literal null} or
* empty.
* @param length number of random bytes to generate. Must be greater than zero.
*/
public VaultBytesKeyGenerator(VaultOperations vaultOperations, String transitPath,
int length) {
Assert.notNull(vaultOperations, "VaultOperations must not be null");
Assert.hasText(transitPath, "Transit path must not be null or empty");
Assert.isTrue(length > 0, "Byte count must be greater zero");
this.vaultOperations = vaultOperations;
this.transitPath = transitPath;
this.length = length;
}
@Override
public int getKeyLength() {
return length;
}
@Override
public byte[] generateKey() {
VaultResponse response = vaultOperations.write(
String.format("%s/random/%d", transitPath, getKeyLength()),
Collections.singletonMap("format", "base64"));
String randomBytes = (String) response.getRequiredData().get("random_bytes");
return Base64Utils.decodeFromString(randomBytes);
}
}

View File

@@ -0,0 +1,7 @@
/**
* Integration with Spring Security.
*/
@org.springframework.lang.NonNullApi
@org.springframework.lang.NonNullFields
package org.springframework.vault.security;

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.vault.security;
import org.junit.Before;
import org.junit.Test;
import org.springframework.vault.core.VaultTransitOperations;
import org.springframework.vault.util.IntegrationTestSupport;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link VaultBytesEncryptor}.
*
* @author Mark Paluch
*/
public class VaultBytesEncryptorIntegrationTests extends IntegrationTestSupport {
static final String KEY_NAME = "security-encryptor";
private VaultTransitOperations transit;
@Before
public void before() {
transit = prepare().getVaultOperations().opsForTransit();
if (!prepare().hasSecret("transit")) {
prepare().mountSecret("transit");
}
if (!transit.getKeys().contains(KEY_NAME)) {
transit.createKey(KEY_NAME);
}
}
@Test
public void shouldEncryptAndDecrypt() {
VaultBytesEncryptor encryptor = new VaultBytesEncryptor(transit, KEY_NAME);
byte[] plaintext = "foo-bar+ü¿ß~€¢".getBytes();
byte[] ciphertext = encryptor.encrypt(plaintext);
byte[] result = encryptor.decrypt(ciphertext);
assertThat(ciphertext).isNotEqualTo(plaintext).startsWith("vault:v1".getBytes());
assertThat(result).isEqualTo(plaintext);
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.vault.security;
import org.junit.Before;
import org.junit.Test;
import org.springframework.vault.util.IntegrationTestSupport;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link VaultBytesKeyGenerator}.
*
* @author Mark Paluch
*/
public class VaultBytesKeyGeneratorIntegrationTests extends IntegrationTestSupport {
@Before
public void before() {
if (!prepare().hasSecret("transit")) {
prepare().mountSecret("transit");
}
}
@Test
public void shouldGenerateRandomBytes() {
VaultBytesKeyGenerator generator = new VaultBytesKeyGenerator(prepare()
.getVaultOperations(), "transit", 16);
assertThat(generator.generateKey()).hasSize(16);
assertThat(generator.getKeyLength()).isEqualTo(16);
}
}

View File

@@ -12,6 +12,7 @@
* Support CSR signing, certificate revocation and CRL retrieval.
* <<vault.authentication.kubernetes,Kubernetes authentication>>.
* RoleId/SecretId unwrapping for <<vault.authentication.approle,AppRole authentication>>.
* <<vault.misc.spring-security,Spring Security integration>> with transit backend-based `BytesKeyGenerator` and `BytesEncryptor`.
[[new-features.1-1-0]]
=== What's new in Spring Vault 1.1.0

View File

@@ -0,0 +1,42 @@
[[vault.misc]]
= Miscellaneous
Learn in this chapter about details worth mentioning like the Spring Security integration.
[[vault.misc.spring-security]]
== Spring Security
Spring Vault integrates with Spring Security by providing implementations for https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#spring-security-crypto-keygenerators[`BytesKeyGenerator`] and https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#spring-security-crypto-encryption[`BytesEncryptor`]. Both implementations use Vault's `transit` backend.
.`VaultBytesKeyGenerator` example
====
[source,java]
----
VaultOperations operations = …;
VaultBytesKeyGenerator generator = new VaultBytesKeyGenerator(operations);
byte[] key = generator.generateKey();
----
====
.`VaultBytesEncryptor` example
====
[source,java]
----
VaultTransitOperations transit = …;
VaultBytesEncryptor encryptor = new VaultBytesEncryptor(transit, "my-key-name");
byte[] ciphertext = encryptor.encrypt(plaintext);
byte[] result = encryptor.decrypt(ciphertext);
----
====
Vault encapsulates an entropy source that is decoupled from your JVM along with server-side key-management. This relieves the burden of proper encryption/decryption from application developers and pushes the burden onto the operators of Vault. Operators of Vault commonly include the security team at an organization, which means they can ensure that data is encrypted/decrypted properly. Additionally, since encrypt/decrypt operations must enter the audit log, any decryption event is recorded.
The backend also supports key rotation, which allows a new version of the named key to be generated. All data encrypted with the key will use the newest version of the key; previously encrypted data can be decrypted using old versions of the key. Administrators can control which previous versions of a key are available for decryption, to prevent an attacker gaining an old copy of ciphertext to be able to successfully decrypt it.
Vault is after all a networked service that incurs each operation with a latency. Components heavily using encryption or random bytes generation may experience a difference in throughput and performance.

View File

@@ -30,3 +30,5 @@ include::vault-repositories.adoc[]
include::client-support.adoc[]
include::authentication.adoc[]
include::misc.adoc[]