diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultOperations.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultOperations.java index 7995067c..eebe7440 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultOperations.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultOperations.java @@ -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. */ diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTemplate.java index fd993289..d1775733 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTemplate.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTemplate.java @@ -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"); diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransformOperations.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransformOperations.java new file mode 100644 index 00000000..79f240af --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransformOperations.java @@ -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 Transform + * Secrets Engine + * @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 encode(String roleName, List 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 decode(String roleName, List batchRequest); +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransformTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransformTemplate.java new file mode 100644 index 00000000..feab723c --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransformTemplate.java @@ -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 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 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 encode(String roleName, List 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> batch = new ArrayList<>(batchRequest.size()); + + for (TransformPlaintext request : batchRequest) { + + Map 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 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 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 decode(String roleName, List 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> batch = new ArrayList<>(batchRequest.size()); + + for (TransformCiphertext request : batchRequest) { + + Map 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 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 toEncodedResults(VaultResponse vaultResponse, + List batchRequest) { + + List result = new ArrayList<>(batchRequest.size()); + List> batchData = getBatchData(vaultResponse); + + for (int i = 0; i < batchRequest.size(); i++) { + + VaultTransformEncodeResult encoded; + TransformPlaintext plaintext = batchRequest.get(i); + if (batchData.size() > i) { + + Map 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 toDecryptionResults(VaultResponse vaultResponse, + List batchRequest) { + + List result = new ArrayList<>(batchRequest.size()); + List> 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 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> getBatchData(VaultResponse vaultResponse) { + return (List>) 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(); + } + +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/TransformCiphertext.java b/spring-vault-core/src/main/java/org/springframework/vault/support/TransformCiphertext.java new file mode 100644 index 00000000..bfeeb261 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/TransformCiphertext.java @@ -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); + } + +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/TransformPlaintext.java b/spring-vault-core/src/main/java/org/springframework/vault/support/TransformPlaintext.java new file mode 100644 index 00000000..4796c37d --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/TransformPlaintext.java @@ -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; + } + +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultTransformContext.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultTransformContext.java new file mode 100644 index 00000000..ca9264e9 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultTransformContext.java @@ -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); + } + + } + +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultTransformDecodeResult.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultTransformDecodeResult.java new file mode 100644 index 00000000..7d60eb8d --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultTransformDecodeResult.java @@ -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 { + + 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(); + } + +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultTransformEncodeResult.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultTransformEncodeResult.java new file mode 100644 index 00000000..0a71298f --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultTransformEncodeResult.java @@ -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 { + + 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(); + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultNamespaceSecretIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultNamespaceSecretIntegrationTests.java index 78f57d12..ea553c1b 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultNamespaceSecretIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultNamespaceSecretIntegrationTests.java @@ -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(); diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTemplateTransformIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTemplateTransformIntegrationTests.java new file mode 100644 index 00000000..192f308d --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTemplateTransformIntegrationTests.java @@ -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); + } + +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTransformTemplateIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTransformTemplateIntegrationTests.java new file mode 100644 index 00000000..137bacd7 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTransformTemplateIntegrationTests.java @@ -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 ssns = Arrays.asList( + "123-01-4567", + "123-02-4567", + "123-03-4567", + "123-04-4567", + "123-05-4567" + ); + + List 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 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 ssns = Arrays.asList( + "123-01-4567", + "123-02-4567", + "123-03-4567", + "123-04-4567", + "123-05-4567" + ); + + List 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 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)); + } + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/support/VaultTransformContextUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/support/VaultTransformContextUnitTests.java new file mode 100644 index 00000000..0ce3df82 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/support/VaultTransformContextUnitTests.java @@ -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(); + } + +}