Polishing.

Extract method to avoid code duplications.

Add author and since tags. Add reactive batch rewrap support.

See: gh-687
Original pull request: gh-819
This commit is contained in:
Mark Paluch
2023-10-16 14:24:59 +02:00
parent 347e3b771c
commit 196d3c7470
6 changed files with 104 additions and 69 deletions

View File

@@ -214,6 +214,15 @@ public interface ReactiveVaultTransitOperations {
*/
Mono<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)
*/
Flux<VaultEncryptionResult> rewrap(String keyName, List<Ciphertext> 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

View File

@@ -38,6 +38,7 @@ import org.springframework.vault.support.VaultTransitKeyCreationRequest;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.LinkedHashMap;
@@ -223,9 +224,7 @@ public class ReactiveVaultTransitTemplate implements ReactiveVaultTransitOperati
Assert.hasText(ciphertext, "Ciphertext must not be empty");
Assert.notNull(transitContext, "VaultTransitContext must not be null");
Map<String, String> request = new LinkedHashMap<>();
request.put("ciphertext", ciphertext);
Map<String, String> request = createRewrapRequest(toCiphertext(ciphertext, transitContext));
applyTransitOptions(transitContext, request);
@@ -233,8 +232,23 @@ public class ReactiveVaultTransitTemplate implements ReactiveVaultTransitOperati
.map(response -> (String) response.getRequiredData().get("ciphertext"));
}
@Override
public Flux<VaultEncryptionResult> rewrap(String keyName, List<Ciphertext> 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");
return Flux.fromIterable(batchRequest)
.map(VaultTransitTemplate::createRewrapRequest)
.collectList()
.flatMap(batch -> this.reactiveVaultOperations.write(String.format("%s/rewrap/%s", this.path, keyName),
Collections.singletonMap("batch_input", batch)))
.flatMapIterable(vaultResponse -> toBatchResults(vaultResponse, batchRequest, Ciphertext::getContext));
}
@Override
public Flux<VaultEncryptionResult> encrypt(String keyName, List<Plaintext> 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");
@@ -247,7 +261,7 @@ public class ReactiveVaultTransitTemplate implements ReactiveVaultTransitOperati
.collectList()
.flatMap(batch -> this.reactiveVaultOperations.write(String.format("%s/encrypt/%s", this.path, keyName),
Collections.singletonMap("batch_input", batch)))
.flatMapIterable(vaultResponse -> toEncryptionResults(vaultResponse, batchRequest));
.flatMapIterable(vaultResponse -> toBatchResults(vaultResponse, batchRequest, Plaintext::getContext));
}
@Override

View File

