Skip to content

Commit

Permalink
Add VaultBytesEncryptor and VaultBytesKeyGenerator.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mp911de committed Jan 22, 2018
1 parent 3e6fbe7 commit 28ff17d
Show file tree
Hide file tree
Showing 10 changed files with 352 additions and 0 deletions.
10 changes: 10 additions & 0 deletions pom.xml
Expand Up @@ -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>
Expand Down
8 changes: 8 additions & 0 deletions spring-vault-core/pom.xml
Expand Up @@ -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>
Expand Down
@@ -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();
}
}
@@ -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);
}
}
@@ -0,0 +1,7 @@
/**
* Integration with Spring Security.
*/
@org.springframework.lang.NonNullApi
@org.springframework.lang.NonNullFields
package org.springframework.vault.security;

@@ -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);
}
}
@@ -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);
}
}
1 change: 1 addition & 0 deletions src/main/asciidoc/new-features.adoc
Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions src/main/asciidoc/reference/misc.adoc
@@ -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.
2 changes: 2 additions & 0 deletions src/main/asciidoc/reference/vault.adoc
Expand Up @@ -30,3 +30,5 @@ include::vault-repositories.adoc[]
include::client-support.adoc[]

include::authentication.adoc[]

include::misc.adoc[]

0 comments on commit 28ff17d

Please sign in to comment.