Added transit batch encrypt and decrypt support.

We now support batch encryption and decryption via VaultTransitOperations.

List<VaultEncryptionResult> batchEncryption = transitOperations.encrypt("mykey", Arrays.asList(Plaintext.of("hello"), Plaintext.of("world")));

Ciphertext encryptedHello = batchEncryption.get(0).get();
Ciphertext encryptedWorld = batchEncryption.get(1).get();

List<VaultDecryptionResult> batchDecryption = transitOperations.decrypt("mykey", Arrays.asList(encryptedHello, encryptedWorld));

Original pull request: gh-138.
Related ticket: gh-137.
Closes gh-138.
This commit is contained in:
Praveendra Singh
2017-09-13 09:58:34 -07:00
committed by Mark Paluch
parent aa8eb13f66
commit ed920ec0ab
10 changed files with 540 additions and 12 deletions

View File

@@ -246,6 +246,7 @@ The following scripts need to be run prior to building the project for the tests
$ ./src/test/bash/install_vault.sh
$ ./src/test/bash/create_certificates.sh
$ ./src/test/bash/env.sh
$ ./src/test/bash/local_run_vault.sh
Changes to the documentation should be made to the adocs found under `src/main/asciidoc/`

View File

@@ -17,18 +17,23 @@ package org.springframework.vault.core;
import java.util.List;
import org.springframework.vault.support.RawTransitKey;
import org.springframework.vault.support.TransitKeyType;
import org.springframework.vault.support.VaultDecryptionPayload;
import org.springframework.vault.support.VaultDecryptionResult;
import org.springframework.vault.support.VaultEncryptionPayload;
import org.springframework.vault.support.VaultEncryptionResult;
import org.springframework.vault.support.VaultTransitContext;
import org.springframework.vault.support.VaultTransitKey;
import org.springframework.vault.support.VaultTransitKeyConfiguration;
import org.springframework.vault.support.VaultTransitKeyCreationRequest;
import org.springframework.vault.support.RawTransitKey;
/**
* Interface that specifies operations using the {@code transit} backend.
*
* @author Mark Paluch
* @author Sven Schürmann
* @author Praveendra Singh
* @see <a href="https://www.vaultproject.io/docs/secrets/transit/index.html">Transit
* Secret Backend</a>
*/
@@ -122,6 +127,19 @@ public interface VaultTransitOperations {
*/
String encrypt(String keyName, byte[] plaintext, VaultTransitContext transitRequest);
/**
* Encrypts the provided list of plaintext using the named key and context.
* The encryption is done using transit backend's batch operation.
*
* works with Vault 0.6.5 and later.
*
* @param keyName must not be empty or {@literal null}.
* @param batchRequest a list of VaultEncryptionPayload which includes plaintext and optional context
* @return list of cipher text in the same order as in plaintexts.
* throws VaultException in case of not matching context found.
*/
List<VaultEncryptionResult> encrypt(String keyName, List<VaultEncryptionPayload> batchRequest);
/**
* Decrypts the provided plaintext using the named key.
*
@@ -141,6 +159,19 @@ public interface VaultTransitOperations {
*/
byte[] decrypt(String keyName, String ciphertext, VaultTransitContext transitRequest);
/**
* Decrypts the provided list of ciphertext using the named key and context.
* The decryption is done using transit backend's batch operation.
*
* works with Vault 0.6.5 and later.
*
* @param keyName must not be empty or {@literal null}.
* @param batchRequest a list of VaultDecryptionPayload which includes plaintext and optional context
* @return list of plain text in the same order as in ciphertexts.
* throws VaultException in case of not matching context found.
*/
List<VaultDecryptionResult> decrypt(String keyName, List<VaultDecryptionPayload> batchRequest);
/**
* Rewrap the provided ciphertext using the latest version of the named key. Because
* this never returns plaintext, it is possible to delegate this functionality to

View File

@@ -15,30 +15,38 @@
*/
package org.springframework.vault.core;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import org.springframework.util.Assert;
import org.springframework.util.Base64Utils;
import org.springframework.vault.support.RawTransitKey;
import org.springframework.vault.support.TransitKeyType;
import org.springframework.vault.support.VaultDecryptionPayload;
import org.springframework.vault.support.VaultDecryptionResult;
import org.springframework.vault.support.VaultEncryptionDecryptionResultHelper;
import org.springframework.vault.support.VaultEncryptionPayload;
import org.springframework.vault.support.VaultEncryptionResult;
import org.springframework.vault.support.VaultResponse;
import org.springframework.vault.support.VaultResponseSupport;
import org.springframework.vault.support.VaultTransitContext;
import org.springframework.vault.support.VaultTransitKey;
import org.springframework.vault.support.VaultTransitKeyConfiguration;
import org.springframework.vault.support.VaultTransitKeyCreationRequest;
import org.springframework.vault.support.RawTransitKey;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* Default implementation of {@link VaultTransitOperations}.
*
* @author Mark Paluch
* @author Sven Schürmann
* @author Praveendra Singh
*/
public class VaultTransitTemplate implements VaultTransitOperations {
@@ -152,7 +160,7 @@ public class VaultTransitTemplate implements VaultTransitOperations {
.write(String.format("%s/encrypt/%s", path, keyName), request).getData()
.get("ciphertext");
}
@Override
public String encrypt(String keyName, byte[] plaintext,
VaultTransitContext transitRequest) {
@@ -173,6 +181,37 @@ public class VaultTransitTemplate implements VaultTransitOperations {
.get("ciphertext");
}
@Override
public List<VaultEncryptionResult> encrypt(String keyName, List<VaultEncryptionPayload> batchRequest) {
Assert.hasText(keyName, "KeyName must not be empty");
Assert.notEmpty(batchRequest, "batchRequest must not be null and should have at least one entry");
List<Map<String, String>> batch = new ArrayList<Map<String, String>>();
for (VaultEncryptionPayload request : batchRequest) {
Assert.notNull(request.getPlaintext(), "Plain text must not be null");
Map<String, String> vaultRequest = new LinkedHashMap<String, String>();
vaultRequest.put("plaintext", Base64Utils.encodeToString(request.getPlaintext()));
if (request.getContext() != null) {
applyTransitOptions(request.getContext(), vaultRequest);
}
batch.add(vaultRequest);
}
Map<String, List<Map<String, String>>> request = new LinkedHashMap<String, List<Map<String, String>>>();
request.put("batch_input", batch);
VaultResponse vaultResponse = vaultOperations.write(String.format("%s/encrypt/%s", path, keyName), request);
return VaultEncryptionDecryptionResultHelper.fetchEncryptionResult(vaultResponse);
}
@Override
public String decrypt(String keyName, String ciphertext) {
@@ -211,6 +250,37 @@ public class VaultTransitTemplate implements VaultTransitOperations {
return Base64Utils.decodeFromString(plaintext);
}
@Override
public List<VaultDecryptionResult> decrypt(String keyName, List<VaultDecryptionPayload> batchRequest) {
Assert.hasText(keyName, "KeyName must not be empty");
Assert.notEmpty(batchRequest, "batchRequest must not be null and should have at least one entry");
List<Map<String, String>> batch = new ArrayList<Map<String, String>>();
for (VaultDecryptionPayload request : batchRequest) {
Assert.notNull(request.getCiphertext(), "Cipher text must not be null");
Map<String, String> vaultRequest = new LinkedHashMap<String, String>();
vaultRequest.put("ciphertext", request.getCiphertext());
if (request.getContext() != null) {
applyTransitOptions(request.getContext(), vaultRequest);
}
batch.add(vaultRequest);
}
Map<String, List<Map<String, String>>> request = new LinkedHashMap<String, List<Map<String, String>>>();
request.put("batch_input", batch);
VaultResponse vaultResponse = vaultOperations.write(String.format("%s/decrypt/%s", path, keyName), request);
return VaultEncryptionDecryptionResultHelper.fetchDecryptionResult(vaultResponse);
}
@Override
public String rewrap(String keyName, String ciphertext) {
@@ -303,4 +373,5 @@ public class VaultTransitTemplate implements VaultTransitOperations {
private String name;
}
}

View File

@@ -0,0 +1,47 @@
package org.springframework.vault.support;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
/**
* Decryption Value Object used for encrypt() operations.
*
* @author Praveendra Singh
*
*/
@Getter
@Setter
@AllArgsConstructor
public class VaultDecryptionPayload {
private String ciphertext;
private VaultTransitContext context;
/**
* factory method helps to create decryption value object using ciphertext
* in String
*
* @param ciphertext
* to be decrypted
* @return decryption value object
*/
public static VaultDecryptionPayload of(String ciphertext) {
if (ciphertext == null) {
throw new IllegalArgumentException("The ciphertext must not be null");
}
return new VaultDecryptionPayload(ciphertext, null);
}
/**
* sets the decryption context to the value object.
*
* @param context
* transit decryption context
* @return decryption value object
*/
public VaultDecryptionPayload with(VaultTransitContext context) {
return new VaultDecryptionPayload(this.getCiphertext(), context);
}
}

View File

@@ -0,0 +1,39 @@
package org.springframework.vault.support;
import org.springframework.util.StringUtils;
import org.springframework.vault.VaultException;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
/**
* Holds the response from decryption operation and provides helper methods to
* deal with the data.
*
* @author Praveendra Singh
*
*/
@Getter
@Setter
@AllArgsConstructor
public class VaultDecryptionResult {
private byte[] cipherText;
private String error;
/**
* returns the list of plaintext or throws VaultException if error
* encountered.
*
* @return plaintext
*/
public byte[] get() {
if (!StringUtils.isEmpty(error)) {
throw new VaultException(error);
}
return cipherText;
}
}

View File

@@ -0,0 +1,55 @@
package org.springframework.vault.support;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.springframework.util.Base64Utils;
import lombok.Getter;
import lombok.Setter;
/**
* Holds the response from encryption/decryption operation and provides helper
* methods to generate list of encryption/decryption objects by fetching the
* respective fields from VaultResponse.
*
* @author Praveendra Singh
*
*/
@Getter
@Setter
public class VaultEncryptionDecryptionResultHelper {
public static List<VaultEncryptionResult> fetchEncryptionResult(VaultResponse vaultResponse) {
List<VaultEncryptionResult> result = new ArrayList<VaultEncryptionResult>();
for (Map<String, String> data : getBatchData(vaultResponse)) {
VaultEncryptionResult res = new VaultEncryptionResult(data.get("ciphertext"), data.get("error"));
result.add(res);
}
return result;
}
public static List<VaultDecryptionResult> fetchDecryptionResult(VaultResponse vaultResponse) {
List<VaultDecryptionResult> result = new ArrayList<VaultDecryptionResult>();
for (Map<String, String> data : getBatchData(vaultResponse)) {
VaultDecryptionResult res = new VaultDecryptionResult(Base64Utils.decodeFromString(data.get("plaintext")),
data.get("error"));
result.add(res);
}
return result;
}
@SuppressWarnings("unchecked")
protected static List<Map<String, String>> getBatchData(VaultResponse vaultResponse) {
return (List<Map<String, String>>) vaultResponse.getData().get("batch_results");
}
}

View File

@@ -0,0 +1,66 @@
package org.springframework.vault.support;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
/**
* Encryption Value Object used for encrypt() operations.
*
* @author Praveendra Singh
*
*/
@Getter
@Setter
@AllArgsConstructor
public class VaultEncryptionPayload {
private byte[] plaintext;
private VaultTransitContext context;
/**
* factory method helps to create encryption value object using plaintext in
* bytes
*
* @param plaintext
* data to be encrypted
*
* @return encryption value object
*/
public static VaultEncryptionPayload of(byte[] plaintext) {
if ((plaintext == null) || (plaintext.length == 0)) {
throw new IllegalArgumentException("The plaintext must not be null or empty");
}
return new VaultEncryptionPayload(plaintext, null);
}
/**
* factory method helps to create encryption value object using plaintext in
* String
*
* @param plaintext
* data to be encrypted
*
* @return encryption value object
*/
public static VaultEncryptionPayload of(String plaintext) {
if (plaintext == null) {
throw new IllegalArgumentException("The plaintext must not be null");
}
return of(plaintext.getBytes());
}
/**
* sets the encryption context to the value object.
*
* @param context
* transit encryption context
* @return encryption value object
*/
public VaultEncryptionPayload with(VaultTransitContext context) {
return new VaultEncryptionPayload(this.getPlaintext(), context);
}
}

View File

@@ -0,0 +1,39 @@
package org.springframework.vault.support;
import org.springframework.util.StringUtils;
import org.springframework.vault.VaultException;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
/**
* Holds the response from encryption operation and provides helper methods to
* deal with the data.
*
* @author Praveendra Singh
*
*/
@Getter
@Setter
@AllArgsConstructor
public class VaultEncryptionResult {
private String cipherText;
private String error;
/**
* returns the list of ciphertexts or throws VaultException if error
* encountered.
*
* @return ciphertexts
*/
public String get() {
if (!StringUtils.isEmpty(error)) {
throw new VaultException(error);
}
return cipherText;
}
}

View File

@@ -15,17 +15,26 @@
*/
package org.springframework.vault.core;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.junit.Assume.assumeTrue;
import java.util.ArrayList;
import java.util.List;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.vault.VaultException;
import org.springframework.vault.support.VaultDecryptionPayload;
import org.springframework.vault.support.VaultDecryptionResult;
import org.springframework.vault.support.VaultEncryptionPayload;
import org.springframework.vault.support.VaultEncryptionResult;
import org.springframework.vault.support.VaultMount;
import org.springframework.vault.support.VaultTransitContext;
import org.springframework.vault.support.VaultTransitKey;
@@ -34,20 +43,19 @@ import org.springframework.vault.support.VaultTransitKeyCreationRequest;
import org.springframework.vault.util.IntegrationTestSupport;
import org.springframework.vault.util.Version;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.junit.Assume.assumeTrue;
/**
* Integration tests for {@link VaultTransitTemplate} through
* {@link VaultTransitOperations}.
*
* @author Mark Paluch
* @author Praveendra Singh
*/
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = VaultIntegrationTestConfiguration.class)
public class VaultTransitTemplateIntegrationTests extends IntegrationTestSupport {
private static final String BATCH_INTRODUCED_IN_VERSION = "0.6.5";
@Autowired
private VaultOperations vaultOperations;
private VaultTransitOperations transitOperations;
@@ -98,7 +106,7 @@ public class VaultTransitTemplateIntegrationTests extends IntegrationTestSupport
deleteKey("derived");
}
}
@Test
public void createKeyShouldCreateKey() {
@@ -277,4 +285,170 @@ public class VaultTransitTemplateIntegrationTests extends IntegrationTestSupport
String rewrapped = transitOperations.rewrap("mykey", ciphertext, transitRequest);
assertThat(rewrapped).startsWith("vault:v2");
}
@Test
public void batchEncryptionAndDecryptionTestWithoutContext() {
if (prepare().getVersion().isLessThan(Version.parse(BATCH_INTRODUCED_IN_VERSION))) {
return;
}
transitOperations.createKey("mykey");
List<String> plaintexts = new ArrayList<String>();
plaintexts.add("one");
plaintexts.add("two");
batchEncryptionAndDecryption(plaintexts, null, null);
}
@Test
public void batchEncryptionAndDecryptionTestWithMatchingContext() {
if (prepare().getVersion().isLessThan(Version.parse(BATCH_INTRODUCED_IN_VERSION))) {
return;
}
VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder() //
.derived(true) //
.build();
transitOperations.createKey("mykey", request);
List<String> plaintexts = new ArrayList<String>();
plaintexts.add("one");
plaintexts.add("two");
List<VaultTransitContext> contexts = new ArrayList<VaultTransitContext>();
contexts.add(VaultTransitContext.builder().context("oneContext".getBytes()).build());
contexts.add(VaultTransitContext.builder().context("twoContext".getBytes()).build());
batchEncryptionAndDecryption(plaintexts, contexts, contexts);
}
@Test
public void batchEncryptionAndDecryptionTestWithNonEqualContext() {
if (prepare().getVersion().isLessThan(Version.parse(BATCH_INTRODUCED_IN_VERSION))) {
return;
}
try {
VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder() //
.derived(true) //
.build();
transitOperations.createKey("mykey", request);
List<String> plaintexts = new ArrayList<String>();
plaintexts.add("one");
plaintexts.add("two");
List<VaultTransitContext> encryptionContexts = new ArrayList<VaultTransitContext>();
encryptionContexts.add(VaultTransitContext.builder().context("oneContext".getBytes()).build());
encryptionContexts.add(VaultTransitContext.builder().context("twoContext".getBytes()).build());
List<VaultTransitContext> decryptionContext = new ArrayList<VaultTransitContext>();
decryptionContext.add(VaultTransitContext.builder().context("oneContext".getBytes()).build());
batchEncryptionAndDecryption(plaintexts, encryptionContexts, decryptionContext);
} catch (IllegalArgumentException e) {
return;
} catch (VaultException e) {
return;
}
Assert.fail();
}
@Test
public void batchEncryptionAndDecryptionTestWithNonMatchingContext() {
if (prepare().getVersion().isLessThan(Version.parse(BATCH_INTRODUCED_IN_VERSION))) {
return;
}
try {
VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder() //
.derived(true) //
.build();
transitOperations.createKey("mykey", request);
List<String> plaintexts = new ArrayList<String>();
plaintexts.add("one");
plaintexts.add("two");
List<VaultTransitContext> encryptionContexts = new ArrayList<VaultTransitContext>();
encryptionContexts.add(VaultTransitContext.builder().context("oneContext".getBytes()).build());
encryptionContexts.add(VaultTransitContext.builder().context("twoContext".getBytes()).build());
List<VaultTransitContext> decryptionContext = new ArrayList<VaultTransitContext>();
decryptionContext.add(VaultTransitContext.builder().context("oneContext".getBytes()).build());
decryptionContext.add(VaultTransitContext.builder().context("wrongTwoContext".getBytes()).build());
batchEncryptionAndDecryption(plaintexts, encryptionContexts, decryptionContext);
} catch (VaultException e) {
return;
}
Assert.fail();
}
private void batchEncryptionAndDecryption(List<String> plaintexts, List<VaultTransitContext> encryptionContexts,
List<VaultTransitContext> decryptionContext) {
List<VaultEncryptionPayload> encryptionBatchRequest = new ArrayList<VaultEncryptionPayload>();
int index = 0;
for (String plaintext : plaintexts) {
VaultEncryptionPayload req = VaultEncryptionPayload.of(plaintext);
if (encryptionContexts != null) {
if (encryptionContexts.size() >= (index + 1)) {
req = req.with(encryptionContexts.get(index));
}
}
encryptionBatchRequest.add(req);
index++;
}
List<VaultEncryptionResult> cipherResult = transitOperations.encrypt("mykey", encryptionBatchRequest);
List<VaultDecryptionPayload> decryptionBatchRequest = new ArrayList<VaultDecryptionPayload>();
index = 0;
for (VaultEncryptionResult cipher : cipherResult) {
VaultDecryptionPayload req = VaultDecryptionPayload.of(cipher.get());
if (decryptionContext != null) {
if (decryptionContext.size() >= (index + 1)) {
req = req.with(decryptionContext.get(index));
}
}
decryptionBatchRequest.add(req);
index++;
}
List<VaultDecryptionResult> plaintextResult = transitOperations.decrypt("mykey", decryptionBatchRequest);
Assert.assertEquals(plaintexts.size(), plaintextResult.size());
int i = 0;
for (String plaintext : plaintexts) {
String decrypted = new String(plaintextResult.get(i++).get());
Assert.assertEquals(plaintext, decrypted);
}
}
}

View File

@@ -5,3 +5,8 @@
=== What's new in Spring Vault 1.0
* Initial Vault support.
[[new-features.1-1-0]]
=== What's new in Spring Vault 1.1.0
* Batch encryption & decryption support.