@@ -42,6 +42,7 @@ import org.springframework.vault.support.VaultTransitKeyCreationRequest;
* @author Sven Schürmann
* @author Praveendra Singh
* @author Luander Ribeiro
* @author Nanne Baars
* @see <a href="https://www.vaultproject.io/docs/secrets/transit/index.html">Transit
* Secret Backend</a>
*/
@@ -227,6 +228,7 @@ public interface VaultTransitOperations {
* context
* @return the rewrapped result in the order of {@code batchRequest} ciphertexts.
* @see #rewrap(String, String)
* @since 3.1
*/
List<VaultEncryptionResult> rewrap(String keyName, List<Ciphertext> batchRequest);

View File

@@ -22,8 +22,10 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
@@ -227,7 +229,7 @@ public class VaultTransitTemplate implements VaultTransitOperations {
VaultResponse vaultResponse = this.vaultOperations.write(String.format("%s/encrypt/%s", this.path, keyName),
Collections.singletonMap("batch_input", batch));
return toEncryptionResults(vaultResponse, batchRequest);
return toBatchResults(vaultResponse, batchRequest, Plaintext::getContext);
}
@Override
@@ -328,11 +330,7 @@ public class VaultTransitTemplate implements VaultTransitOperations {
Assert.hasText(ciphertext, "Ciphertext must not be empty");
Assert.notNull(transitContext, "VaultTransitContext must not be null");
Map<String, String> request = new LinkedHashMap<>();
request.put("ciphertext", ciphertext);
applyTransitOptions(transitContext, request);
Map<String, String> request = createRewrapRequest(toCiphertext(ciphertext, transitContext));
return (String) this.vaultOperations.write(String.format("%s/rewrap/%s", this.path, keyName), request)
.getRequiredData()
@@ -341,6 +339,7 @@ public class VaultTransitTemplate implements VaultTransitOperations {
@Override
public List<VaultEncryptionResult> rewrap(String keyName, List<Ciphertext> 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");
@@ -348,21 +347,14 @@ public class VaultTransitTemplate implements VaultTransitOperations {
for (Ciphertext request : batchRequest) {
Map<String, String> vaultRequest = new LinkedHashMap<>(2);
vaultRequest.put("ciphertext", request.getCiphertext());
if (request.getContext() != null) {
applyTransitOptions(request.getContext(), vaultRequest);
}
Map<String, String> vaultRequest = createRewrapRequest(request);
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);
return toBatchResults(vaultResponse, batchRequest, Ciphertext::getContext);
}
@Override
@@ -509,45 +501,16 @@ public class VaultTransitTemplate implements VaultTransitOperations {
}
}
static List<VaultEncryptionResult> toEncryptionResults(VaultResponse vaultResponse, List<Plaintext> batchRequest) {
static <T> List<VaultEncryptionResult> toBatchResults(VaultResponse vaultResponse, List<T> batchRequests,
Function<T, VaultTransitContext> contextExtractor) {
List<VaultEncryptionResult> result = new ArrayList<>(batchRequest.size());
List<VaultEncryptionResult> result = new ArrayList<>(batchRequests.size());
List<Map<String, String>> batchData = getBatchData(vaultResponse);
for (int i = 0; i < batchRequest.size(); i++) {
for (int i = 0; i < batchRequests.size(); i++) {
VaultEncryptionResult encrypted;
Plaintext plaintext = batchRequest.get(i);
if (batchData.size() > i) {
Map<String, String> 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"), plaintext.getContext()));
}
}
else {
encrypted = new VaultEncryptionResult(new VaultException("No result for plaintext #" + i));
}
result.add(encrypted);
}
return result;
}
static List<VaultEncryptionResult> toRewrappedEncryptionResults(VaultResponse vaultResponse,
List<Ciphertext> batchRequest) {
List<VaultEncryptionResult> result = new ArrayList<>(batchRequest.size());
List<Map<String, String>> batchData = getBatchData(vaultResponse);
for (int i = 0; i < batchRequest.size(); i++) {
VaultEncryptionResult encrypted;
Ciphertext ciphertext = batchRequest.get(i);
T request = batchRequests.get(i);
if (batchData.size() > i) {
Map<String, String> data = batchData.get(i);
@@ -556,11 +519,11 @@ public class VaultTransitTemplate implements VaultTransitOperations {
}
else {
encrypted = new VaultEncryptionResult(
toCiphertext(data.get("ciphertext"), ciphertext.getContext()));
toCiphertext(data.get("ciphertext"), contextExtractor.apply(request)));
}
}
else {
encrypted = new VaultEncryptionResult(new VaultException("No result for cipher text #" + i));
encrypted = new VaultEncryptionResult(new VaultException("No result for request #" + i));
}
result.add(encrypted);
@@ -607,6 +570,15 @@ public class VaultTransitTemplate implements VaultTransitOperations {
return new VaultDecryptionResult(Plaintext.empty().with(ciphertext.getContext()));
}
static Map<String, String> createRewrapRequest(Ciphertext request) {
Map<String, String> vaultRequest = new LinkedHashMap<>(2);
vaultRequest.put("ciphertext", request.getCiphertext());
applyTransitOptions(request.getContext(), vaultRequest);
return vaultRequest;
}
static Ciphertext toCiphertext(String ciphertext, @Nullable VaultTransitContext context) {
return context != null ? Ciphertext.of(ciphertext).with(context) : Ciphertext.of(ciphertext);
}

View File

@@ -15,10 +15,20 @@
*/
package org.springframework.vault.core;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@@ -38,16 +48,15 @@ import org.springframework.vault.support.VaultTransitKeyCreationRequest;
import org.springframework.vault.util.IntegrationTestSupport;
import org.springframework.vault.util.RequiresVaultVersion;
import org.springframework.vault.util.Version;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.vault.core.VaultTransitTemplateIntegrationTests.*;
import static org.springframework.vault.core.VaultTransitTemplateIntegrationTests.AES256_GCM96_INTRODUCED_IN_VERSION;
import static org.springframework.vault.core.VaultTransitTemplateIntegrationTests.BATCH_INTRODUCED_IN_VERSION;
import static org.springframework.vault.core.VaultTransitTemplateIntegrationTests.ECDSA521_INTRODUCED_IN_VERSION;
import static org.springframework.vault.core.VaultTransitTemplateIntegrationTests.ED25519_INTRODUCED_IN_VERSION;
import static org.springframework.vault.core.VaultTransitTemplateIntegrationTests.KEY_EXPORT_INTRODUCED_IN_VERSION;
import static org.springframework.vault.core.VaultTransitTemplateIntegrationTests.RSA3072_INTRODUCED_IN_VERSION;
import static org.springframework.vault.core.VaultTransitTemplateIntegrationTests.SIGN_VERIFY_INTRODUCED_IN_VERSION;
/**
* Integration tests for {@link ReactiveVaultTransitTemplate} using the {@code transit}
@@ -441,6 +450,36 @@ public class ReactiveVaultTransitIntegrationTests extends IntegrationTestSupport
.verifyComplete();
}
@Test
void encryptAndRewrapInBatchShouldCreateCiphertext() {
this.reactiveTransitOperations
.createKey("mykey",
VaultTransitKeyCreationRequest.builder().convergentEncryption(true).derived(true).build())
.as(StepVerifier::create)
.verifyComplete();
VaultTransitContext transitRequest = VaultTransitContext.builder() //
.context("blubb".getBytes()) //
.nonce("123456789012".getBytes()) //
.build();
String ciphertext1 = this.reactiveTransitOperations.encrypt("mykey", "hello-world".getBytes(), transitRequest)
.block();
String ciphertext2 = this.reactiveTransitOperations.encrypt("mykey", "hello-vault".getBytes(), transitRequest)
.block();
this.reactiveTransitOperations.rotate("mykey").as(StepVerifier::create).verifyComplete();
List<Ciphertext> batchRequest = Stream.of(ciphertext1, ciphertext2)
.map(ct -> Ciphertext.of(ct).with(transitRequest))
.toList();
this.reactiveTransitOperations.rewrap("mykey", batchRequest)
.as(StepVerifier::create)
.assertNext(it -> assertThat(it.get().getCiphertext()).startsWith("vault:v2"))
.expectNextCount(1)
.verifyComplete();
}
@Test
void shouldEncryptBinaryPlaintext() {

View File

@@ -55,6 +55,7 @@ import org.springframework.vault.util.IntegrationTestSupport;
import org.springframework.vault.util.RequiresVaultVersion;
import org.springframework.vault.util.Version;
import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.fail;
@@ -67,6 +68,7 @@ import static org.assertj.core.api.Assertions.fail;
* @author Praveendra Singh
* @author Luander Ribeiro
* @author Mikko Koli
* @author Nanne Baars
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = VaultIntegrationTestConfiguration.class)
@@ -372,7 +374,7 @@ class VaultTransitTemplateIntegrationTests extends IntegrationTestSupport {
assertThat(ciphertext).startsWith("vault:%s:".formatted(expectedKeyPrefix));
}
catch (Exception e) {
Assertions.assertThat(expectedKeyPrefix).isNullOrEmpty();
assertThat(expectedKeyPrefix).isNullOrEmpty();
}
}
@@ -549,14 +551,11 @@ class VaultTransitTemplateIntegrationTests extends IntegrationTestSupport {
String ciphertext2 = this.transitOperations.encrypt("mykey", "hello-vault".getBytes(), transitRequest);
this.transitOperations.rotate("mykey");
List<Ciphertext> batchRequest = List.of(ciphertext1, ciphertext2)
.stream()
List<Ciphertext> batchRequest = Stream.of(ciphertext1, ciphertext2)
.map(ct -> Ciphertext.of(ct).with(transitRequest))
.toList();
List<VaultEncryptionResult> rewrappedResult = this.transitOperations.rewrap("mykey", batchRequest);
Assertions.assertThat(rewrappedResult)
.hasSize(2)
.allMatch(result -> result.get().getCiphertext().startsWith("vault:v2"));
assertThat(rewrappedResult).hasSize(2).allMatch(result -> result.get().getCiphertext().startsWith("vault:v2"));
}
@Test