diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransitOperations.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransitOperations.java index 89a3b196..ba9d3644 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransitOperations.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransitOperations.java @@ -18,11 +18,11 @@ package org.springframework.vault.core; import java.util.List; import org.springframework.lang.Nullable; +import org.springframework.vault.support.Ciphertext; +import org.springframework.vault.support.Plaintext; import org.springframework.vault.support.RawTransitKey; import org.springframework.vault.support.TransitKeyType; -import org.springframework.vault.support.VaultDecryptionPayload; import org.springframework.vault.support.VaultDecryptionResult; -import org.springframework.vault.support.VaultEncryptionPayload; import org.springframework.vault.support.VaultEncryptionResult; import org.springframework.vault.support.VaultTransitContext; import org.springframework.vault.support.VaultTransitKey; @@ -120,6 +120,16 @@ public interface VaultTransitOperations { */ String encrypt(String keyName, String plaintext); + /** + * Encrypts the provided plaintext using the named key. + * + * @param keyName must not be empty or {@literal null}. + * @param plaintext must not be {@literal null}. + * @since 1.1 + * @return cipher text. + */ + Ciphertext encrypt(String keyName, Plaintext plaintext); + /** * Encrypts the provided plaintext using the named key. * @@ -132,17 +142,16 @@ public interface VaultTransitOperations { String encrypt(String keyName, byte[] plaintext, VaultTransitContext transitRequest); /** - * Encrypts the provided list of plaintext using the named key and context. - * The encryption is done using transit backend's batch operation. - * - * works with Vault 0.6.5 and later. + * Encrypts the provided batch of plaintext using the named key and context. The + * encryption is done using transit backend's batch operation. * * @param keyName must not be empty or {@literal null}. - * @param batchRequest a list of VaultEncryptionPayload which includes plaintext and optional context - * @return list of cipher text in the same order as in plaintexts. - * throws VaultException in case of not matching context found. + * @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. + * @since 1.1 */ - List encrypt(String keyName, List batchRequest); + List encrypt(String keyName, List batchRequest); /** * Decrypts the provided plaintext using the named key. @@ -153,6 +162,16 @@ public interface VaultTransitOperations { */ String decrypt(String keyName, String ciphertext); + /** + * Decrypts the provided plaintext using the named key. + * + * @param keyName must not be empty or {@literal null}. + * @param ciphertext must not be {@literal null}. + * @return plain text. + * @since 1.1 + */ + Plaintext decrypt(String keyName, Ciphertext ciphertext); + /** * Decrypts the provided plaintext using the named key. * @@ -166,17 +185,17 @@ public interface VaultTransitOperations { byte[] decrypt(String keyName, String ciphertext, VaultTransitContext transitContext); /** - * Decrypts the provided list of ciphertext using the named key and context. - * The decryption is done using transit backend's batch operation. + * Decrypts the provided barch of ciphertext using the named key and context. The* + * decryption is done using transit backend's batch operation. * - * works with Vault 0.6.5 and later. * * @param keyName must not be empty or {@literal null}. - * @param batchRequest a list of VaultDecryptionPayload which includes plaintext and optional context - * @return list of plain text in the same order as in ciphertexts. - * throws VaultException in case of not matching context found. + * @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. + * @since 1.1 */ - List<VaultDecryptionResult> decrypt(String keyName, List<VaultDecryptionPayload> batchRequest); + List<VaultDecryptionResult> decrypt(String keyName, List<Ciphertext> batchRequest); /** * Rewrap the provided ciphertext using the latest version of the named key. Because diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransitTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransitTemplate.java index 74c0a578..a6cbc6a0 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransitTemplate.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransitTemplate.java @@ -28,12 +28,13 @@ 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.Ciphertext; +import org.springframework.vault.support.Plaintext; import org.springframework.vault.support.RawTransitKey; import org.springframework.vault.support.TransitKeyType; -import org.springframework.vault.support.VaultDecryptionPayload; import org.springframework.vault.support.VaultDecryptionResult; -import org.springframework.vault.support.VaultEncryptionDecryptionResultHelper; -import org.springframework.vault.support.VaultEncryptionPayload; import org.springframework.vault.support.VaultEncryptionResult; import org.springframework.vault.support.VaultResponse; import org.springframework.vault.support.VaultResponseSupport; @@ -42,10 +43,6 @@ import org.springframework.vault.support.VaultTransitKey; import org.springframework.vault.support.VaultTransitKeyConfiguration; import org.springframework.vault.support.VaultTransitKeyCreationRequest; -import com.fasterxml.jackson.annotation.JsonProperty; - -import lombok.Data; - /** * Default implementation of {@link VaultTransitOperations}. * @@ -157,7 +154,7 @@ public class VaultTransitTemplate implements VaultTransitOperations { public String encrypt(String keyName, String plaintext) { Assert.hasText(keyName, "KeyName must not be empty"); - Assert.notNull(plaintext, "Plain text must not be null"); + Assert.notNull(plaintext, "Plaintext must not be null"); Map<String, String> request = new LinkedHashMap<>(); @@ -168,12 +165,23 @@ public class VaultTransitTemplate implements VaultTransitOperations { .getRequiredData().get("ciphertext"); } + @Override + public Ciphertext encrypt(String keyName, Plaintext plaintext) { + + Assert.notNull(plaintext, "Plaintext must not be null"); + + String ciphertext = encrypt(keyName, plaintext.getPlaintext(), + plaintext.getContext()); + + return toCiphertext(ciphertext, plaintext.getContext()); + } + @Override public String encrypt(String keyName, byte[] plaintext, VaultTransitContext transitContext) { Assert.hasText(keyName, "KeyName must not be empty"); - Assert.notNull(plaintext, "Plain text must not be null"); + Assert.notNull(plaintext, "Plaintext must not be null"); Assert.notNull(transitContext, "VaultTransitContext must not be null"); Map<String, String> request = new LinkedHashMap<>(); @@ -188,20 +196,22 @@ public class VaultTransitTemplate implements VaultTransitOperations { } @Override - public List<VaultEncryptionResult> encrypt(String keyName, List<VaultEncryptionPayload> batchRequest) { + public List<VaultEncryptionResult> encrypt(String keyName, + List<Plaintext> batchRequest) { Assert.hasText(keyName, "KeyName must not be empty"); - Assert.notEmpty(batchRequest, "batchRequest must not be null and should have at least one entry"); + Assert.notEmpty(batchRequest, + "BatchRequest must not be null and must have at least one entry"); - List<Map<String, String>> batch = new ArrayList<Map<String, String>>(); + List<Map<String, String>> batch = new ArrayList<Map<String, String>>( + batchRequest.size()); - for (VaultEncryptionPayload request : batchRequest) { + for (Plaintext request : batchRequest) { - Assert.notNull(request.getPlaintext(), "Plain text must not be null"); + Map<String, String> vaultRequest = new LinkedHashMap<String, String>(2); - Map<String, String> vaultRequest = new LinkedHashMap<String, String>(); - - vaultRequest.put("plaintext", Base64Utils.encodeToString(request.getPlaintext())); + vaultRequest.put("plaintext", + Base64Utils.encodeToString(request.getPlaintext())); if (request.getContext() != null) { applyTransitOptions(request.getContext(), vaultRequest); @@ -210,12 +220,11 @@ public class VaultTransitTemplate implements VaultTransitOperations { batch.add(vaultRequest); } - Map<String, List<Map<String, String>>> request = new LinkedHashMap<String, List<Map<String, String>>>(); - request.put("batch_input", batch); + VaultResponse vaultResponse = vaultOperations.write( + String.format("%s/encrypt/%s", path, keyName), + Collections.singletonMap("batch_input", batch)); - VaultResponse vaultResponse = vaultOperations.write(String.format("%s/encrypt/%s", path, keyName), request); - - return VaultEncryptionDecryptionResultHelper.fetchEncryptionResult(vaultResponse); + return toEncryptionResults(vaultResponse, batchRequest); } @Override @@ -235,6 +244,17 @@ public class VaultTransitTemplate implements VaultTransitOperations { return new String(Base64Utils.decodeFromString(plaintext)); } + @Override + public Plaintext decrypt(String keyName, Ciphertext ciphertext) { + + Assert.hasText(keyName, "Ciphertext must not be null"); + + byte[] plaintext = decrypt(keyName, ciphertext.getCiphertext(), + ciphertext.getContext()); + + return toPlaintext(plaintext, ciphertext.getContext()); + } + @Override public byte[] decrypt(String keyName, String ciphertext, VaultTransitContext transitContext) { @@ -257,18 +277,19 @@ public class VaultTransitTemplate implements VaultTransitOperations { } @Override - public List<VaultDecryptionResult> decrypt(String keyName, List<VaultDecryptionPayload> batchRequest) { + public List<VaultDecryptionResult> decrypt(String keyName, + List<Ciphertext> batchRequest) { Assert.hasText(keyName, "KeyName must not be empty"); - Assert.notEmpty(batchRequest, "batchRequest must not be null and should have at least one entry"); + Assert.notEmpty(batchRequest, + "BatchRequest must not be null and must have at least one entry"); - List<Map<String, String>> batch = new ArrayList<Map<String, String>>(); + List<Map<String, String>> batch = new ArrayList<Map<String, String>>( + batchRequest.size()); - for (VaultDecryptionPayload request : batchRequest) { + for (Ciphertext request : batchRequest) { - Assert.notNull(request.getCiphertext(), "Cipher text must not be null"); - - Map<String, String> vaultRequest = new LinkedHashMap<String, String>(); + Map<String, String> vaultRequest = new LinkedHashMap<String, String>(2); vaultRequest.put("ciphertext", request.getCiphertext()); @@ -279,19 +300,18 @@ public class VaultTransitTemplate implements VaultTransitOperations { batch.add(vaultRequest); } - Map<String, List<Map<String, String>>> request = new LinkedHashMap<String, List<Map<String, String>>>(); - request.put("batch_input", batch); + VaultResponse vaultResponse = vaultOperations.write( + String.format("%s/decrypt/%s", path, keyName), + Collections.singletonMap("batch_input", batch)); - VaultResponse vaultResponse = vaultOperations.write(String.format("%s/decrypt/%s", path, keyName), request); - - return VaultEncryptionDecryptionResultHelper.fetchDecryptionResult(vaultResponse); + return toDecryptionResults(vaultResponse, batchRequest); } @Override public String rewrap(String keyName, String ciphertext) { Assert.hasText(keyName, "KeyName must not be empty"); - Assert.hasText(ciphertext, "Cipher text must not be empty"); + Assert.hasText(ciphertext, "Ciphertext must not be empty"); Map<String, String> request = new LinkedHashMap<>(); request.put("ciphertext", ciphertext); @@ -306,7 +326,7 @@ public class VaultTransitTemplate implements VaultTransitOperations { VaultTransitContext transitContext) { Assert.hasText(keyName, "KeyName must not be empty"); - Assert.hasText(ciphertext, "Cipher text must not be empty"); + Assert.hasText(ciphertext, "Ciphertext must not be empty"); Assert.notNull(transitContext, "VaultTransitContext must not be null"); Map<String, String> request = new LinkedHashMap<>(); @@ -332,6 +352,91 @@ public class VaultTransitTemplate implements VaultTransitOperations { } } + private static List<VaultEncryptionResult> toEncryptionResults( + VaultResponse vaultResponse, List<Plaintext> batchRequest) { + + List<VaultEncryptionResult> result = new ArrayList<VaultEncryptionResult>( + batchRequest.size()); + List<Map<String, String>> batchData = getBatchData(vaultResponse); + + for (int i = 0; i < batchRequest.size(); i++) { + + VaultEncryptionResult encrypted; + Plaintext plaintext = batchRequest.get(i); + if (batchData.size() > i) { + + Map<String, String> data = batchData.get(i); + if (StringUtils.hasText(data.get("error"))) { + encrypted = new VaultEncryptionResult(new VaultException( + data.get("error"))); + } + else { + encrypted = new VaultEncryptionResult(toCiphertext( + data.get("ciphertext"), plaintext.getContext())); + } + } + else { + encrypted = new VaultEncryptionResult(new VaultException( + "No result for plaintext #" + i)); + } + + result.add(encrypted); + } + + return result; + } + + private static List<VaultDecryptionResult> toDecryptionResults( + VaultResponse vaultResponse, List<Ciphertext> batchRequest) { + + List<VaultDecryptionResult> result = new ArrayList<VaultDecryptionResult>( + batchRequest.size()); + List<Map<String, String>> batchData = getBatchData(vaultResponse); + + for (int i = 0; i < batchRequest.size(); i++) { + + VaultDecryptionResult encrypted; + Ciphertext ciphertext = batchRequest.get(i); + if (batchData.size() > i) { + + Map<String, String> data = batchData.get(i); + if (StringUtils.hasText(data.get("error"))) { + encrypted = new VaultDecryptionResult(new VaultException( + data.get("error"))); + } + else { + encrypted = new VaultDecryptionResult(toPlaintext( + Base64Utils.decodeFromString(data.get("plaintext")), + ciphertext.getContext())); + } + } + else { + encrypted = new VaultDecryptionResult(new VaultException( + "No result for ciphertext #" + i)); + } + + result.add(encrypted); + } + + return result; + } + + private static Ciphertext toCiphertext(String ciphertext, VaultTransitContext context) { + return context != null ? Ciphertext.of(ciphertext).with(context) : Ciphertext + .of(ciphertext); + } + + private static Plaintext toPlaintext(byte[] plaintext, VaultTransitContext context) { + return context != null ? Plaintext.of(plaintext).with(context) : Plaintext + .of(plaintext); + } + + @SuppressWarnings("unchecked") + private static List<Map<String, String>> getBatchData(VaultResponse vaultResponse) { + return (List<Map<String, String>>) vaultResponse.getRequiredData().get( + "batch_results"); + } + @Data static class VaultTransitKeyImpl implements VaultTransitKey { diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/AbstractResult.java b/spring-vault-core/src/main/java/org/springframework/vault/support/AbstractResult.java new file mode 100644 index 00000000..2482f312 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/AbstractResult.java @@ -0,0 +1,97 @@ +/* + * Copyright 2017 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 + * + * http://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; + +/** + * Supporting class for computation results allowing introspection of the result value. + * Accessing the result with {@link #get()} returns either the result or throws a + * {@link VaultException} if the execution completed with an error. + * + * @author Mark Paluch + * @since 1.1 + */ +public abstract class AbstractResult<V> { + + private final @Nullable VaultException exception; + + /** + * Create a {@link AbstractResult} completed without an {@link VaultException}. + */ + protected AbstractResult() { + exception = null; + } + + /** + * Create a {@link AbstractResult} completed with an {@link VaultException}. + * + * @param exception must not be {@literal null}. + */ + protected AbstractResult(VaultException exception) { + + Assert.notNull(exception, "VaultException must not be null"); + + this.exception = exception; + } + + /** + * Returns {@link true} if and only if the batch operation was completed successfully. + * Use {@link #getCause()} to obtain the actual exception if the operation completed + * with an error. + * + * @return {@link true} if the batch operation was completed successfully. + */ + public boolean isSuccessful() { + return exception == null; + } + + /** + * Returns the cause of the failed operation if the operation completed with an error. + * + * @return the cause of the failure or {@literal null} if succeeded. + */ + @Nullable + public Exception getCause() { + return exception; + } + + /** + * Return the result 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 V get() { + + if (isSuccessful()) { + return get0(); + } + + throw new VaultException(exception.getMessage()); + } + + /** + * @return the actual result if this result completed successfully. + */ + @Nullable + protected abstract V get0(); +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/Ciphertext.java b/spring-vault-core/src/main/java/org/springframework/vault/support/Ciphertext.java new file mode 100644 index 00000000..3611b8be --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/Ciphertext.java @@ -0,0 +1,73 @@ +/* + * Copyright 2017 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 + * + * http://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 lombok.EqualsAndHashCode; + +import org.springframework.util.Assert; + +/** + * Value object representing ciphertext with an optional {@link VaultTransitContext}. + * + * @author Praveendra Singh + * @author Mark Paluch + * @since 1.1 + */ +@EqualsAndHashCode +public class Ciphertext { + + private final String ciphertext; + + private final VaultTransitContext context; + + private Ciphertext(String ciphertext, VaultTransitContext context) { + + this.ciphertext = ciphertext; + this.context = context; + } + + /** + * Factory method to create {@link Ciphertext} from the given {@code ciphertext}. + * + * @param ciphertext the ciphertext to decrypt, must not be {@literal null} or empty. + * @return the {@link Ciphertext} for {@code ciphertext}. + */ + public static Ciphertext of(String ciphertext) { + + Assert.hasText(ciphertext, "Ciphertext must not be null or empty"); + + return new Ciphertext(ciphertext, VaultTransitContext.empty()); + } + + public String getCiphertext() { + return ciphertext; + } + + public VaultTransitContext getContext() { + return context; + } + + /** + * Create a new {@link Ciphertext} object from this ciphertext associated with the + * given {@link VaultTransitContext}. + * + * @param context transit context. + * @return the new {@link Ciphertext} object. + */ + public Ciphertext with(VaultTransitContext context) { + return new Ciphertext(getCiphertext(), context); + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/Plaintext.java b/spring-vault-core/src/main/java/org/springframework/vault/support/Plaintext.java new file mode 100644 index 00000000..e5e3e438 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/Plaintext.java @@ -0,0 +1,99 @@ +/* + * Copyright 2017 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 + * + * http://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 lombok.EqualsAndHashCode; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Value object representing plaintext with an optional {@link VaultTransitContext}. + * Plaintext is represented binary safe as {@code byte[]}. + * + * @author Praveendra Singh + * @author Mark Paluch + * @since 1.1 + */ +@EqualsAndHashCode +public class Plaintext { + + private final byte[] plaintext; + + private final VaultTransitContext context; + + private Plaintext(byte[] plaintext, VaultTransitContext context) { + + this.plaintext = plaintext; + this.context = context; + } + + /** + * Factory method to create {@link Plaintext} from a byte sequence. + * + * @param plaintext the plaintext to encrypt, must not be {@literal null} or empty. + * @return the {@link Plaintext} for {@code plaintext}. + */ + public static Plaintext of(byte[] plaintext) { + + Assert.isTrue(!ObjectUtils.isEmpty(plaintext), + "Plaintext must not be null or empty"); + + return new Plaintext(plaintext, VaultTransitContext.empty()); + } + + /** + * Factory method to create {@link Plaintext} 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} or empty. + * @return the {@link Plaintext} for {@code plaintext}. + */ + public static Plaintext of(String plaintext) { + + Assert.hasText(plaintext, "Plaintext must not be null or empty"); + + return of(plaintext.getBytes()); + } + + public byte[] getPlaintext() { + return plaintext; + } + + public VaultTransitContext getContext() { + return context; + } + + /** + * Create a new {@link Plaintext} object from this plaintext associated with the given + * {@link VaultTransitContext}. + * + * @param context transit context. + * @return the new {@link Plaintext} object. + */ + public Plaintext with(VaultTransitContext context) { + return new Plaintext(getPlaintext(), context); + } + + /** + * @return the plaintext as {@link String} decoded using the default + * {@link java.nio.charset.Charset}. + */ + public String asString() { + return new String(getPlaintext()); + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultDecryptionPayload.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultDecryptionPayload.java deleted file mode 100644 index 5ea436f5..00000000 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultDecryptionPayload.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.springframework.vault.support; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; - -/** - * Decryption Value Object used for encrypt() operations. - * - * @author Praveendra Singh - * - */ -@Getter -@Setter -@AllArgsConstructor -public class VaultDecryptionPayload { - private String ciphertext; - private VaultTransitContext context; - - /** - * factory method helps to create decryption value object using ciphertext - * in String - * - * @param ciphertext - * to be decrypted - * @return decryption value object - */ - public static VaultDecryptionPayload of(String ciphertext) { - - if (ciphertext == null) { - throw new IllegalArgumentException("The ciphertext must not be null"); - } - - return new VaultDecryptionPayload(ciphertext, null); - } - - /** - * sets the decryption context to the value object. - * - * @param context - * transit decryption context - * @return decryption value object - */ - public VaultDecryptionPayload with(VaultTransitContext context) { - return new VaultDecryptionPayload(this.getCiphertext(), context); - } -} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultDecryptionResult.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultDecryptionResult.java index b874b2ba..63455a33 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultDecryptionResult.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultDecryptionResult.java @@ -1,39 +1,77 @@ +/* + * Copyright 2017 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 + * + * http://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.StringUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.vault.VaultException; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; - /** - * Holds the response from decryption operation and provides helper methods to - * deal with the data. - * - * @author Praveendra Singh + * Holds the response from decryption operation and provides methods to access the result. * + * @author Praveendra Singh + * @author Mark Paluch + * @since 1.1 */ -@Getter -@Setter -@AllArgsConstructor -public class VaultDecryptionResult { +public class VaultDecryptionResult extends AbstractResult<Plaintext> { - private byte[] cipherText; - private String error; + private final @Nullable Plaintext plaintext; /** - * returns the list of plaintext or throws VaultException if error - * encountered. - * - * @return plaintext + * Create {@link VaultDecryptionResult} for a successfully decrypted {@link Plaintext} + * . + * + * @param plaintext must not be {@literal null}. */ - public byte[] get() { + public VaultDecryptionResult(Plaintext plaintext) { - if (!StringUtils.isEmpty(error)) { - throw new VaultException(error); - } - return cipherText; + Assert.notNull(plaintext, "Plaintext must not be null"); + + this.plaintext = plaintext; } + /** + * Create {@link VaultDecryptionResult} for an error during decryption. + * + * @param exception must not be {@literal null}. + */ + public VaultDecryptionResult(VaultException exception) { + + super(exception); + this.plaintext = null; + } + + @Nullable + @Override + protected Plaintext get0() { + return 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() { + + Plaintext plaintext = get(); + return plaintext == null ? null : plaintext.asString(); + } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultEncryptionDecryptionResultHelper.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultEncryptionDecryptionResultHelper.java deleted file mode 100644 index 60b47ac0..00000000 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultEncryptionDecryptionResultHelper.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.springframework.vault.support; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import org.springframework.util.Base64Utils; - -import lombok.Getter; -import lombok.Setter; - -/** - * Holds the response from encryption/decryption operation and provides helper - * methods to generate list of encryption/decryption objects by fetching the - * respective fields from VaultResponse. - * - * @author Praveendra Singh - * - */ -@Getter -@Setter -public class VaultEncryptionDecryptionResultHelper { - - public static List<VaultEncryptionResult> fetchEncryptionResult(VaultResponse vaultResponse) { - - List<VaultEncryptionResult> result = new ArrayList<VaultEncryptionResult>(); - - for (Map<String, String> data : getBatchData(vaultResponse)) { - - VaultEncryptionResult res = new VaultEncryptionResult(data.get("ciphertext"), data.get("error")); - result.add(res); - } - - return result; - } - - public static List<VaultDecryptionResult> fetchDecryptionResult(VaultResponse vaultResponse) { - - List<VaultDecryptionResult> result = new ArrayList<VaultDecryptionResult>(); - - for (Map<String, String> data : getBatchData(vaultResponse)) { - - VaultDecryptionResult res = new VaultDecryptionResult(Base64Utils.decodeFromString(data.get("plaintext")), - data.get("error")); - result.add(res); - } - - return result; - } - - @SuppressWarnings("unchecked") - protected static List<Map<String, String>> getBatchData(VaultResponse vaultResponse) { - return (List<Map<String, String>>) vaultResponse.getData().get("batch_results"); - } -} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultEncryptionPayload.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultEncryptionPayload.java deleted file mode 100644 index cce3f6b6..00000000 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultEncryptionPayload.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.springframework.vault.support; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; - -/** - * Encryption Value Object used for encrypt() operations. - * - * @author Praveendra Singh - * - */ -@Getter -@Setter -@AllArgsConstructor -public class VaultEncryptionPayload { - private byte[] plaintext; - private VaultTransitContext context; - - /** - * factory method helps to create encryption value object using plaintext in - * bytes - * - * @param plaintext - * data to be encrypted - * - * @return encryption value object - */ - public static VaultEncryptionPayload of(byte[] plaintext) { - - if ((plaintext == null) || (plaintext.length == 0)) { - throw new IllegalArgumentException("The plaintext must not be null or empty"); - } - - return new VaultEncryptionPayload(plaintext, null); - } - - /** - * factory method helps to create encryption value object using plaintext in - * String - * - * @param plaintext - * data to be encrypted - * - * @return encryption value object - */ - public static VaultEncryptionPayload of(String plaintext) { - - if (plaintext == null) { - throw new IllegalArgumentException("The plaintext must not be null"); - } - - return of(plaintext.getBytes()); - } - - /** - * sets the encryption context to the value object. - * - * @param context - * transit encryption context - * @return encryption value object - */ - public VaultEncryptionPayload with(VaultTransitContext context) { - return new VaultEncryptionPayload(this.getPlaintext(), context); - } -} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultEncryptionResult.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultEncryptionResult.java index e9fa88f6..30b69592 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultEncryptionResult.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultEncryptionResult.java @@ -1,39 +1,58 @@ +/* + * Copyright 2017 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 + * + * http://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.StringUtils; +import org.springframework.lang.Nullable; import org.springframework.vault.VaultException; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; - /** - * Holds the response from encryption operation and provides helper methods to - * deal with the data. - * - * @author Praveendra Singh + * Holds the response from encryption operation and provides methods to access the result. * + * @author Praveendra Singh + * @author Mark Paluch + * @since 1.1 */ -@Getter -@Setter -@AllArgsConstructor -public class VaultEncryptionResult { +public class VaultEncryptionResult extends AbstractResult<Ciphertext> { - private String cipherText; - private String error; + private final @Nullable Ciphertext cipherText; /** - * returns the list of ciphertexts or throws VaultException if error - * encountered. - * - * @return ciphertexts + * Create {@link VaultEncryptionResult} for a successfully encrypted + * {@link Ciphertext} . + * + * @param cipherText must not be {@literal null}. */ - public String get() { - - if (!StringUtils.isEmpty(error)) { - throw new VaultException(error); - } - return cipherText; + public VaultEncryptionResult(Ciphertext cipherText) { + this.cipherText = cipherText; } + /** + * Create {@link VaultEncryptionResult} for an error during encryption. + * + * @param exception must not be {@literal null}. + */ + public VaultEncryptionResult(VaultException exception) { + + super(exception); + this.cipherText = null; + } + + @Nullable + @Override + protected Ciphertext get0() { + return cipherText; + } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultTransitContext.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultTransitContext.java index 39be1040..05d521f8 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultTransitContext.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultTransitContext.java @@ -15,6 +15,8 @@ */ package org.springframework.vault.support; +import lombok.EqualsAndHashCode; + import org.springframework.util.Assert; /** @@ -22,6 +24,7 @@ import org.springframework.util.Assert; * * @author Mark Paluch */ +@EqualsAndHashCode public class VaultTransitContext { /** diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTransitTemplateIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTransitTemplateIntegrationTests.java index 9b5b32c4..3fd647ab 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTransitTemplateIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTransitTemplateIntegrationTests.java @@ -15,25 +15,21 @@ */ package org.springframework.vault.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; -import static org.junit.Assume.assumeTrue; - -import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.junit.After; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.vault.VaultException; -import org.springframework.vault.support.VaultDecryptionPayload; +import org.springframework.vault.support.Ciphertext; +import org.springframework.vault.support.Plaintext; import org.springframework.vault.support.VaultDecryptionResult; -import org.springframework.vault.support.VaultEncryptionPayload; import org.springframework.vault.support.VaultEncryptionResult; import org.springframework.vault.support.VaultMount; import org.springframework.vault.support.VaultTransitContext; @@ -43,6 +39,10 @@ import org.springframework.vault.support.VaultTransitKeyCreationRequest; import org.springframework.vault.util.IntegrationTestSupport; import org.springframework.vault.util.Version; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.junit.Assume.assumeTrue; + /** * Integration tests for {@link VaultTransitTemplate} through * {@link VaultTransitOperations}. @@ -54,7 +54,7 @@ import org.springframework.vault.util.Version; @ContextConfiguration(classes = VaultIntegrationTestConfiguration.class) public class VaultTransitTemplateIntegrationTests extends IntegrationTestSupport { - private static final String BATCH_INTRODUCED_IN_VERSION = "0.6.5"; + private static final Version BATCH_INTRODUCED_IN_VERSION = Version.parse("0.6.5"); @Autowired private VaultOperations vaultOperations; @@ -106,7 +106,7 @@ public class VaultTransitTemplateIntegrationTests extends IntegrationTestSupport deleteKey("derived"); } } - + @Test public void createKeyShouldCreateKey() { @@ -209,6 +209,24 @@ public class VaultTransitTemplateIntegrationTests extends IntegrationTestSupport assertThat(ciphertext).startsWith("vault:v1:"); } + @Test + public void encryptShouldCreateWrappedCiphertextWithNonceAndContext() { + + transitOperations.createKey("mykey", VaultTransitKeyCreationRequest.builder() + .convergentEncryption(true).derived(true).build()); + + VaultTransitContext context = VaultTransitContext.builder() + .context("blubb".getBytes()) // + .nonce("123456789012".getBytes()) // + .build(); + + Ciphertext ciphertext = transitOperations.encrypt("mykey", + Plaintext.of("hello-world").with(context)); + + assertThat(ciphertext.getCiphertext()).startsWith("vault:v1:"); + assertThat(ciphertext.getContext()).isEqualTo(context); + } + @Test public void decryptShouldCreatePlaintext() { @@ -238,6 +256,25 @@ public class VaultTransitTemplateIntegrationTests extends IntegrationTestSupport assertThat(new String(plaintext)).isEqualTo("hello-world"); } + @Test + public void decryptShouldCreateWrappedPlaintextWithNonceAndContext() { + + transitOperations.createKey("mykey", VaultTransitKeyCreationRequest.builder() + .convergentEncryption(true).derived(true).build()); + + VaultTransitContext context = VaultTransitContext.builder() // + .context("blubb".getBytes()) // + .nonce("123456789012".getBytes()) // + .build(); + + Ciphertext ciphertext = transitOperations.encrypt("mykey", + Plaintext.of("hello-world").with(context)); + Plaintext plaintext = transitOperations.decrypt("mykey", ciphertext); + + assertThat(plaintext.asString()).isEqualTo("hello-world"); + assertThat(plaintext.getContext()).isEqualTo(context); + } + @Test public void encryptAndRewrapShouldCreateCiphertext() { @@ -285,29 +322,50 @@ public class VaultTransitTemplateIntegrationTests extends IntegrationTestSupport String rewrapped = transitOperations.rewrap("mykey", ciphertext, transitRequest); assertThat(rewrapped).startsWith("vault:v2"); } - - @Test - public void batchEncryptionAndDecryptionTestWithoutContext() { - if (prepare().getVersion().isLessThan(Version.parse(BATCH_INTRODUCED_IN_VERSION))) { - return; - } + @Test + public void shouldBatchEncrypt() { + + assumeTrue(prepare().getVersion().isGreaterThanOrEqualTo( + BATCH_INTRODUCED_IN_VERSION)); transitOperations.createKey("mykey"); - List<String> plaintexts = new ArrayList<String>(); - plaintexts.add("one"); - plaintexts.add("two"); + List<VaultEncryptionResult> encrypted = transitOperations.encrypt("mykey", + Arrays.asList(Plaintext.of("one"), Plaintext.of("two"))); - batchEncryptionAndDecryption(plaintexts, null, null); + assertThat(encrypted.get(0).get().getCiphertext()).startsWith("vault:"); + assertThat(encrypted.get(1).get().getCiphertext()).startsWith("vault:"); } @Test - public void batchEncryptionAndDecryptionTestWithMatchingContext() { + public void shouldBatchDecrypt() { - if (prepare().getVersion().isLessThan(Version.parse(BATCH_INTRODUCED_IN_VERSION))) { - return; - } + assumeTrue(prepare().getVersion().isGreaterThanOrEqualTo( + BATCH_INTRODUCED_IN_VERSION)); + + transitOperations.createKey("mykey"); + + Ciphertext one = transitOperations.encrypt("mykey", Plaintext.of("one")); + Ciphertext two = transitOperations.encrypt("mykey", Plaintext.of("two")); + + Plaintext plainOne = transitOperations.decrypt("mykey", one); + Plaintext plainTwo = transitOperations.decrypt("mykey", two); + + List<VaultDecryptionResult> decrypted = transitOperations.decrypt("mykey", + Arrays.asList(one, two)); + + assertThat(decrypted.get(0).get()).isEqualTo(plainOne); + assertThat(decrypted.get(0).getAsString()).isEqualTo("one"); + assertThat(decrypted.get(1).get()).isEqualTo(plainTwo); + assertThat(decrypted.get(1).getAsString()).isEqualTo("two"); + } + + @Test + public void shouldBatchEncryptWithContext() { + + assumeTrue(prepare().getVersion().isGreaterThanOrEqualTo( + BATCH_INTRODUCED_IN_VERSION)); VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder() // .derived(true) // @@ -315,140 +373,77 @@ public class VaultTransitTemplateIntegrationTests extends IntegrationTestSupport transitOperations.createKey("mykey", request); - List<String> plaintexts = new ArrayList<String>(); - plaintexts.add("one"); - plaintexts.add("two"); + Plaintext one = Plaintext.of("one").with( + VaultTransitContext.builder().context("oneContext".getBytes()).build()); - List<VaultTransitContext> contexts = new ArrayList<VaultTransitContext>(); - contexts.add(VaultTransitContext.builder().context("oneContext".getBytes()).build()); - contexts.add(VaultTransitContext.builder().context("twoContext".getBytes()).build()); + Plaintext two = Plaintext.of("two").with( + VaultTransitContext.builder().context("twoContext".getBytes()).build()); - batchEncryptionAndDecryption(plaintexts, contexts, contexts); + List<VaultEncryptionResult> encrypted = transitOperations.encrypt("mykey", + Arrays.asList(one, two)); + + assertThat(encrypted.get(0).get().getContext()).isEqualTo(one.getContext()); + assertThat(encrypted.get(1).get().getContext()).isEqualTo(two.getContext()); } @Test - public void batchEncryptionAndDecryptionTestWithNonEqualContext() { + public void shouldBatchDecryptWithContext() { - if (prepare().getVersion().isLessThan(Version.parse(BATCH_INTRODUCED_IN_VERSION))) { - return; - } + assumeTrue(prepare().getVersion().isGreaterThanOrEqualTo( + BATCH_INTRODUCED_IN_VERSION)); - try { + VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder() // + .derived(true) // + .build(); - VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder() // - .derived(true) // - .build(); + transitOperations.createKey("mykey", request); - transitOperations.createKey("mykey", request); + Plaintext one = Plaintext.of("one").with( + VaultTransitContext.builder().context("oneContext".getBytes()).build()); - List<String> plaintexts = new ArrayList<String>(); - plaintexts.add("one"); - plaintexts.add("two"); + Plaintext two = Plaintext.of("two").with( + VaultTransitContext.builder().context("twoContext".getBytes()).build()); - List<VaultTransitContext> encryptionContexts = new ArrayList<VaultTransitContext>(); - encryptionContexts.add(VaultTransitContext.builder().context("oneContext".getBytes()).build()); - encryptionContexts.add(VaultTransitContext.builder().context("twoContext".getBytes()).build()); + List<VaultEncryptionResult> encrypted = transitOperations.encrypt("mykey", + Arrays.asList(one, two)); + List<VaultDecryptionResult> decrypted = transitOperations.decrypt("mykey", + Arrays.asList(encrypted.get(0).get(), encrypted.get(1).get())); - List<VaultTransitContext> decryptionContext = new ArrayList<VaultTransitContext>(); - decryptionContext.add(VaultTransitContext.builder().context("oneContext".getBytes()).build()); - - batchEncryptionAndDecryption(plaintexts, encryptionContexts, decryptionContext); - - } catch (IllegalArgumentException e) { - return; - } catch (VaultException e) { - return; - } - - Assert.fail(); + assertThat(decrypted.get(0).get()).isEqualTo(one); + assertThat(decrypted.get(1).get()).isEqualTo(two); } @Test - public void batchEncryptionAndDecryptionTestWithNonMatchingContext() { + public void shouldBatchDecryptWithWrongContext() { - if (prepare().getVersion().isLessThan(Version.parse(BATCH_INTRODUCED_IN_VERSION))) { - return; - } + assumeTrue(prepare().getVersion().isGreaterThanOrEqualTo( + BATCH_INTRODUCED_IN_VERSION)); - try { + VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder() // + .derived(true) // + .build(); - VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder() // - .derived(true) // - .build(); + transitOperations.createKey("mykey", request); - transitOperations.createKey("mykey", request); + Plaintext one = Plaintext.of("one").with( + VaultTransitContext.builder().context("oneContext".getBytes()).build()); - List<String> plaintexts = new ArrayList<String>(); - plaintexts.add("one"); - plaintexts.add("two"); + Plaintext two = Plaintext.of("two").with( + VaultTransitContext.builder().context("twoContext".getBytes()).build()); - List<VaultTransitContext> encryptionContexts = new ArrayList<VaultTransitContext>(); - encryptionContexts.add(VaultTransitContext.builder().context("oneContext".getBytes()).build()); - encryptionContexts.add(VaultTransitContext.builder().context("twoContext".getBytes()).build()); + List<VaultEncryptionResult> encrypted = transitOperations.encrypt("mykey", + Arrays.asList(one, two)); - List<VaultTransitContext> decryptionContext = new ArrayList<VaultTransitContext>(); - decryptionContext.add(VaultTransitContext.builder().context("oneContext".getBytes()).build()); - decryptionContext.add(VaultTransitContext.builder().context("wrongTwoContext".getBytes()).build()); + Ciphertext encryptedOne = encrypted.get(0).get(); + Ciphertext decryptedTwo = encrypted.get(1).get(); - batchEncryptionAndDecryption(plaintexts, encryptionContexts, decryptionContext); + Ciphertext tampered = decryptedTwo.with(encryptedOne.getContext()); - } catch (VaultException e) { - return; - } + List<VaultDecryptionResult> decrypted = transitOperations.decrypt("mykey", + Arrays.asList(encryptedOne, tampered)); - Assert.fail(); - } - - private void batchEncryptionAndDecryption(List<String> plaintexts, List<VaultTransitContext> encryptionContexts, - List<VaultTransitContext> decryptionContext) { - - List<VaultEncryptionPayload> encryptionBatchRequest = new ArrayList<VaultEncryptionPayload>(); - - int index = 0; - - for (String plaintext : plaintexts) { - - VaultEncryptionPayload req = VaultEncryptionPayload.of(plaintext); - - if (encryptionContexts != null) { - if (encryptionContexts.size() >= (index + 1)) { - req = req.with(encryptionContexts.get(index)); - } - } - - encryptionBatchRequest.add(req); - index++; - } - - List<VaultEncryptionResult> cipherResult = transitOperations.encrypt("mykey", encryptionBatchRequest); - - List<VaultDecryptionPayload> decryptionBatchRequest = new ArrayList<VaultDecryptionPayload>(); - - index = 0; - - for (VaultEncryptionResult cipher : cipherResult) { - - VaultDecryptionPayload req = VaultDecryptionPayload.of(cipher.get()); - - if (decryptionContext != null) { - if (decryptionContext.size() >= (index + 1)) { - req = req.with(decryptionContext.get(index)); - } - } - - decryptionBatchRequest.add(req); - index++; - } - - List<VaultDecryptionResult> plaintextResult = transitOperations.decrypt("mykey", decryptionBatchRequest); - - Assert.assertEquals(plaintexts.size(), plaintextResult.size()); - - int i = 0; - - for (String plaintext : plaintexts) { - String decrypted = new String(plaintextResult.get(i++).get()); - Assert.assertEquals(plaintext, decrypted); - } + assertThat(decrypted.get(0).get()).isEqualTo(one); + assertThat(decrypted.get(1).isSuccessful()).isEqualTo(false); + assertThat(decrypted.get(1).getCause()).isInstanceOf(VaultException.class); } } diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 607456cb..a39a6167 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -7,6 +7,7 @@ * Authentication steps DSL to <<vault.authentication.steps,compose authentication flows>>. * Reactive Vault client via `ReactiveVaultOperations`. * <<vault.repositories,Vault repository support>> based on Spring Data KeyValue. +* Transit batch encrypt and decrypt support. [[new-features.1-0-0]] === What's new in Spring Vault 1.0 @@ -16,4 +17,4 @@ [[new-features.1-1-0]] === What's new in Spring Vault 1.1.0 -* Batch encryption & decryption support. +* Transit batch encrypt and decrypt support.