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 90656c69..e6cc1dda 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 @@ -221,6 +221,15 @@ public interface VaultTransitOperations { */ String rewrap(String keyName, String ciphertext, VaultTransitContext transitContext); + /** + * Rewrap the provided batch of cipher text using the latest version of the named key. + * @param batchRequest a list of {@link Ciphertext} which includes cipher text and a + * context + * @return the rewrapped result in the order of {@code batchRequest} ciphertexts. + * @see #rewrap(String, String) + */ + List rewrap(String keyName, List batchRequest); + /** * Create a HMAC using {@code keyName} of given {@link Plaintext} using the default * hash algorithm. The key can be of any type supported by transit; the raw key will 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 c98f0947..bba7833a 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 @@ -339,6 +339,32 @@ public class VaultTransitTemplate implements VaultTransitOperations { .get("ciphertext"); } + @Override + public List rewrap(String keyName, List batchRequest) { + Assert.hasText(keyName, "Key name must not be empty"); + Assert.notEmpty(batchRequest, "BatchRequest must not be null and must have at least one entry"); + + List> batch = new ArrayList<>(batchRequest.size()); + + for (Ciphertext request : batchRequest) { + + Map vaultRequest = new LinkedHashMap<>(2); + + vaultRequest.put("ciphertext", request.getCiphertext()); + + if (request.getContext() != null) { + applyTransitOptions(request.getContext(), vaultRequest); + } + + batch.add(vaultRequest); + } + + VaultResponse vaultResponse = this.vaultOperations.write(String.format("%s/rewrap/%s", this.path, keyName), + Collections.singletonMap("batch_input", batch)); + + return toRewrappedEncryptionResults(vaultResponse, batchRequest); + } + @Override public Hmac getHmac(String keyName, Plaintext plaintext) { @@ -512,6 +538,37 @@ public class VaultTransitTemplate implements VaultTransitOperations { return result; } + static List toRewrappedEncryptionResults(VaultResponse vaultResponse, + List batchRequest) { + + List result = new ArrayList<>(batchRequest.size()); + List> batchData = getBatchData(vaultResponse); + + for (int i = 0; i < batchRequest.size(); i++) { + + VaultEncryptionResult encrypted; + Ciphertext ciphertext = batchRequest.get(i); + if (batchData.size() > i) { + + Map data = batchData.get(i); + if (StringUtils.hasText(data.get("error"))) { + encrypted = new VaultEncryptionResult(new VaultException(data.get("error"))); + } + else { + encrypted = new VaultEncryptionResult( + toCiphertext(data.get("ciphertext"), ciphertext.getContext())); + } + } + else { + encrypted = new VaultEncryptionResult(new VaultException("No result for cipher text #" + i)); + } + + result.add(encrypted); + } + + return result; + } + static List toDecryptionResults(VaultResponse vaultResponse, List batchRequest) { List result = new ArrayList<>(batchRequest.size()); 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 e373c875..d6f8f1ec 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 @@ -534,6 +534,31 @@ class VaultTransitTemplateIntegrationTests extends IntegrationTestSupport { assertThat(rewrapped).startsWith("vault:v2"); } + @Test + void encryptAndRewrapInBatchShouldCreateCiphertext() { + + this.transitOperations.createKey("mykey", + VaultTransitKeyCreationRequest.builder().convergentEncryption(true).derived(true).build()); + + VaultTransitContext transitRequest = VaultTransitContext.builder() // + .context("blubb".getBytes()) // + .nonce("123456789012".getBytes()) // + .build(); + + String ciphertext1 = this.transitOperations.encrypt("mykey", "hello-world".getBytes(), transitRequest); + String ciphertext2 = this.transitOperations.encrypt("mykey", "hello-vault".getBytes(), transitRequest); + this.transitOperations.rotate("mykey"); + + List batchRequest = List.of(ciphertext1, ciphertext2) + .stream() + .map(ct -> Ciphertext.of(ct).with(transitRequest)) + .toList(); + List rewrappedResult = this.transitOperations.rewrap("mykey", batchRequest); + Assertions.assertThat(rewrappedResult) + .hasSize(2) + .allMatch(result -> result.get().getCiphertext().startsWith("vault:v2")); + } + @Test @RequiresVaultVersion(BATCH_INTRODUCED_IN_VERSION) void shouldBatchEncrypt() {