diff --git a/README.adoc b/README.adoc index 63171433..85471d13 100644 --- a/README.adoc +++ b/README.adoc @@ -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/` diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransitOperations.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransitOperations.java index d30db770..89a3b196 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransitOperations.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransitOperations.java @@ -20,6 +20,10 @@ import java.util.List; import org.springframework.lang.Nullable; 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; @@ -30,6 +34,7 @@ import org.springframework.vault.support.VaultTransitKeyCreationRequest; * * @author Mark Paluch * @author Sven Schürmann + * @author Praveendra Singh * @see Transit * Secret Backend */ @@ -126,6 +131,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 encrypt(String keyName, List batchRequest); + /** * Decrypts the provided plaintext using the named key. * @@ -147,6 +165,19 @@ public interface VaultTransitOperations { */ byte[] decrypt(String keyName, String ciphertext, VaultTransitContext transitContext); + /** + * 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 decrypt(String keyName, List 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 diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransitTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransitTemplate.java index 725270e6..74c0a578 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransitTemplate.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransitTemplate.java @@ -15,6 +15,7 @@ */ package org.springframework.vault.core; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; @@ -29,6 +30,11 @@ import org.springframework.util.Base64Utils; import org.springframework.util.ObjectUtils; 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; @@ -36,11 +42,16 @@ import org.springframework.vault.support.VaultTransitKey; import org.springframework.vault.support.VaultTransitKeyConfiguration; import org.springframework.vault.support.VaultTransitKeyCreationRequest; +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 { @@ -176,6 +187,37 @@ public class VaultTransitTemplate implements VaultTransitOperations { .getRequiredData().get("ciphertext"); } + @Override + public List encrypt(String keyName, List 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> batch = new ArrayList>(); + + for (VaultEncryptionPayload request : batchRequest) { + + Assert.notNull(request.getPlaintext(), "Plain text must not be null"); + + Map vaultRequest = new LinkedHashMap(); + + vaultRequest.put("plaintext", Base64Utils.encodeToString(request.getPlaintext())); + + if (request.getContext() != null) { + applyTransitOptions(request.getContext(), vaultRequest); + } + + batch.add(vaultRequest); + } + + Map>> request = new LinkedHashMap>>(); + 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) { @@ -214,6 +256,37 @@ public class VaultTransitTemplate implements VaultTransitOperations { return Base64Utils.decodeFromString(plaintext); } + @Override + public List decrypt(String keyName, List 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> batch = new ArrayList>(); + + for (VaultDecryptionPayload request : batchRequest) { + + Assert.notNull(request.getCiphertext(), "Cipher text must not be null"); + + Map vaultRequest = new LinkedHashMap(); + + vaultRequest.put("ciphertext", request.getCiphertext()); + + if (request.getContext() != null) { + applyTransitOptions(request.getContext(), vaultRequest); + } + + batch.add(vaultRequest); + } + + Map>> request = new LinkedHashMap>>(); + 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) { @@ -306,4 +379,5 @@ public class VaultTransitTemplate implements VaultTransitOperations { @Nullable private String name; } + } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultDecryptionPayload.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultDecryptionPayload.java new file mode 100644 index 00000000..5ea436f5 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultDecryptionPayload.java @@ -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); + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultDecryptionResult.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultDecryptionResult.java new file mode 100644 index 00000000..b874b2ba --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultDecryptionResult.java @@ -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; + } + +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultEncryptionDecryptionResultHelper.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultEncryptionDecryptionResultHelper.java new file mode 100644 index 00000000..60b47ac0 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultEncryptionDecryptionResultHelper.java @@ -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 fetchEncryptionResult(VaultResponse vaultResponse) { + + List result = new ArrayList(); + + for (Map data : getBatchData(vaultResponse)) { + + VaultEncryptionResult res = new VaultEncryptionResult(data.get("ciphertext"), data.get("error")); + result.add(res); + } + + return result; + } + + public static List fetchDecryptionResult(VaultResponse vaultResponse) { + + List result = new ArrayList(); + + for (Map 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> getBatchData(VaultResponse vaultResponse) { + return (List>) vaultResponse.getData().get("batch_results"); + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultEncryptionPayload.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultEncryptionPayload.java new file mode 100644 index 00000000..cce3f6b6 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultEncryptionPayload.java @@ -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); + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultEncryptionResult.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultEncryptionResult.java new file mode 100644 index 00000000..e9fa88f6 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultEncryptionResult.java @@ -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; + } + +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTransitTemplateIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTransitTemplateIntegrationTests.java index a89794a0..9b5b32c4 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTransitTemplateIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTransitTemplateIntegrationTests.java @@ -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 plaintexts = new ArrayList(); + 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 plaintexts = new ArrayList(); + plaintexts.add("one"); + plaintexts.add("two"); + + List contexts = new ArrayList(); + 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 plaintexts = new ArrayList(); + plaintexts.add("one"); + plaintexts.add("two"); + + List encryptionContexts = new ArrayList(); + encryptionContexts.add(VaultTransitContext.builder().context("oneContext".getBytes()).build()); + encryptionContexts.add(VaultTransitContext.builder().context("twoContext".getBytes()).build()); + + List decryptionContext = new ArrayList(); + 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 plaintexts = new ArrayList(); + plaintexts.add("one"); + plaintexts.add("two"); + + List encryptionContexts = new ArrayList(); + encryptionContexts.add(VaultTransitContext.builder().context("oneContext".getBytes()).build()); + encryptionContexts.add(VaultTransitContext.builder().context("twoContext".getBytes()).build()); + + List decryptionContext = new ArrayList(); + 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 plaintexts, List encryptionContexts, + List decryptionContext) { + + List encryptionBatchRequest = new ArrayList(); + + 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 cipherResult = transitOperations.encrypt("mykey", encryptionBatchRequest); + + List decryptionBatchRequest = new ArrayList(); + + 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 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); + } + } } diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 4660764e..607456cb 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -12,3 +12,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.