Add reactive support for transit operations.

Closes gh-620
Original pull request: gh-778
This commit is contained in:
James Luke
2023-04-11 16:13:40 +10:00
committed by Mark Paluch
parent 4f7f7e9021
commit 0fe33b78f5
10 changed files with 1471 additions and 33 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017-2022 the original author or authors.
* Copyright 2017-2023 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.
@@ -15,18 +15,17 @@
*/
package org.springframework.vault.core;
import java.util.function.Function;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.lang.Nullable;
import org.springframework.vault.VaultException;
import org.springframework.vault.support.VaultResponse;
import org.springframework.vault.support.VaultResponseSupport;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientException;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.function.Function;
/**
* Interface that specifies a basic set of Vault operations executed on a reactive
@@ -123,4 +122,17 @@ public interface ReactiveVaultOperations {
<V, T extends Publisher<V>> T doWithSession(Function<WebClient, ? extends T> sessionCallback)
throws VaultException, WebClientException;
/**
* @return the operations interface to interact with the Vault transit backend.
*/
ReactiveVaultTransitOperations opsForTransit();
/**
* Return {@link ReactiveVaultTransitOperations} if the transit backend is mounted on
* a different path than {@code transit}.
* @param path the mount path
* @return the operations interface to interact with the Vault transit backend.
*/
ReactiveVaultTransitOperations opsForTransit(String path);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017-2022 the original author or authors.
* Copyright 2017-2023 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.
@@ -15,17 +15,9 @@
*/
package org.springframework.vault.core;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -49,6 +41,12 @@ import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClient.RequestBodySpec;
import org.springframework.web.reactive.function.client.WebClientException;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import static org.springframework.web.reactive.function.client.ExchangeFilterFunction.ofRequestProcessor;
@@ -367,4 +365,14 @@ public class ReactiveVaultTemplate implements ReactiveVaultOperations {
}
@Override
public ReactiveVaultTransitOperations opsForTransit() {
return opsForTransit("transit");
}
@Override
public ReactiveVaultTransitOperations opsForTransit(String path) {
return new ReactiveVaultTransitTemplate(this, path);
}
}

View File

@@ -0,0 +1,278 @@
/*
* Copyright 2023-2023 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
*
* https://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.core;
import org.springframework.vault.support.Ciphertext;
import org.springframework.vault.support.Hmac;
import org.springframework.vault.support.Plaintext;
import org.springframework.vault.support.RawTransitKey;
import org.springframework.vault.support.Signature;
import org.springframework.vault.support.SignatureValidation;
import org.springframework.vault.support.TransitKeyType;
import org.springframework.vault.support.VaultDecryptionResult;
import org.springframework.vault.support.VaultEncryptionResult;
import org.springframework.vault.support.VaultHmacRequest;
import org.springframework.vault.support.VaultSignRequest;
import org.springframework.vault.support.VaultSignatureVerificationRequest;
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 reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
/**
* Interface that specifies a set of {@code transit} operations executed on a reactive
* infrastructure, implemented by
* {@link org.springframework.vault.core.ReactiveVaultTransitTemplate}.
*
* @author James Luke
*/
public interface ReactiveVaultTransitOperations {
/**
* Create a new named encryption key given a {@code name}
* @param keyName must not be empty or {@literal null}
*/
Mono<Void> createKey(String keyName);
/**
* Create a new named encryption key given a {@code name} and
* {@link VaultTransitKeyCreationRequest}. The key options set here cannot be changed
* after key creation.
* @param keyName must not be empty or {@literal null}.
* @param createKeyRequest must not be {@literal null}.
*/
Mono<Void> createKey(String keyName, VaultTransitKeyCreationRequest createKeyRequest);
/**
* @return stream of transit key names.
*/
Flux<String> getKeys();
/**
* Create a new named encryption key given a {@code name}.
* @param keyName must not be empty or {@literal null}.
* @param keyConfiguration must not be {@literal null}.
*/
Mono<Void> configureKey(String keyName, VaultTransitKeyConfiguration keyConfiguration);
/**
* Returns the value of the named encryption key. Depending on the type of key,
* different information may be returned. The key must be exportable to support this
* operation.
* @param keyName must not be empty or {@literal null}.
* @param type must not be {@literal null}.
* @return the {@link RawTransitKey}. May be empty if key does not exist
*/
Mono<RawTransitKey> exportKey(String keyName, TransitKeyType type);
/**
* Return information about a named encryption key.
* @param keyName must not be empty or {@literal null}.
* @return the {@link VaultTransitKey}. May be empty if key does not exist
*/
Mono<VaultTransitKey> getKey(String keyName);
/**
* Deletes a named encryption key. It will no longer be possible to decrypt any data
* encrypted with the named key.
* @param keyName must not be empty or {@literal null}.
*/
Mono<Void> deleteKey(String keyName);
/**
* Rotates the version of the named key. After rotation, new plain text requests will
* be encrypted with the new version of the key. To upgrade ciphertext to be encrypted
* with the latest version of the key, use {@link #rewrap(String, String)}.
* @param keyName must not be empty or {@literal null}.
* @see #rewrap(String, String)
*/
Mono<Void> rotate(String keyName);
/**
* Encrypts the provided plain text using the named key. The given {@code plaintext}
* is encoded into bytes using the {@link java.nio.charset.Charset#defaultCharset()
* default charset}. Use
* {@link #encrypt(String, org.springframework.vault.support.Plaintext)} to construct
* a {@link org.springframework.vault.support.Plaintext#of(byte[]) Plaintext} object
* from bytes to avoid {@link java.nio.charset.Charset} mismatches.
* @param keyName must not be empty or {@literal null}.
* @param plaintext must not be empty or {@literal null}.
* @return cipher text.
*/
Mono<String> encrypt(String keyName, String plaintext);
/**
* Encrypts the provided {@code plaintext} using the named key.
* @param keyName must not be empty or {@literal null}.
* @param plaintext must not be {@literal null}.
* @return cipher text.
*/
Mono<Ciphertext> encrypt(String keyName, Plaintext plaintext);
/**
* Encrypts the provided {@code plaintext} using the named key.
* @param keyName must not be empty or {@literal null}.
* @param plaintext must not be empty or {@literal null}.
* @param transitRequest must not be {@literal null}. Use
* {@link VaultTransitContext#empty()} if no request options provided.
* @return cipher text.
*/
Mono<String> encrypt(String keyName, byte[] plaintext, VaultTransitContext transitRequest);
/**
* Encrypts the provided batch of {@code plaintext} using the named key and context.
* The encryption is done using transit backend's batch operation.
* @param keyName must not be empty or {@literal null}.
* @param batchRequest a list of {@link Plaintext} which includes plain text and an
* optional context.
* @return the encrypted result in the order of {@code batchRequest} plaintexts.
*/
Flux<VaultEncryptionResult> encrypt(String keyName, List<Plaintext> batchRequest);
/**
* Decrypts the provided plain text using the named key. The decoded {@code plaintext}
* is decoded into {@link String} the {@link java.nio.charset.Charset#defaultCharset()
* default charset}. Use
* {@link #decrypt(String, org.springframework.vault.support.Ciphertext)} to obtain a
* {@link org.springframework.vault.support.Ciphertext} object that allows to control
* the {@link java.nio.charset.Charset} for later consumption.
* @param keyName must not be empty or {@literal null}.
* @param ciphertext must not be empty or {@literal null}.
* @return plain text.
*/
Mono<String> decrypt(String keyName, String ciphertext);
/**
* Decrypts the provided cipher text using the named key.
* @param keyName must not be empty or {@literal null}.
* @param ciphertext must not be {@literal null}.
* @return plain text.
*/
Mono<Plaintext> decrypt(String keyName, Ciphertext ciphertext);
/**
* Decrypts the provided {@code ciphertext} using the named key.
* @param keyName must not be empty or {@literal null}.
* @param ciphertext must not be empty or {@literal null}.
* @param transitContext must not be {@literal null}. Use
* {@link VaultTransitContext#empty()} if no request options provided.
* @return cipher text.
* @return plain text.
*/
Mono<byte[]> decrypt(String keyName, String ciphertext, VaultTransitContext transitContext);
/**
* Decrypts the provided batch of cipher text using the named key and context. The*
* decryption is done using transit backend's batch operation.
* @param keyName must not be empty or {@literal null}.
* @param batchRequest a list of {@link Ciphertext} which includes plain text and an
* optional context.
* @return the decrypted result in the order of {@code batchRequest} ciphertexts.
*/
Flux<VaultDecryptionResult> decrypt(String keyName, List<Ciphertext> batchRequest);
/**
* Rewrap the provided cipher text using the latest version of the named key. Because
* this never returns plain text, it is possible to delegate this functionality to
* untrusted users or scripts.
* @param keyName must not be empty or {@literal null}.
* @param ciphertext must not be empty or {@literal null}.
* @return cipher text.
* @see #rotate(String)
*/
Mono<String> rewrap(String keyName, String ciphertext);
/**
* Rewrap the provided cipher text using the latest version of the named key. Because
* this never returns plain text, it is possible to delegate this functionality to
* untrusted users or scripts.
* @param keyName must not be empty or {@literal null}.
* @param ciphertext must not be empty or {@literal null}.
* @param transitContext must not be {@literal null}. Use
* {@link VaultTransitContext#empty()} if no request options provided.
* @return cipher text.
* @see #rotate(String)
*/
Mono<String> rewrap(String keyName, String ciphertext, VaultTransitContext transitContext);
/**
* 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
* be marshaled into bytes to be used for the HMAC function. If the key is of a type
* that supports rotation, the latest (current) version will be used.
* @param keyName must not be empty or {@literal null}.
* @param plaintext must not be {@literal null}.
* @return the digest of given data the default hash algorithm and the named key.
*/
Mono<Hmac> getHmac(String keyName, Plaintext plaintext);
/**
* Create a HMAC using {@code keyName} of given {@link VaultHmacRequest} using the
* default hash algorithm. The key can be of any type supported by transit; the raw
* key will be marshaled into bytes to be used for the HMAC function. If the key is of
* a type that supports rotation, configured {@link VaultHmacRequest#getKeyVersion()}
* will be used.
* @param keyName must not be empty or {@literal null}.
* @param hmacRequest the {@link VaultHmacRequest}, must not be {@literal null}.
* @return the digest of given data the default hash algorithm and the named key.
*/
Mono<Hmac> getHmac(String keyName, VaultHmacRequest hmacRequest);
/**
* Create a cryptographic signature using {@code keyName} of the given
* {@link Plaintext} and the default hash algorithm. The key must be of a type that
* supports signing.
* @param keyName must not be empty or {@literal null}.
* @param plaintext must not be empty or {@literal null}.
* @return Signature for {@link Plaintext}.
*/
Mono<Signature> sign(String keyName, Plaintext plaintext);
/**
* Create a cryptographic signature using {@code keyName} of the given
* {@link VaultSignRequest} and the specified hash algorithm. The key must be of a
* type that supports signing.
* @param keyName must not be empty or {@literal null}.
* @param signRequest {@link VaultSignRequest} must not be empty or {@literal null}.
* @return Signature for {@link VaultSignRequest}.
*/
Mono<Signature> sign(String keyName, VaultSignRequest signRequest);
/**
* Verify the cryptographic signature using {@code keyName} of the given
* {@link Plaintext} and {@link Signature}.
* @param keyName must not be empty or {@literal null}.
* @param plaintext must not be {@literal null}.
* @param signature Signature to be verified, must not be {@literal null}.
* @return {@literal true} if the signature is valid, {@literal false} otherwise.
*/
Mono<Boolean> verify(String keyName, Plaintext plaintext, Signature signature);
/**
* Verify the cryptographic signature using {@code keyName} of the given
* {@link VaultSignRequest}.
* @param keyName must not be empty or {@literal null}.
* @param verificationRequest {@link VaultSignatureVerificationRequest} must not be
* {@literal null}.
* @return the resulting {@link SignatureValidation}.
*/
Mono<SignatureValidation> verify(String keyName, VaultSignatureVerificationRequest verificationRequest);
}

