Adding Transform Secrets Engine support

This feature is only available if your vault binary is an enterprise
version 1.4 or higher.

Original pull request: gh-570.
This commit is contained in:
Lauren Voswinkel
2020-07-16 16:38:26 -07:00
committed by Mark Paluch
parent 06eb31857f
commit 1d18e52a51
13 changed files with 1363 additions and 7 deletions

View File

@@ -84,6 +84,21 @@ public interface VaultOperations {
*/
VaultTokenOperations opsForToken();
/**
* @return the operations interface to interact with the Vault transform backend.
* @since 2.3
*/
VaultTransformOperations opsForTransform();
/**
* Return {@link VaultTransformOperations} if the transit backend is mounted on a
* different path than {@code transform}.
* @param path the mount path
* @return the operations interface to interact with the Vault transform backend.
* @since 2.3
*/
VaultTransformOperations opsForTransform(String path);
/**
* @return the operations interface to interact with the Vault transit backend.
*/

View File

@@ -320,6 +320,16 @@ public class VaultTemplate implements InitializingBean, VaultOperations, Disposa
return new VaultTokenTemplate(this);
}
@Override
public VaultTransformOperations opsForTransform() {
return opsForTransform("transform");
}
@Override
public VaultTransformOperations opsForTransform(String path) {
return new VaultTransformTemplate(this, path);
}
@Override
public VaultTransitOperations opsForTransit() {
return opsForTransit("transit");

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2020 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.*;
import java.util.List;
/**
* Interface that specifies operations using the {@code transform} backend.
*
* @author Lauren Voswinkel
* @see <a href="https://www.vaultproject.io/docs/secrets/transform/index.html">Transform
* Secrets Engine</a>
* @since 2.3
*/
public interface VaultTransformOperations {
/**
* Encodes the provided plaintext using the named role.
* @param roleName must not be empty or {@literal null}.
* @param plaintext must not be empty or {@literal null}.
* @return cipher text.
*/
String encode(String roleName, String plaintext);
/**
* Encodes the provided plaintext using the named role.
* @param roleName must not be empty or {@literal null}.
* @param plaintext must not be {@literal null}.
* @return cipher text.
*/
TransformCiphertext encode(String roleName, TransformPlaintext plaintext);
/**
* Encodes the provided plaintext using the named role.
* @param roleName must not be empty or {@literal null}.
* @param plaintext must not be empty or {@literal null}.
* @param transformRequest must not be {@literal null}. Use
* {@link VaultTransformContext#empty()} if no request options provided.
* @return cipher text.
*/
String encode(String roleName, byte[] plaintext, VaultTransformContext transformRequest);
/**
* Encode the provided batch of plaintext using the role given and transformation in
* each list item. The encryption is done using transformation secret backend's batch
* operation.
* @param roleName must not be empty or {@literal null}.
* @param batchRequest a list of {@link Plaintext} which includes plaintext and an
* optional context.
* @return the encrypted result in the order of {@code batchRequest} plaintexts.
*/
List<VaultTransformEncodeResult> encode(String roleName, List<TransformPlaintext> batchRequest);
/**
* Decode the provided ciphertext using the named role.
* @param roleName must not be empty or {@literal null}.
* @param ciphertext must not be empty or {@literal null}.
* @return plain text.
*/
String decode(String roleName, String ciphertext);
/**
* Decode the provided ciphertext using the named role.
* @param roleName must not be empty or {@literal null}.
* @param ciphertext must not be {@literal null}.
* @return plain text.
*/
TransformPlaintext decode(String roleName, TransformCiphertext ciphertext);
/**
* Decode the provided ciphertext using the named role.
* @param roleName must not be empty or {@literal null}.
* @param ciphertext must not be empty or {@literal null}.
* @param transformContext must not be {@literal null}. Use
* {@link VaultTransformContext#empty()} if no request options provided.
* @return plain text.
*/
String decode(String roleName, String ciphertext, VaultTransformContext transformContext);
/**
* Decode the provided batch of ciphertext using the role given and transformation in
* each list item. The decryption is done using transformation secret backend's batch
* operation.
* @param roleName must not be empty or {@literal null}.
* @param batchRequest a list of {@link Ciphertext} which includes plaintext and an
* optional context.
* @return the decrypted result in the order of {@code batchRequest} ciphertexts.
*/
List<VaultTransformDecodeResult> decode(String roleName, List<TransformCiphertext> batchRequest);
}

View File

@@ -0,0 +1,297 @@
/*
* Copyright 2020 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.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.Base64Utils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.vault.VaultException;
import org.springframework.vault.support.*;
import java.util.*;
/**
* Default implementation of {@link VaultTransformOperations}.
*
* @author Lauren Voswinkel
* @since 2.3
*/
public class VaultTransformTemplate implements VaultTransformOperations {
private final VaultOperations vaultOperations;
private final String path;
/**
* Create a new {@link VaultTransformTemplate} given {@link VaultOperations} and the
* mount {@code path}.
* @param vaultOperations must not be {@literal null}.
* @param path must not be empty or {@literal null}.
*/
public VaultTransformTemplate(VaultOperations vaultOperations, String path) {
Assert.notNull(vaultOperations, "VaultOperations must not be null");
Assert.hasText(path, "Path must not be empty");
this.vaultOperations = vaultOperations;
this.path = path;
}
@Override
public String encode(String roleName, String plaintext) {
Assert.hasText(roleName, "Role name must not be empty");
Assert.notNull(plaintext, "Plaintext must not be null");
Map<String, String> request = new LinkedHashMap<>();
request.put("value", plaintext);
return (String) this.vaultOperations.write(String.format("%s/encode/%s", this.path, roleName), request)
.getRequiredData().get("encoded_value");
}
@Override
public TransformCiphertext encode(String roleName, TransformPlaintext plaintext) {
Assert.hasText(roleName, "Role name must not be empty");
Assert.notNull(plaintext, "Plaintext must not be null");
String ciphertext = encode(roleName, plaintext.getPlaintext(), plaintext.getContext());
return toCiphertext(ciphertext, plaintext.getContext());
}
@Override
public String encode(String roleName, byte[] plaintext, VaultTransformContext transformContext) {
Assert.hasText(roleName, "Role name must not be empty");
Assert.notNull(plaintext, "Plaintext must not be null");
Assert.notNull(transformContext, "VaultTransformContext must not be null");
Map<String, String> request = new LinkedHashMap<>();
String value = new String(plaintext);
request.put("value", value);
applyTransformOptions(transformContext, request);
return (String) this.vaultOperations.write(String.format("%s/encode/%s", this.path, roleName), request)
.getRequiredData().get("encoded_value");
}
@Override
public List<VaultTransformEncodeResult> encode(String roleName, List<TransformPlaintext> batchRequest) {
Assert.hasText(roleName, "Role name must not be empty");
Assert.notEmpty(batchRequest, "BatchRequest must not be null and must have at least one entry");
List<Map<String, String>> batch = new ArrayList<>(batchRequest.size());
for (TransformPlaintext request : batchRequest) {
Map<String, String> vaultRequest = new LinkedHashMap<>(2);
vaultRequest.put("value", request.asString());
if (request.getContext() != null) {
applyTransformOptions(request.getContext(), vaultRequest);
}
batch.add(vaultRequest);
}
VaultResponse vaultResponse = this.vaultOperations.write(String.format("%s/encode/%s", this.path, roleName),
Collections.singletonMap("batch_input", batch));
return toEncodedResults(vaultResponse, batchRequest);
}
@Override
public String decode(String roleName, String ciphertext) {
Assert.hasText(roleName, "Key name must not be empty");
Assert.hasText(ciphertext, "Ciphertext must not be empty");
Map<String, String> request = new LinkedHashMap<>();
request.put("value", ciphertext);
String plaintext = (String) this.vaultOperations
.write(String.format("%s/decode/%s", this.path, roleName), request).getRequiredData().get("decoded_value");
return new String(plaintext);
}
@Override
public TransformPlaintext decode(String roleName, TransformCiphertext ciphertext) {
Assert.hasText(roleName, "Role name must not be null");
Assert.notNull(ciphertext, "Ciphertext must not be null");
String plaintext = decode(roleName, ciphertext.getCiphertext(), ciphertext.getContext());
return TransformPlaintext.of(plaintext).with(ciphertext.getContext());
}
@Override
public String decode(String roleName, String ciphertext, VaultTransformContext transformContext) {
Assert.hasText(roleName, "Role name must not be empty");
Assert.hasText(ciphertext, "Ciphertext must not be empty");
Assert.notNull(transformContext, "VaultTransformContext must not be null");
Map<String, String> request = new LinkedHashMap<>();
request.put("value", ciphertext);
applyTransformOptions(transformContext, request);
String plaintext = (String) this.vaultOperations
.write(String.format("%s/decode/%s", this.path, roleName), request).getRequiredData().get("decoded_value");
return plaintext;
}
@Override
public List<VaultTransformDecodeResult> decode(String roleName, List<TransformCiphertext> batchRequest) {
Assert.hasText(roleName, "Role name must not be empty");
Assert.notEmpty(batchRequest, "BatchRequest must not be null and must have at least one entry");
List<Map<String, String>> batch = new ArrayList<>(batchRequest.size());
for (TransformCiphertext request : batchRequest) {
Map<String, String> vaultRequest = new LinkedHashMap<>(2);
vaultRequest.put("value", request.getCiphertext());
if (request.getContext() != null) {
applyTransformOptions(request.getContext(), vaultRequest);
}
batch.add(vaultRequest);
}
VaultResponse vaultResponse = this.vaultOperations.write(String.format("%s/decode/%s", this.path, roleName),
Collections.singletonMap("batch_input", batch));
return toDecryptionResults(vaultResponse, batchRequest);
}
private static void applyTransformOptions(VaultTransformContext context, Map<String, String> request) {
if (!ObjectUtils.isEmpty(context.getTransformation())) {
request.put("transformation", context.getTransformation());
}
if (!ObjectUtils.isEmpty(context.getTweak())) {
request.put("tweak", Base64Utils.encodeToString(context.getTweak()));
}
}
private static List<VaultTransformEncodeResult> toEncodedResults(VaultResponse vaultResponse,
List<TransformPlaintext> batchRequest) {
List<VaultTransformEncodeResult> result = new ArrayList<>(batchRequest.size());
List<Map<String, String>> batchData = getBatchData(vaultResponse);
for (int i = 0; i < batchRequest.size(); i++) {
VaultTransformEncodeResult encoded;
TransformPlaintext plaintext = batchRequest.get(i);
if (batchData.size() > i) {
Map<String, String> data = batchData.get(i);
if (StringUtils.hasText(data.get("error"))) {
encoded = new VaultTransformEncodeResult(new VaultException(data.get("error")));
}
else {
encoded = new VaultTransformEncodeResult(toCiphertext(data.get("encoded_value"), plaintext.getContext()));
}
}
else {
encoded = new VaultTransformEncodeResult(new VaultException("No result for plaintext #" + i));
}
result.add(encoded);
}
return result;
}
private static List<VaultTransformDecodeResult> toDecryptionResults(VaultResponse vaultResponse,
List<TransformCiphertext> batchRequest) {
List<VaultTransformDecodeResult> result = new ArrayList<>(batchRequest.size());
List<Map<String, String>> batchData = getBatchData(vaultResponse);
for (int i = 0; i < batchRequest.size(); i++) {
VaultTransformDecodeResult encrypted;
TransformCiphertext ciphertext = batchRequest.get(i);
if (batchData.size() > i) {
encrypted = getDecryptionResult(batchData.get(i), ciphertext);
}
else {
encrypted = new VaultTransformDecodeResult(new VaultException("No result for ciphertext #" + i));
}
result.add(encrypted);
}
return result;
}
private static VaultTransformDecodeResult getDecryptionResult(Map<String, String> data, TransformCiphertext ciphertext) {
if (StringUtils.hasText(data.get("error"))) {
return new VaultTransformDecodeResult(new VaultException(data.get("error")));
}
if (StringUtils.hasText(data.get("decoded_value"))) {
byte[] plaintext = data.get("decoded_value").getBytes();
return new VaultTransformDecodeResult(TransformPlaintext.of(plaintext).with(ciphertext.getContext()));
}
return new VaultTransformDecodeResult(TransformPlaintext.empty().with(ciphertext.getContext()));
}
private static TransformCiphertext toCiphertext(String ciphertext, @Nullable VaultTransformContext context) {
return context != null ? TransformCiphertext.of(ciphertext).with(context) : TransformCiphertext.of(ciphertext);
}
@SuppressWarnings("unchecked")
private static List<Map<String, String>> getBatchData(VaultResponse vaultResponse) {
return (List<Map<String, String>>) vaultResponse.getRequiredData().get("batch_results");
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append(getClass().getSimpleName());
sb.append(" [vaultOperations=").append(this.vaultOperations);
sb.append(", path='").append(this.path).append('\'');
sb.append(']');
return sb.toString();
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2020 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 org.springframework.util.Assert;
import java.util.Objects;
/**
* Value object representing ciphertext with an optional {@link VaultTransformContext}.
*
* @author Lauren Voswinkel
* @since 2.3
*/
public class TransformCiphertext {
private final String ciphertext;
private final VaultTransformContext context;
private TransformCiphertext(String ciphertext, VaultTransformContext context) {
this.ciphertext = ciphertext;
this.context = context;
}
/**
* Factory method to create {@link TransformCiphertext} from the given {@code ciphertext}.
* @param ciphertext the ciphertext to decrypt, must not be {@literal null} or empty.
* @return the {@link TransformCiphertext} for {@code ciphertext}.
*/
public static TransformCiphertext of(String ciphertext) {
Assert.hasText(ciphertext, "Ciphertext must not be null or empty");
return new TransformCiphertext(ciphertext, VaultTransformContext.empty());
}
public String getCiphertext() {
return this.ciphertext;
}
public VaultTransformContext getContext() {
return this.context;
}
/**
* Create a new {@link TransformCiphertext} object from this ciphertext associated with the
* given {@link VaultTransformContext}.
* @param context transit context, must not be {@literal null}.
* @return the new {@link TransformCiphertext} object.
*/
public TransformCiphertext with(VaultTransformContext context) {
Assert.notNull(context, "VaultTransitContext must not be null");
return new TransformCiphertext(getCiphertext(), context);
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof TransformCiphertext))
return false;
TransformCiphertext that = (TransformCiphertext) o;
return this.ciphertext.equals(that.ciphertext) && this.context.equals(that.context);
}
@Override
public int hashCode() {
return Objects.hash(this.ciphertext, this.context);
}
}

View File

@@ -0,0 +1,130 @@
/*
* Copyright 2020 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 org.springframework.util.Assert;
import java.util.Arrays;
import java.util.Objects;
/**
* Value object representing plaintext with an optional {@link VaultTransformContext}.
* Plaintext is represented binary safe as {@code byte[]}.
*
* @author Lauren Voswinkel
* @since 2.3
*/
public class TransformPlaintext {
private static final TransformPlaintext EMPTY = new TransformPlaintext(new byte[0], VaultTransformContext.empty());
private final byte[] plaintext;
private final VaultTransformContext context;
private TransformPlaintext(byte[] plaintext, VaultTransformContext context) {
this.plaintext = plaintext;
this.context = context;
}
/**
* Factory method to create an empty {@link TransformPlaintext}.
* @return the empty {@link TransformPlaintext} object.
* @since 1.1.2
*/
public static TransformPlaintext empty() {
return EMPTY;
}
/**
* Factory method to create {@link TransformPlaintext} from a byte sequence.
* @param plaintext the plaintext to encrypt, must not be {@literal null}.
* @return the {@link TransformPlaintext} for {@code plaintext}.
*/
public static TransformPlaintext of(byte[] plaintext) {
Assert.notNull(plaintext, "Plaintext must not be null");
if (plaintext.length == 0) {
return empty();
}
return new TransformPlaintext(plaintext, VaultTransformContext.empty());
}
/**
* Factory method to create {@link TransformPlaintext} using from {@link String}.
* {@link String} is encoded to {@code byte} using the default
* {@link java.nio.charset.Charset}.
* @param plaintext the plaintext to encrypt, must not be {@literal null}.
* @return the {@link TransformPlaintext} for {@code plaintext}.
*/
public static TransformPlaintext of(String plaintext) {
Assert.notNull(plaintext, "Plaintext must not be null");
if (plaintext.length() == 0) {
return empty();
}
return of(plaintext.getBytes());
}
public byte[] getPlaintext() {
return this.plaintext;
}
public VaultTransformContext getContext() {
return this.context;
}
/**
* Create a new {@link TransformPlaintext} object from this plaintext associated with the given
* {@link VaultTransformContext}.
* @param context transform context.
* @return the new {@link TransformPlaintext} object.
*/
public TransformPlaintext with(VaultTransformContext context) {
return new TransformPlaintext(getPlaintext(), context);
}
/**
* @return the plaintext as {@link String} decoded using the default
* {@link java.nio.charset.Charset}.
*/
public String asString() {
return new String(getPlaintext());
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof TransformPlaintext))
return false;
TransformPlaintext plaintext1 = (TransformPlaintext) o;
return Arrays.equals(this.plaintext, plaintext1.plaintext) && this.context.equals(plaintext1.context);
}
@Override
public int hashCode() {
int result = Objects.hash(this.context);
result = 31 * result + Arrays.hashCode(this.plaintext);
return result;
}
}