View File

@@ -0,0 +1,334 @@
/*
* Copyright 2023-2023 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
*
* https://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.core;
import org.springframework.util.Assert;
import org.springframework.vault.core.VaultTransitTemplate.VaultTransitKeyImpl;
import org.springframework.vault.support.Ciphertext;
import org.springframework.vault.support.Hmac;
import org.springframework.vault.support.Plaintext;
import org.springframework.vault.support.RawTransitKey;
import org.springframework.vault.support.Signature;
import org.springframework.vault.support.SignatureValidation;
import org.springframework.vault.support.TransitKeyType;
import org.springframework.vault.support.VaultDecryptionResult;
import org.springframework.vault.support.VaultEncryptionResult;
import org.springframework.vault.support.VaultHmacRequest;
import org.springframework.vault.support.VaultResponse;
import org.springframework.vault.support.VaultResponseSupport;
import org.springframework.vault.support.VaultSignRequest;
import org.springframework.vault.support.VaultSignatureVerificationRequest;
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 reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Base64;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static org.springframework.vault.core.VaultTransitTemplate.*;
/**
* Default implementation of {@link ReactiveVaultTransitOperations}
*
* @author James Luke
*/
public class ReactiveVaultTransitTemplate implements ReactiveVaultTransitOperations {
private final ReactiveVaultOperations reactiveVaultOperations;
private final String path;
public ReactiveVaultTransitTemplate(ReactiveVaultOperations reactiveVaultOperations, String path) {
Assert.notNull(reactiveVaultOperations, "ReactiveVaultOperations must not be null");
Assert.hasText(path, "Path must not be empty");
this.reactiveVaultOperations = reactiveVaultOperations;
this.path = path;
}
@Override
public Mono<Void> createKey(String keyName) {
Assert.hasText(keyName, "Key name must not be empty");
return this.reactiveVaultOperations.write(String.format("%s/keys/%s", this.path, keyName), null).then();
}
@Override
public Mono<Void> createKey(String keyName, VaultTransitKeyCreationRequest createKeyRequest) {
Assert.hasText(keyName, "Key name must not be empty");
Assert.notNull(createKeyRequest, "VaultTransitKeyCreationRequest must not be empty");
return this.reactiveVaultOperations.write(String.format("%s/keys/%s", this.path, keyName), createKeyRequest)
.then();
}
@Override
public Mono<Void> rotate(String keyName) {
Assert.hasText(keyName, "Key name must not be empty");
return this.reactiveVaultOperations.write(String.format("%s/keys/%s/rotate", this.path, keyName), null).then();
}
@Override
public Mono<String> encrypt(String keyName, String plaintext) {
Assert.hasText(keyName, "Key name must not be empty");
Assert.notNull(plaintext, "Plaintext must not be null");
Map<String, String> request = new LinkedHashMap<>();
request.put("plaintext", Base64.getEncoder().encodeToString(plaintext.getBytes()));
return this.reactiveVaultOperations.write(String.format("%s/encrypt/%s", this.path, keyName), request)
.map(it -> (String) it.getRequiredData().get("ciphertext"));
}
@Override
public Mono<Void> configureKey(String keyName, VaultTransitKeyConfiguration keyConfiguration) {
Assert.hasText(keyName, "Key name must not be empty");
Assert.notNull(keyConfiguration, "VaultKeyConfiguration must not be empty");
return this.reactiveVaultOperations
.write(String.format("%s/keys/%s/config", this.path, keyName), keyConfiguration).then();
}
@Override
public Mono<Void> deleteKey(String keyName) {
Assert.hasText(keyName, "Key name must not be empty");
return this.reactiveVaultOperations.delete(String.format("%s/keys/%s", this.path, keyName));
}
@Override
@SuppressWarnings("unchecked")
public Flux<String> getKeys() {
return this.reactiveVaultOperations.read(String.format("%s/keys?list=true", this.path))
.flatMapIterable(it -> (List<String>) it.getRequiredData().get("keys"));
}
@Override
public Mono<String> encrypt(String keyName, byte[] plaintext, VaultTransitContext transitContext) {
Assert.notNull(plaintext, "Plaintext must not be null");
Assert.hasText(keyName, "Key name must not be empty");
Assert.notNull(transitContext, "VaultTransitContext must not be null");
Map<String, String> request = new LinkedHashMap<>();
request.put("plaintext", Base64.getEncoder().encodeToString(plaintext));
applyTransitOptions(transitContext, request);
return this.reactiveVaultOperations.write(String.format("%s/encrypt/%s", this.path, keyName), request)
.map(it -> (String) it.getRequiredData().get("ciphertext"));
}
@Override
public Mono<Ciphertext> encrypt(String keyName, Plaintext plaintext) {
Assert.hasText(keyName, "Key name must not be empty");
Assert.notNull(plaintext, "Plaintext must not be null");
return encrypt(keyName, plaintext.getPlaintext(), plaintext.getContext())
.map(ciphertext -> toCiphertext(ciphertext, plaintext.getContext()));
}
@Override
public Mono<String> decrypt(String keyName, String ciphertext) {
Assert.hasText(keyName, "Key name must not be empty");
Assert.hasText(ciphertext, "Ciphertext must not be empty");
Map<String, String> request = new LinkedHashMap<>();
request.put("ciphertext", ciphertext);
return this.reactiveVaultOperations.write(String.format("%s/decrypt/%s", this.path, keyName), request)
.map(it -> (String) it.getRequiredData().get("plaintext"))
.map(plaintext -> new String(Base64.getDecoder().decode(plaintext)));
}
@Override
public Mono<Plaintext> decrypt(String keyName, Ciphertext ciphertext) {
Assert.hasText(keyName, "Key name must not be null");
Assert.notNull(ciphertext, "Ciphertext must not be null");
return decrypt(keyName, ciphertext.getCiphertext(), ciphertext.getContext())
.map(plaintext -> Plaintext.of(plaintext).with(ciphertext.getContext()));
}
@Override
public Mono<byte[]> decrypt(String keyName, String ciphertext, VaultTransitContext transitContext) {
Assert.hasText(keyName, "Key name must not be empty");
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);
return this.reactiveVaultOperations.write(String.format("%s/decrypt/%s", this.path, keyName), request)
.map(it -> (String) it.getRequiredData().get("plaintext")).map(Base64.getDecoder()::decode);
}
@Override
public Mono<String> rewrap(String keyName, String ciphertext) {
Assert.hasText(keyName, "Key name must not be empty");
Assert.hasText(ciphertext, "Ciphertext must not be empty");
Map<String, String> request = new LinkedHashMap<>();
request.put("ciphertext", ciphertext);
return this.reactiveVaultOperations.write(String.format("%s/rewrap/%s", this.path, keyName), request)
.map(response -> (String) response.getRequiredData().get("ciphertext"));
}
@Override
public Mono<String> rewrap(String keyName, String ciphertext, VaultTransitContext transitContext) {
Assert.hasText(keyName, "Key name must not be empty");
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);
return this.reactiveVaultOperations.write(String.format("%s/rewrap/%s", this.path, keyName), request)
.map(response -> (String) response.getRequiredData().get("ciphertext"));
}
@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");
return Flux.fromIterable(batchRequest).map(request -> {
Map<String, String> vaultRequest = new LinkedHashMap<>(2);
vaultRequest.put("plaintext", Base64.getEncoder().encodeToString(request.getPlaintext()));
applyTransitOptions(request.getContext(), vaultRequest);
return vaultRequest;
}).collectList()
.flatMap(batch -> this.reactiveVaultOperations.write(String.format("%s/encrypt/%s", this.path, keyName),
Collections.singletonMap("batch_input", batch)))
.flatMapIterable(vaultResponse -> toEncryptionResults(vaultResponse, batchRequest));
}
@Override
public Flux<VaultDecryptionResult> decrypt(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(request -> {
Map<String, String> vaultRequest = new LinkedHashMap<>(2);
vaultRequest.put("ciphertext", request.getCiphertext());
applyTransitOptions(request.getContext(), vaultRequest);
return vaultRequest;
}).collectList()
.flatMap(batch -> this.reactiveVaultOperations.write(String.format("%s/decrypt/%s", this.path, keyName),
Collections.singletonMap("batch_input", batch)))
.flatMapIterable(vaultResponse -> toDecryptionResults(vaultResponse, batchRequest));
}
@Override
public Mono<Hmac> getHmac(String keyName, Plaintext plaintext) {
Assert.hasText(keyName, "Key name must not be empty");
Assert.notNull(plaintext, "Plaintext must not be null");
VaultHmacRequest request = VaultHmacRequest.create(plaintext);
return getHmac(keyName, request);
}
@Override
public Mono<Hmac> getHmac(String keyName, VaultHmacRequest hmacRequest) {
Assert.hasText(keyName, "Key name must not be empty");
Assert.notNull(hmacRequest, "HMAC request must not be null");
return this.reactiveVaultOperations.write(String.format("%s/hmac/%s", this.path, keyName), hmacRequest)
.map(vaultResponse -> (String) vaultResponse.getRequiredData().get("hmac")).map(Hmac::of);
}
@Override
public Mono<Signature> sign(String keyName, Plaintext plaintext) {
Assert.hasText(keyName, "Key name must not be empty");
Assert.notNull(plaintext, "Plaintext must not be null");
VaultSignRequest request = VaultSignRequest.create(plaintext);
return sign(keyName, request);
}
@Override
public Mono<Signature> sign(String keyName, VaultSignRequest signRequest) {
Assert.hasText(keyName, "Key name must not be empty");
Assert.notNull(signRequest, "Sign request must not be null");
return this.reactiveVaultOperations.write(String.format("%s/sign/%s", this.path, keyName), signRequest)
.map(vaultResponse -> (String) vaultResponse.getRequiredData().get("signature")).map(Signature::of);
}
@Override
public Mono<Boolean> verify(String keyName, Plaintext plaintext, Signature signature) {
Assert.hasText(keyName, "Key name must not be empty");
Assert.notNull(plaintext, "Plaintext must not be null");
Assert.notNull(signature, "Signature must not be null");
VaultSignatureVerificationRequest request = VaultSignatureVerificationRequest.create(plaintext, signature);
return verify(keyName, request).map(SignatureValidation::isValid);
}
@Override
public Mono<SignatureValidation> verify(String keyName, VaultSignatureVerificationRequest verificationRequest) {
Assert.hasText(keyName, "Key name must not be empty");
Assert.notNull(verificationRequest, "Signature verification request must not be null");
return this.reactiveVaultOperations
.write(String.format("%s/verify/%s", this.path, keyName), verificationRequest)
.map(VaultResponse::getRequiredData).map(vaultResponse -> {
if (vaultResponse.containsKey("valid") && (Boolean) vaultResponse.get("valid")) {
return SignatureValidation.valid();
}
return SignatureValidation.invalid();
});
}
@Override
public Mono<RawTransitKey> exportKey(String keyName, TransitKeyType type) {
Assert.hasText(keyName, "Key name must not be empty");
Assert.notNull(type, "Key type must not be null");
return this.reactiveVaultOperations
.read(String.format("%s/export/%s/%s", this.path, type.getValue(), keyName),
VaultTransitTemplate.RawTransitKeyImpl.class)
.flatMap(vaultResponse -> Mono.justOrEmpty(vaultResponse.getRequiredData()));
}
@Override
public Mono<VaultTransitKey> getKey(String keyName) {
Assert.hasText(keyName, "Key name must not be empty");
return this.reactiveVaultOperations
.read(String.format("%s/keys/%s", this.path, keyName), VaultTransitKeyImpl.class)
.map(VaultResponseSupport::getRequiredData);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016-2022 the original author or authors.
* Copyright 2016-2023 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.
@@ -15,15 +15,7 @@
*/
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 java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.Base64Utils;
@@ -49,6 +41,13 @@ import org.springframework.vault.support.VaultTransitKey;
import org.springframework.vault.support.VaultTransitKeyConfiguration;
import org.springframework.vault.support.VaultTransitKeyCreationRequest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Default implementation of {@link VaultTransitOperations}.
*
@@ -383,7 +382,6 @@ public class VaultTransitTemplate implements VaultTransitOperations {
@Override
public Signature sign(String keyName, VaultSignRequest signRequest) {
Assert.hasText(keyName, "Key name must not be empty");
Assert.notNull(signRequest, "Sign request must not be null");
@@ -443,7 +441,7 @@ public class VaultTransitTemplate implements VaultTransitOperations {
return SignatureValidation.invalid();
}
private static void applyTransitOptions(VaultTransitContext context, Map<String, String> request) {
public static void applyTransitOptions(VaultTransitContext context, Map<String, String> request) {
if (!ObjectUtils.isEmpty(context.getContext())) {
request.put("context", Base64Utils.encodeToString(context.getContext()));
@@ -454,7 +452,7 @@ public class VaultTransitTemplate implements VaultTransitOperations {
}
}
private static List<VaultEncryptionResult> toEncryptionResults(VaultResponse vaultResponse,
public static List<VaultEncryptionResult> toEncryptionResults(VaultResponse vaultResponse,
List<Plaintext> batchRequest) {
List<VaultEncryptionResult> result = new ArrayList<>(batchRequest.size());
@@ -484,7 +482,7 @@ public class VaultTransitTemplate implements VaultTransitOperations {
return result;
}
private static List<VaultDecryptionResult> toDecryptionResults(VaultResponse vaultResponse,
public static List<VaultDecryptionResult> toDecryptionResults(VaultResponse vaultResponse,
List<Ciphertext> batchRequest) {
List<VaultDecryptionResult> result = new ArrayList<>(batchRequest.size());
@@ -523,12 +521,12 @@ public class VaultTransitTemplate implements VaultTransitOperations {
return new VaultDecryptionResult(Plaintext.empty().with(ciphertext.getContext()));
}
private static Ciphertext toCiphertext(String ciphertext, @Nullable VaultTransitContext context) {
public static Ciphertext toCiphertext(String ciphertext, @Nullable VaultTransitContext context) {
return context != null ? Ciphertext.of(ciphertext).with(context) : Ciphertext.of(ciphertext);
}
@SuppressWarnings("unchecked")
private static List<Map<String, String>> getBatchData(VaultResponse vaultResponse) {
public static List<Map<String, String>> getBatchData(VaultResponse vaultResponse) {
return (List<Map<String, String>>) vaultResponse.getRequiredData().get("batch_results");
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2023-2023 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
*
* https://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.support;
import com.fasterxml.jackson.databind.util.StdConverter;
import org.springframework.util.Base64Utils;
import java.util.Base64;
/**
* Converts Plaintext to Base64 encoded string for use with
* {@link com.fasterxml.jackson.databind.ObjectMapper}
*
* @author James Luke
*/
public class PlaintextToBase64StringConverter extends StdConverter<Plaintext, String> {
@Override
public String convert(Plaintext plaintext) {
return Base64.getEncoder().encodeToString(plaintext.getPlaintext());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017-2022 the original author or authors.
* Copyright 2017-2023 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.
@@ -15,6 +15,9 @@
*/
package org.springframework.vault.support;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -25,12 +28,17 @@ import org.springframework.util.Assert;
* @author Mark Paluch
* @since 2.0
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public class VaultHmacRequest {
@JsonProperty("input")
@JsonSerialize(converter = PlaintextToBase64StringConverter.class)
private final Plaintext plaintext;
@JsonProperty("algorithm")
private final @Nullable String algorithm;
@JsonProperty("key_version")
private final @Nullable Integer keyVersion;
private VaultHmacRequest(Plaintext plaintext, @Nullable String algorithm, @Nullable Integer keyVersion) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017-2022 the original author or authors.
* Copyright 2017-2023 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.
@@ -15,6 +15,8 @@
*/
package org.springframework.vault.support;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -28,6 +30,8 @@ import org.springframework.util.Assert;
*/
public class VaultSignRequest {
@JsonProperty("input")
@JsonSerialize(converter = PlaintextToBase64StringConverter.class)
private final Plaintext plaintext;
private final @Nullable String hashAlgorithm;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016-2022 the original author or authors.
* Copyright 2016-2023 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.
@@ -15,8 +15,16 @@
*/
package org.springframework.vault.support;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.vault.support.VaultSignatureVerificationRequest.VaultSignatureVerificationRequestSerializer;
import java.io.IOException;
/**
* Request for a signature verification.
@@ -24,8 +32,10 @@ import org.springframework.util.Assert;
* @author Luander Ribeiro
* @author Mark Paluch
* @author My-Lan Aragon
* @author James Luke
* @since 2.0
*/
@JsonSerialize(using = VaultSignatureVerificationRequestSerializer.class)
public class VaultSignatureVerificationRequest {
private final Plaintext plaintext;
@@ -135,6 +145,31 @@ public class VaultSignatureVerificationRequest {
return getSignatureAlgorithm();
}
static class VaultSignatureVerificationRequestSerializer extends JsonSerializer<VaultSignatureVerificationRequest> {
static PlaintextToBase64StringConverter plaintextConverter = new PlaintextToBase64StringConverter();
@Override
public void serialize(VaultSignatureVerificationRequest request, JsonGenerator gen,
SerializerProvider serializers) throws IOException {
gen.writeStartObject();
gen.writeStringField("input", plaintextConverter.convert(request.plaintext));
if (request.getHmac() != null) {
gen.writeStringField("hmac", request.getHmac().getHmac());
}
if (request.getSignature() != null) {
gen.writeStringField("signature", request.getSignature().getSignature());
}
if (StringUtils.hasText(request.getAlgorithm())) {
gen.writeStringField("algorithm", request.getAlgorithm());
}
gen.writeEndObject();
}
}
/**
* Builder to build a {@link VaultSignatureVerificationRequest}.
*/

View File

@@ -0,0 +1,725 @@
/*
* Copyright 2023-2023 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
*
* https://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.core;
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.vault.VaultException;
import org.springframework.vault.support.Ciphertext;
import org.springframework.vault.support.Plaintext;
import org.springframework.vault.support.SignatureValidation;
import org.springframework.vault.support.TransitKeyType;
import org.springframework.vault.support.VaultDecryptionResult;
import org.springframework.vault.support.VaultHmacRequest;
import org.springframework.vault.support.VaultMount;
import org.springframework.vault.support.VaultSignRequest;
import org.springframework.vault.support.VaultSignatureVerificationRequest;
import org.springframework.vault.support.VaultTransitContext;
import org.springframework.vault.support.VaultTransitKeyConfiguration;
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.*;
/**
* Integration tests for {@link ReactiveVaultTransitTemplate} using the {@code generic}
* backend.
*
* @author James Luke
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = VaultIntegrationTestConfiguration.class)
public class ReactiveVaultTransitIntegrationTests extends IntegrationTestSupport {
@Autowired
VaultOperations vaultOperations;
@Autowired
ReactiveVaultOperations reactiveVaultOperations;
ReactiveVaultTransitOperations reactiveTransitOperations;
Version vaultVersion;
@BeforeEach
void before() {
this.reactiveTransitOperations = this.reactiveVaultOperations.opsForTransit();
if (!this.vaultOperations.opsForSys().getMounts().containsKey("transit/")) {
this.vaultOperations.opsForSys().mount("transit", VaultMount.create("transit"));
}
this.vaultVersion = prepare().getVersion();
removeKeys();
}
@AfterEach
void tearDown() {
removeKeys();
}
private Mono<Void> deleteKey(String keyName) {
return this.reactiveTransitOperations
.configureKey(keyName, VaultTransitKeyConfiguration.builder().deletionAllowed(true).build())
.and(this.reactiveTransitOperations.deleteKey(keyName)).onErrorResume(e -> Mono.empty());
}
private void removeKeys() {
reactiveTransitOperations.getKeys().flatMap(this::deleteKey).blockLast();
}
@Test
void createKeyShouldCreateKey() {
this.reactiveTransitOperations.createKey("myKey").then(this.reactiveTransitOperations.getKey("myKey"))
.as(StepVerifier::create).assertNext(myKey -> {
assertThat(myKey).isNotNull();
assertThat(myKey.getType()).startsWith("aes");
assertThat(myKey.getName()).isEqualTo("myKey");
assertThat(myKey.isDeletionAllowed()).isFalse();
assertThat(myKey.isDerived()).isFalse();
assertThat(myKey.getMinDecryptionVersion()).isEqualTo(1);
assertThat(myKey.getLatestVersion()).isEqualTo(1);
if (this.vaultVersion.isGreaterThanOrEqualTo(Version.parse("0.7.0"))) {
assertThat(myKey.supportsDecryption()).isTrue();
assertThat(myKey.supportsEncryption()).isTrue();
assertThat(myKey.supportsDerivation()).isTrue();
assertThat(myKey.supportsSigning()).isFalse();
}
}).verifyComplete();
}
@Test
@RequiresVaultVersion("0.6.4")
void createKeyShouldCreateEcDsaKey() {
createEcdsaP256Key().flatMap(keyName -> this.reactiveTransitOperations.getKey(keyName)).as(StepVerifier::create)
.assertNext(myKey -> {
assertThat(myKey).isNotNull();
assertThat(myKey.getType()).startsWith("ecdsa");
assertThat(myKey.getKeys()).isNotEmpty();
}).verifyComplete();
}
@Test
@RequiresVaultVersion(ED25519_INTRODUCED_IN_VERSION)
void createKeyShouldCreateEdKey() {
VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.ofKeyType("ed25519");
this.reactiveTransitOperations.createKey("ed-key", request)
.then(this.reactiveTransitOperations.getKey("ed-key")).as(StepVerifier::create).assertNext(myKey -> {
assertThat(myKey).isNotNull();
assertThat(myKey.getType()).startsWith("ed");
assertThat(myKey.getKeys()).isNotEmpty();
}).verifyComplete();
}
@Test
@RequiresVaultVersion(ECDSA521_INTRODUCED_IN_VERSION)
void createKeyShouldCreateEcdsaKey() {
VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.ofKeyType("ecdsa-p521");
this.reactiveTransitOperations.createKey("ecdsa-p521-key", request)
.then(this.reactiveTransitOperations.getKey("ecdsa-p521-key")).as(StepVerifier::create)
.assertNext(myKey -> {
assertThat(myKey.getType()).isEqualTo("ecdsa-p521");
assertThat(myKey.getKeys()).isNotEmpty();
}).verifyComplete();
}
@Test
@RequiresVaultVersion(RSA3072_INTRODUCED_IN_VERSION)
void createKeyShouldCreateRsa3072Key() {
VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.ofKeyType("rsa-3072");
this.reactiveTransitOperations.createKey("rsa-3072-key", request)
.then(this.reactiveTransitOperations.getKey("rsa-3072-key")).as(StepVerifier::create)
.assertNext(myKey -> {
assertThat(myKey.getType()).isEqualTo("rsa-3072");
assertThat(myKey.getKeys()).isNotEmpty();
}).verifyComplete();
}
@Test
@RequiresVaultVersion(AES256_GCM96_INTRODUCED_IN_VERSION)
void createKeyShouldCreateAes256Gcm96Key() {
VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.ofKeyType("aes256-gcm96");
this.reactiveTransitOperations.createKey("aes256-gcm96-key", request)
.then(this.reactiveTransitOperations.getKey("aes256-gcm96-key")).as(StepVerifier::create)
.assertNext(myKey -> {
assertThat(myKey.getType()).isEqualTo("aes256-gcm96");
assertThat(myKey.getKeys()).isNotEmpty();
}).verifyComplete();
}
@Test
void createKeyShouldCreateKeyWithOptions() {
VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder().convergentEncryption(true)
.derived(true).build();
this.reactiveTransitOperations.createKey("myKey", request).then(this.reactiveTransitOperations.getKey("myKey"))
.as(StepVerifier::create).assertNext(myKey -> {
assertThat(myKey.getName()).isEqualTo("myKey");
assertThat(myKey.isDeletionAllowed()).isFalse();
assertThat(myKey.isDerived()).isTrue();
assertThat(myKey.getMinDecryptionVersion()).isEqualTo(1);
assertThat(myKey.getLatestVersion()).isEqualTo(1);
}).verifyComplete();
}
@Test
void shouldConfigureKey() {
VaultTransitKeyConfiguration configuration = VaultTransitKeyConfiguration.builder().deletionAllowed(true)
.minDecryptionVersion(1).minEncryptionVersion(2).build();
this.reactiveTransitOperations.createKey("myKey").then(this.reactiveTransitOperations.rotate("myKey"))
.then(this.reactiveTransitOperations.rotate("myKey"))
.then(this.reactiveTransitOperations.configureKey("myKey", configuration))
.then(this.reactiveTransitOperations.getKey("myKey")).as(StepVerifier::create).assertNext(myKey -> {
assertThat(myKey.getMinDecryptionVersion()).isEqualTo(1);
if (this.vaultVersion.isGreaterThanOrEqualTo(Version.parse("0.8.0"))) {
assertThat(myKey.getMinEncryptionVersion()).isEqualTo(2);
}
else {
assertThat(myKey.getMinEncryptionVersion()).isEqualTo(0);
}
}).verifyComplete();
}
@Test
@RequiresVaultVersion("0.6.4")
void shouldEnumerateKey() {
this.reactiveTransitOperations.getKeys().as(StepVerifier::create).verifyComplete();
this.reactiveTransitOperations.createKey("myKey").thenMany(this.reactiveTransitOperations.getKeys())
.as(StepVerifier::create).assertNext(keys -> assertThat(keys).contains("myKey")).verifyComplete();
}
@Test
void getKeyShouldReturnEmptyIfKeyNotExists() {
this.reactiveTransitOperations.getKey("myKey").as(StepVerifier::create).verifyComplete();
}
@Test
void deleteKeyShouldFailIfKeyNotExists() {
this.reactiveTransitOperations.deleteKey("myKey").as(StepVerifier::create)
.consumeErrorWith(e -> assertThat(e).hasMessageContaining("Status 400")).verify();
}
@Test
void deleteKeyShouldDeleteKey() {
VaultTransitKeyConfiguration configuration = VaultTransitKeyConfiguration.builder().deletionAllowed(true)
.build();
this.reactiveTransitOperations.createKey("myKey")
.then(this.reactiveTransitOperations.configureKey("myKey", configuration))
.then(this.reactiveTransitOperations.deleteKey("myKey"))
.then(this.reactiveTransitOperations.getKey("myKey")).as(StepVerifier::create).verifyComplete();
}
@Test
void encryptShouldCreateCiphertext() {
this.reactiveTransitOperations.createKey("myKey")
.then(this.reactiveTransitOperations.encrypt("myKey", "hello-world")).as(StepVerifier::create)
.assertNext(ciphertext -> assertThat(ciphertext).startsWith("vault:v")).verifyComplete();
}
@Test
void encryptShouldCreateCiphertextWithNonceAndContext() {
VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder().convergentEncryption(true)
.derived(true).build();
VaultTransitContext context = VaultTransitContext.builder().context("blubb".getBytes())
.nonce("123456789012".getBytes()).build();
this.reactiveTransitOperations.createKey("mykey", request)
.then(this.reactiveTransitOperations.encrypt("myKey", "hello-world".getBytes(), context))
.as(StepVerifier::create).assertNext(ciphertext -> assertThat(ciphertext).startsWith("vault:v1:"))
.verifyComplete();
}
@Test
@RequiresVaultVersion(BATCH_INTRODUCED_IN_VERSION)
void encryptShouldEncryptEmptyValues() {
VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder().convergentEncryption(true)
.derived(true).build();
VaultTransitContext context = VaultTransitContext.builder().context("blubb".getBytes())
.nonce("123456789012".getBytes()).build();
this.reactiveTransitOperations.createKey("myKey", request)
.then(this.reactiveTransitOperations.encrypt("myKey", Plaintext.of("").with(context)))
.as(StepVerifier::create).assertNext(ciphertext -> {
assertThat(ciphertext.getCiphertext()).startsWith("vault:v1:");
assertThat(ciphertext.getContext()).isEqualTo(context);
}).verifyComplete();
}
@Test
void encryptShouldCreateWrappedCiphertextWithNonceAndContext() {
VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder().convergentEncryption(true)
.derived(true).build();
VaultTransitContext context = VaultTransitContext.builder().context("blubb".getBytes())
.nonce("123456789012".getBytes()).build();
this.reactiveTransitOperations.createKey("myKey", request)
.then(this.reactiveTransitOperations.encrypt("myKey", Plaintext.of("hello-world").with(context)))
.as(StepVerifier::create).assertNext(ciphertext -> {
assertThat(ciphertext.getCiphertext()).startsWith("vault:v1:");
assertThat(ciphertext.getContext()).isEqualTo(context);
}).verifyComplete();
}
@Test
void decryptShouldCreatePlaintext() {
this.reactiveTransitOperations.createKey("myKey")
.then(this.reactiveTransitOperations.encrypt("myKey", "hello-world"))
.flatMap(ciphertext -> this.reactiveTransitOperations.decrypt("myKey", ciphertext))
.as(StepVerifier::create).assertNext(plaintext -> assertThat(plaintext).isEqualTo("hello-world"))
.verifyComplete();
}
@Test
void decryptShouldCreatePlaintextWithNonceAndContext() {
VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder().convergentEncryption(true)
.derived(true).build();
VaultTransitContext transitRequest = VaultTransitContext.builder().context("blubb".getBytes())
.nonce("123456789012".getBytes()).build();
this.reactiveTransitOperations.createKey("myKey", request)
.then(this.reactiveTransitOperations.encrypt("myKey", "hello-world".getBytes(), transitRequest))
.flatMap(ciphertext -> this.reactiveTransitOperations.decrypt("myKey", ciphertext, transitRequest))
.as(StepVerifier::create)
.assertNext(plaintext -> assertThat(new String(plaintext)).isEqualTo("hello-world")).verifyComplete();
}
@Test
void decryptShouldCreateWrappedPlaintextWithNonceAndContext() {
VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder().convergentEncryption(true)
.derived(true).build();
VaultTransitContext context = VaultTransitContext.builder().context("blubb".getBytes())
.nonce("123456789012".getBytes()).build();
this.reactiveTransitOperations.createKey("myKey", request)
.then(this.reactiveTransitOperations.encrypt("myKey", Plaintext.of("hello-world").with(context)))
.flatMap(ciphertext -> this.reactiveTransitOperations.decrypt("myKey", ciphertext))
.as(StepVerifier::create).assertNext(plaintext -> {
assertThat(plaintext.asString()).isEqualTo("hello-world");
assertThat(plaintext.getContext()).isEqualTo(context);
}).verifyComplete();
}
@Test
void encryptAndRewrapShouldCreateCiphertext() {
String ciphertext = this.reactiveTransitOperations.createKey("myKey")
.then(this.reactiveTransitOperations.encrypt("myKey", "hello-world")).block();
assertThat(ciphertext).isNotNull();
this.reactiveTransitOperations.rotate("myKey").then(this.reactiveTransitOperations.rewrap("myKey", ciphertext))
.as(StepVerifier::create).assertNext(rewrapped -> assertThat(rewrapped).startsWith("vault:v2:"))
.verifyComplete();
}
@Test
void shouldEncryptBinaryPlaintext() {
this.reactiveTransitOperations.createKey("myKey");
byte[] plaintext = new byte[] { 1, 2, 3, 4, 5 };
this.reactiveTransitOperations.encrypt("myKey", plaintext, VaultTransitContext.empty())
.flatMap(ciphertext -> this.reactiveTransitOperations.decrypt("myKey", ciphertext,
VaultTransitContext.empty()))
.as(StepVerifier::create).assertNext(decrypted -> assertThat(decrypted).isEqualTo(plaintext))
.verifyComplete();
}
@Test
void encryptAndRewrapShouldCreateCiphertextWithNonceAndContext() {
VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder().convergentEncryption(true)
.derived(true).build();
VaultTransitContext transitRequest = VaultTransitContext.builder().context("blubb".getBytes())
.nonce("123456789012".getBytes()).build();
String ciphertext = this.reactiveTransitOperations.createKey("myKey", request)
.then(this.reactiveTransitOperations.encrypt("myKey", "hello-world".getBytes(), transitRequest))
.block();
assertThat(ciphertext).isNotNull();
this.reactiveTransitOperations.rotate("myKey")
.then(this.reactiveTransitOperations.rewrap("myKey", ciphertext, transitRequest))
.as(StepVerifier::create).assertNext(rewrapped -> assertThat(rewrapped).startsWith("vault:v2"))
.verifyComplete();
}
@Test
@RequiresVaultVersion(BATCH_INTRODUCED_IN_VERSION)
void shouldBatchEncrypt() {
this.reactiveTransitOperations.createKey("myKey")
.thenMany(this.reactiveTransitOperations.encrypt("myKey",
Arrays.asList(Plaintext.of("one"), Plaintext.of("two"))))
.as(StepVerifier::create).assertNext(encrypted -> {
assertThat(encrypted.get()).isNotNull();
assertThat(Objects.requireNonNull(encrypted.get()).getCiphertext()).startsWith("vault:");
}).assertNext(encrypted -> {
assertThat(encrypted.get()).isNotNull();
assertThat(Objects.requireNonNull(encrypted.get()).getCiphertext()).startsWith("vault:");
}).verifyComplete();
}
@Test
@RequiresVaultVersion(BATCH_INTRODUCED_IN_VERSION)
void shouldBatchDecrypt() {
this.reactiveTransitOperations.createKey("myKey").block();
Ciphertext one = this.reactiveTransitOperations.encrypt("myKey", Plaintext.of("one")).block();
Ciphertext two = this.reactiveTransitOperations.encrypt("myKey", Plaintext.of("two")).block();
assertThat(one).isNotNull();
assertThat(two).isNotNull();
this.reactiveTransitOperations.decrypt("myKey", Arrays.asList(one, two))
.zipWith(Flux.merge(this.reactiveTransitOperations.decrypt("myKey", one),
this.reactiveTransitOperations.decrypt("myKey", two)))
.as(StepVerifier::create).assertNext(it -> {
assertThat(it.getT1().getAsString()).isEqualTo(it.getT2().asString());
assertThat(it.getT1().getAsString()).isEqualTo("one");
}).assertNext(it -> {
assertThat(it.getT1().getAsString()).isEqualTo(it.getT2().asString());
assertThat(it.getT1().getAsString()).isEqualTo("two");
}).verifyComplete();
}
@Test
@RequiresVaultVersion(BATCH_INTRODUCED_IN_VERSION)
void shouldBatchEncryptWithContext() {
VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder().derived(true).build();
VaultTransitContext context1 = VaultTransitContext.builder().context("oneContext".getBytes()).build();
VaultTransitContext context2 = VaultTransitContext.builder().context("twoContext".getBytes()).build();
this.reactiveTransitOperations.createKey("myKey", request)
.thenMany(this.reactiveTransitOperations.encrypt("myKey",
Arrays.asList(Plaintext.of("one").with(context1), Plaintext.of("two").with(context2))))
.as(StepVerifier::create)
.assertNext(it -> assertThat(Objects.requireNonNull(it.get()).getContext()).isEqualTo(context1))
.assertNext(it -> assertThat(Objects.requireNonNull(it.get()).getContext()).isEqualTo(context2))
.verifyComplete();
}
@Test
@RequiresVaultVersion(BATCH_INTRODUCED_IN_VERSION)
void shouldBatchDecryptWithContext() {
VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder().derived(true).build();
Plaintext one = Plaintext.of("one")
.with(VaultTransitContext.builder().context("oneContext".getBytes()).build());
Plaintext two = Plaintext.of("two")
.with(VaultTransitContext.builder().context("twoContext".getBytes()).build());
this.reactiveTransitOperations.createKey("myKey", request)
.thenMany(this.reactiveTransitOperations.encrypt("myKey", Arrays.asList(one, two)))
.flatMap(it -> Mono.justOrEmpty(it.get())).collectList()
.flatMapMany(it -> this.reactiveTransitOperations.decrypt("myKey", it)).as(StepVerifier::create)
.assertNext(it -> assertThat(it.get()).isEqualTo(one))
.assertNext(it -> assertThat(it.get()).isEqualTo(two)).verifyComplete();
}
@Test
@RequiresVaultVersion(BATCH_INTRODUCED_IN_VERSION)
void shouldBatchDecryptWithWrongContext() {
VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder().derived(true).build();
Plaintext one = Plaintext.of("one")
.with(VaultTransitContext.builder().context("oneContext".getBytes()).build());
Plaintext two = Plaintext.of("two")
.with(VaultTransitContext.builder().context("twoContext".getBytes()).build());
List<Ciphertext> encrypted = this.reactiveTransitOperations.createKey("myKey", request)
.thenMany(this.reactiveTransitOperations.encrypt("myKey", Arrays.asList(one, two)))
.flatMap(it -> Mono.justOrEmpty(it.get())).collectList().block();
assertThat(encrypted).isNotNull();
Ciphertext encryptedOne = encrypted.get(0);
Ciphertext decryptedTwo = encrypted.get(1);
Ciphertext tampered = decryptedTwo.with(encryptedOne.getContext());
StepVerifier.FirstStep<VaultDecryptionResult> stepVerifier = this.reactiveTransitOperations
.decrypt("myKey", Arrays.asList(encryptedOne, tampered)).as(StepVerifier::create);
if (this.vaultVersion.isGreaterThanOrEqualTo(Version.parse("1.6.0"))) {
stepVerifier.consumeErrorWith(e -> assertThat(e).hasMessageContaining("error")).verify();
}
else {
stepVerifier.assertNext(it -> assertThat(it.get()).isEqualTo(one)).assertNext(it -> {
assertThat(it.isSuccessful()).isEqualTo(false);
assertThat(it.getCause()).isInstanceOf(VaultException.class);
}).verifyComplete();
}
}
@Test
@RequiresVaultVersion(BATCH_INTRODUCED_IN_VERSION)
void shouldBatchDecryptEmptyPlaintext() {
this.reactiveTransitOperations.createKey("myKey")
.then(this.reactiveTransitOperations.encrypt("myKey", Plaintext.empty()))
.flatMapMany(empty -> this.reactiveTransitOperations.decrypt("myKey", Collections.singletonList(empty)))
.as(StepVerifier::create).assertNext(it -> assertThat(it.getAsString()).isEmpty()).verifyComplete();
}
@Test
@RequiresVaultVersion(BATCH_INTRODUCED_IN_VERSION)
void shouldBatchDecryptEmptyPlaintextWithContext() {
VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder().derived(true).build();
Plaintext empty = Plaintext.empty()
.with(VaultTransitContext.builder().context("oneContext".getBytes()).build());
this.reactiveTransitOperations.createKey("myKey", request)
.thenMany(this.reactiveTransitOperations.encrypt("myKey", Collections.singletonList(empty)))
.flatMap(it -> Mono.justOrEmpty(it.get())).collectList()
.flatMapMany(it -> this.reactiveTransitOperations.decrypt("myKey", it)).as(StepVerifier::create)
.assertNext(it -> assertThat(it.get()).isEqualTo(empty)).verifyComplete();
}
@Test
@RequiresVaultVersion(SIGN_VERIFY_INTRODUCED_IN_VERSION)
void generateHmacShouldCreateHmac() {
createEcdsaP256Key()
.flatMap(keyName -> this.reactiveTransitOperations.getHmac(keyName, Plaintext.of("hello-world")))
.as(StepVerifier::create).assertNext(hmac -> assertThat(hmac.getHmac()).isNotEmpty()).verifyComplete();
}
@Test
@RequiresVaultVersion(SIGN_VERIFY_INTRODUCED_IN_VERSION)
void generateHmacShouldCreateHmacForRotatedKey() {
VaultHmacRequest request = VaultHmacRequest.builder().plaintext(Plaintext.of("hello-world")).keyVersion(2)
.build();
createEcdsaP256Key()
.flatMap(keyName -> this.reactiveTransitOperations.rotate(keyName)
.then(this.reactiveTransitOperations.getHmac(keyName, request)))
.as(StepVerifier::create).assertNext(hmac -> assertThat(hmac.getHmac()).isNotEmpty()).verifyComplete();
}
@Test
@RequiresVaultVersion(SIGN_VERIFY_INTRODUCED_IN_VERSION)
void generateHmacWithCustomAlgorithmShouldCreateHmac() {
VaultHmacRequest request = VaultHmacRequest.builder().plaintext(Plaintext.of("hello-world"))
.algorithm("sha2-512").build();
createEcdsaP256Key().flatMap(keyName -> this.reactiveTransitOperations.getHmac(keyName, request))
.as(StepVerifier::create).assertNext(hmac -> assertThat(hmac.getHmac()).isNotEmpty()).verifyComplete();
}
@Test
void generateHmacWithInvalidAlgorithmShouldFail() {
VaultHmacRequest request = VaultHmacRequest.builder().plaintext(Plaintext.of("hello-world"))
.algorithm("blah-512").build();
createEcdsaP256Key().flatMap(keyName -> this.reactiveTransitOperations.getHmac("myKey", request))
.as(StepVerifier::create).consumeErrorWith(e -> assertThat(e).isInstanceOf(VaultException.class))
.verify();
}
@Test
@RequiresVaultVersion(SIGN_VERIFY_INTRODUCED_IN_VERSION)
void signShouldCreateSignature() {
createEcdsaP256Key()
.flatMap(keyName -> this.reactiveTransitOperations.sign(keyName, Plaintext.of("hello-world")))
.as(StepVerifier::create).assertNext(signature -> assertThat(signature.getSignature()).isNotEmpty())
.verifyComplete();
}
@Test
@RequiresVaultVersion(ED25519_INTRODUCED_IN_VERSION)
void signShouldCreateSignatureUsingEd25519() {
VaultTransitKeyCreationRequest keyCreationRequest = VaultTransitKeyCreationRequest.ofKeyType("ed25519");
this.reactiveTransitOperations.createKey("ed-key", keyCreationRequest)
.then(this.reactiveTransitOperations.sign("ed-key", Plaintext.of("hello-world")))
.as(StepVerifier::create).assertNext(signature -> assertThat(signature.getSignature()).isNotEmpty())
.verifyComplete();
}
@Test
void signWithInvalidKeyFormatShouldFail() {
this.reactiveTransitOperations.createKey("myKey")
.then(this.reactiveTransitOperations.sign("myKey", Plaintext.of("hello-world")))
.as(StepVerifier::create).consumeErrorWith(e -> assertThat(e).isInstanceOf(VaultException.class))
.verify();
}
@Test
@RequiresVaultVersion(SIGN_VERIFY_INTRODUCED_IN_VERSION)
void signWithCustomAlgorithmShouldCreateSignature() {
VaultSignRequest request = VaultSignRequest.builder().plaintext(Plaintext.of("hello-world"))
.signatureAlgorithm("sha2-512").build();
createEcdsaP256Key().flatMap(keyName -> this.reactiveTransitOperations.sign(keyName, request))
.as(StepVerifier::create).assertNext(signature -> assertThat(signature.getSignature()).isNotEmpty())
.verifyComplete();
}
@Test
@RequiresVaultVersion(SIGN_VERIFY_INTRODUCED_IN_VERSION)
void shouldVerifyValidSignature() {
Plaintext plaintext = Plaintext.of("hello-world");
createEcdsaP256Key()
.flatMap(keyName -> this.reactiveTransitOperations.sign(keyName, plaintext)
.flatMap(signature -> this.reactiveTransitOperations.verify(keyName, plaintext, signature)))
.as(StepVerifier::create).assertNext(valid -> assertThat(valid).isTrue()).verifyComplete();
}
@Test
@RequiresVaultVersion(SIGN_VERIFY_INTRODUCED_IN_VERSION)
void shouldVerifyValidHmac() {
Plaintext plaintext = Plaintext.of("hello-world");
createEcdsaP256Key()
.flatMap(keyName -> this.reactiveTransitOperations.getHmac(keyName, plaintext)
.flatMap(hmac -> this.reactiveTransitOperations.verify(keyName,
VaultSignatureVerificationRequest.create(plaintext, hmac))))
.as(StepVerifier::create).assertNext(valid -> assertThat(valid).isEqualTo(SignatureValidation.valid()))
.verifyComplete();
}
@Test
@RequiresVaultVersion(SIGN_VERIFY_INTRODUCED_IN_VERSION)
void shouldVerifyValidSignatureWithCustomAlgorithm() {
Plaintext plaintext = Plaintext.of("hello-world");
VaultSignRequest request = VaultSignRequest.builder().plaintext(plaintext).signatureAlgorithm("sha2-512")
.build();
createEcdsaP256Key()
.flatMap((keyName) -> this.reactiveTransitOperations.sign(keyName, request)
.map(signature -> VaultSignatureVerificationRequest.builder().signatureAlgorithm("sha2-512")
.plaintext(plaintext).signature(signature).build())
.flatMap(verificationRequest -> this.reactiveTransitOperations.verify(keyName,
verificationRequest)))
.as(StepVerifier::create).assertNext(valid -> assertThat(valid).isEqualTo(SignatureValidation.valid()))
.verifyComplete();
}
@Test
@RequiresVaultVersion(KEY_EXPORT_INTRODUCED_IN_VERSION)
void shouldCreateNewExportableKey() {
VaultTransitKeyCreationRequest vaultTransitKeyCreationRequest = VaultTransitKeyCreationRequest.builder()
.exportable(true).derived(true).build();
reactiveTransitOperations.createKey("myKey", vaultTransitKeyCreationRequest)
.then(reactiveTransitOperations.getKey("myKey")).as(StepVerifier::create)
.assertNext(vaultTransitKey -> {
assertThat(vaultTransitKey.getName()).isEqualTo("myKey");
assertThat(vaultTransitKey.isExportable()).isTrue();
}).verifyComplete();
}
@Test
@RequiresVaultVersion(KEY_EXPORT_INTRODUCED_IN_VERSION)
void shouldCreateNotExportableKeyByDefault() {
reactiveTransitOperations.createKey("myKey").then(reactiveTransitOperations.getKey("myKey"))
.as(StepVerifier::create).assertNext(vaultTransitKey -> {
assertThat(vaultTransitKey.getName()).isEqualTo("myKey");
assertThat(vaultTransitKey.isExportable()).isFalse();
}).verifyComplete();
}
@Test
@RequiresVaultVersion(KEY_EXPORT_INTRODUCED_IN_VERSION)
void shouldExportEncryptionKey() {
VaultTransitKeyCreationRequest vaultTransitKeyCreationRequest = VaultTransitKeyCreationRequest.builder()
.exportable(true).build();
reactiveTransitOperations.createKey("myKey", vaultTransitKeyCreationRequest)
.then(reactiveTransitOperations.exportKey("myKey", TransitKeyType.ENCRYPTION_KEY))
.as(StepVerifier::create).assertNext(rawTransitKey -> {
assertThat(rawTransitKey.getName()).isEqualTo("myKey");
assertThat(rawTransitKey.getKeys()).isNotEmpty();
assertThat(rawTransitKey.getKeys().get("1")).isNotBlank();
}).verifyComplete();
}
@Test
@RequiresVaultVersion(KEY_EXPORT_INTRODUCED_IN_VERSION)
void shouldNotAllowExportSigningKey() {
VaultTransitKeyCreationRequest vaultTransitKeyCreationRequest = VaultTransitKeyCreationRequest.builder()
.exportable(true).build();
reactiveTransitOperations.createKey("myKey", vaultTransitKeyCreationRequest)
.then(reactiveTransitOperations.exportKey("myKey", TransitKeyType.SIGNING_KEY)).as(StepVerifier::create)
.consumeErrorWith(e -> assertThat(e).isInstanceOf(VaultException.class)).verify();
}
@Test
@RequiresVaultVersion(KEY_EXPORT_INTRODUCED_IN_VERSION)
void shouldExportEcDsaKey() {
VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder().type("ecdsa-p256")
.exportable(true).build();
this.reactiveTransitOperations.createKey("myKey", request)
.thenMany(Flux.merge(this.reactiveTransitOperations.exportKey("myKey", TransitKeyType.HMAC_KEY),
this.reactiveTransitOperations.exportKey("myKey", TransitKeyType.SIGNING_KEY)))
.as(StepVerifier::create).assertNext(hmacKey -> assertThat(hmacKey.getKeys()).isNotEmpty())
.assertNext(signingKey -> assertThat(signingKey.getKeys()).isNotEmpty()).verifyComplete();
}
@Test
@RequiresVaultVersion(ED25519_INTRODUCED_IN_VERSION)
void shouldExportEdKey() {
VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder().type("ed25519")
.exportable(true).build();
this.reactiveTransitOperations.createKey("myKey", request)
.thenMany(Flux.merge(this.reactiveTransitOperations.exportKey("myKey", TransitKeyType.HMAC_KEY),
this.reactiveTransitOperations.exportKey("myKey", TransitKeyType.SIGNING_KEY)))
.as(StepVerifier::create).assertNext(hmacKey -> assertThat(hmacKey.getKeys()).isNotEmpty())
.assertNext(signingKey -> assertThat(signingKey.getKeys()).isNotEmpty()).verifyComplete();
}
private Mono<String> createEcdsaP256Key() {
String keyName = "ecdsa-key";
VaultTransitKeyCreationRequest keyCreationRequest = VaultTransitKeyCreationRequest.ofKeyType("ecdsa-p256");
return this.reactiveTransitOperations.createKey(keyName, keyCreationRequest).thenReturn(keyName);
}
}