View File

@@ -0,0 +1,161 @@
/*
* Copyright 2020 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 org.springframework.util.Assert;
import java.util.Arrays;
/**
* Transform backend encode/decode context object.
*
* @author Lauren Voswinkel
* @since 2.3
*/
public class VaultTransformContext {
/**
* Empty (default) {@link VaultTransformContext} without a {@literal context} and
* {@literal nonce}.
*/
private static final VaultTransformContext EMPTY = new VaultTransformContext("", new byte[0]);
private final String transformation;
private final byte[] tweak;
VaultTransformContext(String transformation, byte[] tweak) {
this.transformation = transformation;
this.tweak = tweak;
}
/**
* @return a new {@link VaultTransformRequestBuilder}.
*/
public static VaultTransformRequestBuilder builder() {
return new VaultTransformRequestBuilder();
}
/**
* @return an empty {@link VaultTransformContext}.
*/
public static VaultTransformContext empty() {
return EMPTY;
}
/**
* Create a {@link VaultTransformContext} given {@code transformation} bytes.
* @param transformation name as a byte array, must not be {@literal null}.
* @return a {@link VaultTransformContext} for {@code transformation}.
*/
public static VaultTransformContext fromTransformation(String transformation) {
return builder().transformation(transformation).build();
}
/**
* Create a {@link VaultTransformContext} given {@code tweak} String.
* @param tweak bytes, must be 7 characters long, must not be {@literal null}.
* @return a {@link VaultTransformContext} for {@code tweak}.
*/
public static VaultTransformContext fromTweak(byte[] tweak) {
return builder().tweak(tweak).build();
}
/**
* @return the transformation name.
*/
public String getTransformation() {
return this.transformation;
}
/**
* @return the tweak
*/
public byte[] getTweak() {
return this.tweak;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof VaultTransformContext))
return false;
VaultTransformContext that = (VaultTransformContext) o;
return this.transformation.equals(that.transformation) && Arrays.equals(this.tweak, that.tweak);
}
@Override
public int hashCode() {
int result = this.transformation.hashCode();
result = 31 * result + Arrays.hashCode(this.tweak);
return result;
}
/**
* Builder for {@link VaultTransformContext}.
*/
public static class VaultTransformRequestBuilder {
private String transformation = "";
private byte[] tweak = new byte[0];
VaultTransformRequestBuilder() {
}
/**
* Configure a transformation to be used with the {@code transform} operation.
* @param transformation name, provided as a String.
* @return {@code this} {@link VaultTransformRequestBuilder}.
*/
public VaultTransformRequestBuilder transformation(String transformation) {
Assert.notNull(transformation, "Transformation must not be null");
this.transformation = transformation;
return this;
}
/**
* Configure the tweak value for a {@code transform} operation. Must be provided
* during decoding for all transformations with a tweak_source of "supplied" or
* "generated". Must be provided during encoding for all transformations with a
* tweak_source of "supplied".
* @param tweak value must be exactly 56 bits (7 bytes), value must be supplied
* during any subsequent decoding after an encoding. Failure to do so will result
* in a decode that does not return the original value.
* @return {@code this} {@link VaultTransformRequestBuilder}.
*/
public VaultTransformRequestBuilder tweak(byte[] tweak) {
Assert.notNull(tweak, "Tweak must not be null");
this.tweak = tweak;
return this;
}
/**
* Build a new {@link VaultTransformContext} instance.
* @return a new {@link VaultTransformContext}.
*/
public VaultTransformContext build() {
return new VaultTransformContext(this.transformation, this.tweak);
}
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2020 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 org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.vault.VaultException;
/**
* Holds the response from decryption operation and provides methods to access the result.
*
* @author Lauren Voswinkel
* @since 2.3
*/
public class VaultTransformDecodeResult extends AbstractResult<TransformPlaintext> {
private final @Nullable TransformPlaintext plaintext;
/**
* Create {@link VaultTransformDecodeResult} for a successfully decrypted {@link TransformPlaintext}
* .
* @param plaintext must not be {@literal null}.
*/
public VaultTransformDecodeResult(TransformPlaintext plaintext) {
Assert.notNull(plaintext, "Plaintext must not be null");
this.plaintext = plaintext;
}
/**
* Create {@link VaultTransformDecodeResult} for an error during decryption.
* @param exception must not be {@literal null}.
*/
public VaultTransformDecodeResult(VaultException exception) {
super(exception);
this.plaintext = null;
}
@Nullable
@Override
protected TransformPlaintext get0() {
return this.plaintext;
}
/**
* Return the result as {@link String} or throw a {@link VaultException} if the
* operation completed with an error. Use {@link #isSuccessful()} to verify the
* success status of this result without throwing an exception.
* @return the result value.
* @throws VaultException if the operation completed with an error.
*/
@Nullable
public String getAsString() {
TransformPlaintext plaintext = get();
return plaintext == null ? null : plaintext.asString();
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2020 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 org.springframework.lang.Nullable;
import org.springframework.vault.VaultException;
/**
* Holds the response from encryption operation and provides methods to access the result.
*
* @author Lauren Voswinkel
* @since 2.3
*/
public class VaultTransformEncodeResult extends AbstractResult<TransformCiphertext> {
private final @Nullable TransformCiphertext cipherText;
/**
* Create {@link VaultTransformEncodeResult} for a successfully encrypted
* {@link TransformCiphertext} .
* @param cipherText must not be {@literal null}.
*/
public VaultTransformEncodeResult(TransformCiphertext cipherText) {
this.cipherText = cipherText;
}
/**
* Create {@link VaultTransformEncodeResult} for an error during encryption.
* @param exception must not be {@literal null}.
*/
public VaultTransformEncodeResult(VaultException exception) {
super(exception);
this.cipherText = null;
}
@Nullable
@Override
protected TransformCiphertext get0() {
return this.cipherText;
}
/**
* Return the result as {@link String} or throw a {@link VaultException} if the
* operation completed with an error. Use {@link #isSuccessful()} to verify the
* success status of this result without throwing an exception.
* @return the result value.
* @throws VaultException if the operation completed with an error.
*/
@Nullable
public String getAsString() {
TransformCiphertext ciphertext = get();
return ciphertext == null ? null : ciphertext.getCiphertext();
}
}

View File

@@ -67,7 +67,7 @@ class VaultNamespaceSecretIntegrationTests extends IntegrationTestSupport {
RestTemplateBuilder devRestTemplate;
RestTemplateBuilder maketingRestTemplate;
RestTemplateBuilder marketingRestTemplate;
WebClientBuilder marketingWebClientBuilder = WebClientBuilder.builder()
.httpConnector(ClientHttpConnectorFactory.create(new ClientOptions(), Settings.createSslConfiguration()))
@@ -97,7 +97,7 @@ class VaultNamespaceSecretIntegrationTests extends IntegrationTestSupport {
.endpoint(TestRestTemplateFactory.TEST_VAULT_ENDPOINT).customizers(restTemplate -> restTemplate
.getInterceptors().add(VaultClients.createNamespaceInterceptor("dev")));
this.maketingRestTemplate = RestTemplateBuilder.builder()
this.marketingRestTemplate = RestTemplateBuilder.builder()
.requestFactory(
ClientHttpRequestFactoryFactory.create(new ClientOptions(), Settings.createSslConfiguration()))
.endpoint(TestRestTemplateFactory.TEST_VAULT_ENDPOINT)
@@ -111,7 +111,7 @@ class VaultNamespaceSecretIntegrationTests extends IntegrationTestSupport {
this.devToken = dev.opsForToken().create(VaultTokenRequest.builder().withPolicy("relaxed").build()).getToken()
.getToken();
VaultTemplate marketing = new VaultTemplate(this.maketingRestTemplate,
VaultTemplate marketing = new VaultTemplate(this.marketingRestTemplate,
new SimpleSessionManager(new TokenAuthentication(Settings.token())));
mountKv(marketing, "marketing-secrets");
@@ -137,7 +137,7 @@ class VaultNamespaceSecretIntegrationTests extends IntegrationTestSupport {
VaultTemplate dev = new VaultTemplate(this.devRestTemplate,
new SimpleSessionManager(new TokenAuthentication(this.devToken)));
VaultTemplate marketing = new VaultTemplate(this.maketingRestTemplate,
VaultTemplate marketing = new VaultTemplate(this.marketingRestTemplate,
new SimpleSessionManager(new TokenAuthentication(this.marketingToken)));
dev.write("dev-secrets/my-secret", Collections.singletonMap("key", "dev"));
@@ -163,7 +163,7 @@ class VaultNamespaceSecretIntegrationTests extends IntegrationTestSupport {
@Test
void reactiveNamespaceSecretsAreIsolated() {
VaultTemplate marketing = new VaultTemplate(this.maketingRestTemplate,
VaultTemplate marketing = new VaultTemplate(this.marketingRestTemplate,
new SimpleSessionManager(new TokenAuthentication(this.marketingToken)));
ReactiveVaultTemplate reactiveMarketing = new ReactiveVaultTemplate(this.marketingWebClientBuilder,
@@ -181,7 +181,7 @@ class VaultNamespaceSecretIntegrationTests extends IntegrationTestSupport {
@Test
void shouldReportInitialized() {
VaultTemplate marketing = new VaultTemplate(this.maketingRestTemplate,
VaultTemplate marketing = new VaultTemplate(this.marketingRestTemplate,
new SimpleSessionManager(new TokenAuthentication(this.marketingToken)));
assertThat(marketing.opsForSys().isInitialized()).isTrue();
@@ -190,7 +190,7 @@ class VaultNamespaceSecretIntegrationTests extends IntegrationTestSupport {
@Test
void shouldReportHealth() {
VaultTemplate marketing = new VaultTemplate(this.maketingRestTemplate,
VaultTemplate marketing = new VaultTemplate(this.marketingRestTemplate,
new SimpleSessionManager(new TokenAuthentication(this.marketingToken)));
assertThat(marketing.opsForSys().health().isInitialized()).isTrue();

View File

@@ -0,0 +1,102 @@
/*
* Copyright 2020 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.Assumptions;
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.util.Base64Utils;
import org.springframework.vault.support.VaultMount;
import org.springframework.vault.support.VaultResponse;
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.assertThat;
/**
* Integration tests for {@link VaultTemplate} using the {@code transform} backend.
*
* @author Lauren Voswinkel
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = VaultIntegrationTestConfiguration.class)
@RequiresVaultVersion("1.4.0")
class VaultTemplateTransformIntegrationTests extends IntegrationTestSupport {
@Autowired
VaultOperations vaultOperations;
Version vaultVersion;
@BeforeEach
void before() {
Assumptions.assumeTrue(prepare().getVersion().isEnterprise(), "Transform Secrets Engine requires enterprise version");
VaultSysOperations adminOperations = this.vaultOperations.opsForSys();
this.vaultVersion = prepare().getVersion();
if (!adminOperations.getMounts().containsKey("transform/")) {
adminOperations.mount("transform", VaultMount.create("transform"));
}
// Write a transformation/role
this.vaultOperations.write("transform/transformation/myssn", "{\"type\": \"fpe\", \"template\": \"builtin/socialsecuritynumber\", \"allowed_roles\": [\"myrole\"]}");
this.vaultOperations.write("transform/role/myrole", "{\"transformations\": [\"myssn\"]}");
}
@AfterEach
void tearDown() {
this.vaultOperations.delete("transform/role/myrole");
this.vaultOperations.delete("transform/transformation/myssn");
}
@Test
void shouldEncode() {
VaultResponse response = this.vaultOperations.write("transform/encode/myrole",
String.format("{\"value\": \"123-45-6789\", \"tweak\": \"%s\"}",
Base64Utils.encodeToString("somenum".getBytes())));
assertThat((String) response.getRequiredData().get("encoded_value")).isNotEmpty();
}
@Test
void shouldEncodeAndDecode() {
String value = "123-45-6789";
VaultResponse response = this.vaultOperations.write("transform/encode/myrole",
String.format("{\"value\": \"%s\", \"tweak\": \"%s\"}",
value,
Base64Utils.encodeToString("somenum".getBytes())));
String encoded = (String) response.getRequiredData().get("encoded_value");
VaultResponse decoded = this.vaultOperations.write("transform/decode/myrole",
String.format("{\"value\": \"%s\", \"tweak\": \"%s\"}",
encoded,
Base64Utils.encodeToString("somenum".getBytes())));
assertThat((String) decoded.getRequiredData().get("decoded_value"))
.isEqualTo(value);
}
}

View File

@@ -0,0 +1,244 @@
/*
* Copyright 2020 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.Assumptions;
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.TransformPlaintext;
import org.springframework.vault.support.TransformCiphertext;
import org.springframework.vault.support.VaultMount;
import org.springframework.vault.support.VaultTransformContext;
import org.springframework.vault.support.VaultTransformDecodeResult;
import org.springframework.vault.support.VaultTransformEncodeResult;
import org.springframework.vault.util.IntegrationTestSupport;
import org.springframework.vault.util.RequiresVaultVersion;
import org.springframework.vault.util.Version;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
/**
* Integration tests for {@link VaultTemplate} using the {@code transform} backend.
*
* @author Lauren Voswinkel
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = VaultIntegrationTestConfiguration.class)
@RequiresVaultVersion("1.4.0")
class VaultTransformTemplateIntegrationTests extends IntegrationTestSupport {
@Autowired
VaultOperations vaultOperations;
VaultTransformOperations transformOperations;
Version vaultVersion;
@BeforeEach
void before() {
Assumptions.assumeTrue(prepare().getVersion().isEnterprise(), "Transform Secrets Engine requires enterprise version");
VaultSysOperations adminOperations = this.vaultOperations.opsForSys();
this.transformOperations = this.vaultOperations.opsForTransform();
this.vaultVersion = prepare().getVersion();
if (!adminOperations.getMounts().containsKey("transform/")) {
adminOperations.mount("transform", VaultMount.create("transform"));
}
// Write a transformation/role
this.vaultOperations.write("transform/transformation/myssn", "{" +
"\"type\": \"fpe\", " +
"\"template\": \"builtin/socialsecuritynumber\", " +
"\"allowed_roles\": [\"myrole\"]}"
);
this.vaultOperations.write("transform/role/myrole", "{\"transformations\": [\"myssn\", \"internalssn\"]}");
this.vaultOperations.write("transform/transformation/internalssn", "{" +
"\"type\": \"fpe\", " +
"\"tweak_source\": \"internal\", " +
"\"template\": \"builtin/socialsecuritynumber\", " +
"\"allowed_roles\": [\"myrole\", \"internalrole\"]}"
);
this.vaultOperations.write("transform/role/internalrole", "{\"transformations\": [\"internalssn\"]}");
}
@AfterEach
void tearDown() {
this.vaultOperations.delete("transform/role/myrole");
this.vaultOperations.delete("transform/transformation/myssn");
}
@Test
void encodeCreatesCiphertextWithTransformationAndTweak() {
VaultTransformContext transformRequest = VaultTransformContext.builder().transformation("myssn")
.tweak("somenum".getBytes())
.build();
String response = this.transformOperations.encode("myrole", "123-45-6789".getBytes(), transformRequest);
assertThat(response).matches("[0-9]{3}-[0-9]{2}-[0-9]{4}");
}
@Test
void encodeThrowsVaultExceptionWithSuppliedTransformationAndNoTweak() {
VaultTransformContext transformRequest = VaultTransformContext.builder().transformation("myssn").build();
Exception exception = assertThrows(VaultException.class, () -> {
this.transformOperations.encode("myrole", "123-45-6789".getBytes(), transformRequest);
});
String expectedMessage = "incorrect tweak size provided";
String actualMessage = exception.getMessage();
assertThat(actualMessage).contains(expectedMessage);
}
@Test
void encodeCreatesCiphertextWithInternalTransformationAndNoTweak() {
VaultTransformContext transformRequest = VaultTransformContext.builder().transformation("internalssn").build();
String response = this.transformOperations.encode("myrole", "123-45-6789".getBytes(), transformRequest);
assertThat(response).matches("[0-9]{3}-[0-9]{2}-[0-9]{4}");
}
@Test
void encodeAndDecodeYieldsStartingResultWithSameTweakValueProvided() {
VaultTransformContext transformRequest = VaultTransformContext.builder().transformation("myssn").tweak("somenum".getBytes()).build();
String targetValue = "123-45-6789";
String response = this.transformOperations.encode("myrole", targetValue.getBytes(), transformRequest);
assertThat(response).matches("[0-9]{3}-[0-9]{2}-[0-9]{4}");
assertThat(response).isNotEqualTo(targetValue);
String decodeResponse = this.transformOperations.decode("myrole", response, transformRequest);
assertThat(decodeResponse).isEqualTo(targetValue);
}
@Test
void encodeAndDecodeDoesNotYieldStartingResultWithDifferentTweakValueProvided() {
VaultTransformContext transformRequest = VaultTransformContext.builder().transformation("myssn").tweak("somenum".getBytes()).build();
String targetValue = "123-45-6789";
String response = this.transformOperations.encode("myrole", targetValue.getBytes(), transformRequest);
assertThat(response).matches("[0-9]{3}-[0-9]{2}-[0-9]{4}");
assertThat(response).isNotEqualTo(targetValue);
VaultTransformContext decodeRequest = VaultTransformContext.builder().transformation("myssn").tweak("numsome".getBytes()).build();
String decodeResponse = this.transformOperations.decode("myrole", response, decodeRequest);
assertThat(decodeResponse).isNotEqualTo(targetValue);
}
@Test
void encodeAndDecodeWithoutContextWorksForInternalTweakSource() {
String targetValue = "123-45-6789";
String response = this.transformOperations.encode("internalrole", targetValue);
assertThat(response).matches("[0-9]{3}-[0-9]{2}-[0-9]{4}");
assertThat(response).isNotEqualTo(targetValue);
String decodeResponse = this.transformOperations.decode("internalrole", response);
assertThat(decodeResponse).isEqualTo(targetValue);
}
@Test
void batchEncodeAndDecodeYieldsStartingResults() {
VaultTransformContext transformRequest = VaultTransformContext.builder().transformation("myssn").tweak("somenum".getBytes()).build();
List<String> ssns = Arrays.asList(
"123-01-4567",
"123-02-4567",
"123-03-4567",
"123-04-4567",
"123-05-4567"
);
List<VaultTransformEncodeResult> encoded = this.transformOperations.encode("myrole",
Arrays.asList(
TransformPlaintext.of(ssns.get(0)).with(transformRequest),
TransformPlaintext.of(ssns.get(1)).with(transformRequest),
TransformPlaintext.of(ssns.get(2)).with(transformRequest),
TransformPlaintext.of(ssns.get(3)).with(transformRequest),
TransformPlaintext.of(ssns.get(4)).with(transformRequest)
)
);
List<VaultTransformDecodeResult> decoded = this.transformOperations.decode("myrole",
Arrays.asList(
TransformCiphertext.of(encoded.get(0).getAsString()).with(transformRequest),
TransformCiphertext.of(encoded.get(1).getAsString()).with(transformRequest),
TransformCiphertext.of(encoded.get(2).getAsString()).with(transformRequest),
TransformCiphertext.of(encoded.get(3).getAsString()).with(transformRequest),
TransformCiphertext.of(encoded.get(4).getAsString()).with(transformRequest)
)
);
for (int i = 0; i < decoded.size(); i++) {
assertThat(decoded.get(i).getAsString()).isEqualTo(ssns.get(i));
}
}
@Test
void batchEncodeAndDecodeYieldsStartingResultsForInternalWithNoContext() {
List<String> ssns = Arrays.asList(
"123-01-4567",
"123-02-4567",
"123-03-4567",
"123-04-4567",
"123-05-4567"
);
List<VaultTransformEncodeResult> encoded = this.transformOperations.encode("internalrole",
Arrays.asList(
TransformPlaintext.of(ssns.get(0)),
TransformPlaintext.of(ssns.get(1)),
TransformPlaintext.of(ssns.get(2)),
TransformPlaintext.of(ssns.get(3)),
TransformPlaintext.of(ssns.get(4))
)
);
List<VaultTransformDecodeResult> decoded = this.transformOperations.decode("internalrole",
Arrays.asList(
TransformCiphertext.of(encoded.get(0).getAsString()),
TransformCiphertext.of(encoded.get(1).getAsString()),
TransformCiphertext.of(encoded.get(2).getAsString()),
TransformCiphertext.of(encoded.get(3).getAsString()),
TransformCiphertext.of(encoded.get(4).getAsString())
)
);
for (int i = 0; i < decoded.size(); i++) {
assertThat(decoded.get(i).getAsString()).isEqualTo(ssns.get(i));
}
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2020 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 org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Unit tests for {@link VaultTransitContext}.
*
* @author Lauren Voswinkel
*/
class VaultTransformContextUnitTests {
@Test
void rejectsNullTransformation() {
assertThatIllegalArgumentException().isThrownBy(() -> VaultTransformContext.fromTransformation(null));
}
@Test
void createsFromTransformation() {
String transformName = "some_transformation";
VaultTransformContext context = VaultTransformContext.fromTransformation(transformName);
assertThat(context.getTransformation()).isEqualTo(transformName);
assertThat(context.getTweak()).isEmpty();
}
@Test
void rejectsNullTweak() {
assertThatIllegalArgumentException().isThrownBy(() -> VaultTransformContext.fromTweak(null));
}
@Test
void createsFromTweak() {
byte[] bytes = new byte[] { 1 };
VaultTransformContext context = VaultTransformContext.fromTweak(bytes);
assertThat(context.getTweak()).isEqualTo(bytes);
assertThat(context.getTransformation()).isEmpty();
}
}