diff --git a/pom.xml b/pom.xml index 6c485750c..c426db5e6 100644 --- a/pom.xml +++ b/pom.xml @@ -53,16 +53,23 @@ - org.mockito - mockito-core - 2.28.1 + org.junit.jupiter + junit-jupiter-engine + 5.5.2 + test + + + + org.junit.vintage + junit-vintage-engine + 5.5.2 test - junit - junit - 4.12 + org.mockito + mockito-junit-jupiter + 3.1.0 test diff --git a/src/main/java/com/amazonaws/encryptionsdk/EncryptedDataKey.java b/src/main/java/com/amazonaws/encryptionsdk/EncryptedDataKey.java index 4629a9e07..aa5cfeb89 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/EncryptedDataKey.java +++ b/src/main/java/com/amazonaws/encryptionsdk/EncryptedDataKey.java @@ -16,9 +16,13 @@ //@ model import java.util.Arrays; //@ model import java.nio.charset.StandardCharsets; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; //@ nullable_by_default public interface EncryptedDataKey { + + Charset PROVIDER_ENCODING = StandardCharsets.UTF_8; //@// An EncryptedDataKey object abstractly contains 3 pieces of data. //@// These are represented by 3 byte arrays: diff --git a/src/main/java/com/amazonaws/encryptionsdk/keyrings/DecryptionMaterials.java b/src/main/java/com/amazonaws/encryptionsdk/keyrings/DecryptionMaterials.java new file mode 100644 index 000000000..1cf84058e --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/keyrings/DecryptionMaterials.java @@ -0,0 +1,227 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.encryptionsdk.keyrings; + +import com.amazonaws.encryptionsdk.CryptoAlgorithm; + +import javax.crypto.SecretKey; +import java.security.PublicKey; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static java.util.Objects.requireNonNull; +import static org.apache.commons.lang3.Validate.isTrue; + +/** + * Contains the cryptographic materials needed for a decryption operation with Keyrings. + */ +public final class DecryptionMaterials { + private final CryptoAlgorithm algorithmSuite; + private SecretKey plaintextDataKey; + private final PublicKey verificationKey; + private final Map encryptionContext; + private final KeyringTrace keyringTrace; + + private DecryptionMaterials(Builder b) { + requireNonNull(b.algorithmSuite, "algorithmSuite is required"); + requireNonNull(b.keyringTrace, "keyringTrace is required"); + requireNonNull(b.encryptionContext, "encryptionContext is required"); + validatePlaintextDataKey(b.algorithmSuite, b.plaintextDataKey); + validateVerificationKey(b.algorithmSuite, b.verificationKey); + + algorithmSuite = b.algorithmSuite; + plaintextDataKey = b.plaintextDataKey; + verificationKey = b.verificationKey; + encryptionContext = b.encryptionContext; + keyringTrace = b.keyringTrace; + } + + /** + * The algorithm suite to use for this decryption operation. + */ + public CryptoAlgorithm getAlgorithmSuite() { + return algorithmSuite; + } + + /** + * Returns true if a plaintext data key has been populated. + * + * @return True if plaintext data key is populated, false otherwise. + */ + public boolean hasPlaintextDataKey() { + return this.plaintextDataKey != null; + } + + /** + * A data key to be used as input for encryption. + * + * @return The plaintext data key. + * @throws IllegalStateException if plaintext data key has not been populated. + */ + public SecretKey getPlaintextDataKey() throws IllegalStateException { + if (!hasPlaintextDataKey()) { + throw new IllegalStateException("plaintextDataKey has not been populated"); + } + return plaintextDataKey; + } + + /** + * Sets the plaintext data key. The plaintext data key must not already be populated. + * + * @param plaintextDataKey The plaintext data key. + * @param keyringTraceEntry The keyring trace entry recording this action. + */ + public void setPlaintextDataKey(SecretKey plaintextDataKey, KeyringTraceEntry keyringTraceEntry) { + if (hasPlaintextDataKey()) { + throw new IllegalStateException("plaintextDataKey was already populated"); + } + requireNonNull(plaintextDataKey, "plaintextDataKey is required"); + requireNonNull(keyringTraceEntry, "keyringTraceEntry is required"); + validatePlaintextDataKey(algorithmSuite, plaintextDataKey); + this.plaintextDataKey = plaintextDataKey; + keyringTrace.add(keyringTraceEntry); + } + + /** + * Returns true if verification key has been populated. + * + * @return True if verification key is populated, false otherwise. + */ + public boolean hasVerificationKey() { + return verificationKey != null; + } + + /** + * The verification key used for signature verification. + * + * @return The verification key. + * @throws IllegalStateException if a verification key has not been populated. + */ + public PublicKey getVerificationKey() throws IllegalStateException { + if (!hasVerificationKey()) { + throw new IllegalStateException(String.format( + "Signature verification is not supported by AlgorithmSuite %s", algorithmSuite.name())); + } + return verificationKey; + } + + public Map getEncryptionContext() { + return encryptionContext; + } + + public KeyringTrace getKeyringTrace() { + return keyringTrace; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DecryptionMaterials that = (DecryptionMaterials) o; + return algorithmSuite == that.algorithmSuite && + Objects.equals(plaintextDataKey, that.plaintextDataKey) && + Objects.equals(verificationKey, that.verificationKey) && + Objects.equals(encryptionContext, that.encryptionContext) && + Objects.equals(keyringTrace, that.keyringTrace); + } + + @Override + public int hashCode() { + return Objects.hash(algorithmSuite, plaintextDataKey, verificationKey, encryptionContext, keyringTrace); + } + + public static Builder newBuilder(CryptoAlgorithm algorithm) { + return new Builder(algorithm); + } + + public Builder toBuilder() { + return new Builder(this); + } + + private void validatePlaintextDataKey(CryptoAlgorithm algorithmSuite, SecretKey plaintextDataKey) throws IllegalArgumentException { + if (plaintextDataKey != null) { + isTrue(algorithmSuite.getDataKeyLength() == plaintextDataKey.getEncoded().length, + String.format("Incorrect key length. Expected %s but got %s", + algorithmSuite.getDataKeyLength(), plaintextDataKey.getEncoded().length)); + isTrue(algorithmSuite.getDataKeyAlgo().equalsIgnoreCase(plaintextDataKey.getAlgorithm()), + String.format("Incorrect key algorithm. Expected %s but got %s", + algorithmSuite.getDataKeyAlgo(), plaintextDataKey.getAlgorithm())); + } + } + + /** + * Validates that a verification key is specified if and only if + * the given algorithm suite supports signature verification. + */ + private void validateVerificationKey(CryptoAlgorithm algorithmSuite, PublicKey verificationKey) throws IllegalArgumentException { + if (algorithmSuite.getTrailingSignatureAlgo() == null && verificationKey != null) { + throw new IllegalArgumentException( + String.format("Algorithm Suite %s does not support signature verification", algorithmSuite.name())); + } else if (algorithmSuite.getTrailingSignatureAlgo() != null && verificationKey == null) { + throw new IllegalArgumentException( + String.format("Algorithm %s requires a verification key for signature verification", algorithmSuite.name())); + } + } + + public static final class Builder { + private CryptoAlgorithm algorithmSuite; + private SecretKey plaintextDataKey; + private PublicKey verificationKey; + private Map encryptionContext = Collections.emptyMap(); + private KeyringTrace keyringTrace = new KeyringTrace(); + + private Builder(CryptoAlgorithm algorithmSuite) { + this.algorithmSuite = algorithmSuite; + } + + private Builder(DecryptionMaterials result) { + this.algorithmSuite = result.algorithmSuite; + this.plaintextDataKey = result.plaintextDataKey; + this.verificationKey = result.verificationKey; + this.encryptionContext = result.encryptionContext; + this.keyringTrace = result.keyringTrace; + } + + public Builder algorithmSuite(CryptoAlgorithm algorithmSuite) { + this.algorithmSuite = algorithmSuite; + return this; + } + + public Builder plaintextDataKey(SecretKey plaintextDataKey) { + this.plaintextDataKey = plaintextDataKey; + return this; + } + + public Builder verificationKey(PublicKey verificationKey) { + this.verificationKey = verificationKey; + return this; + } + + public Builder encryptionContext(Map encryptionContext) { + this.encryptionContext = Collections.unmodifiableMap(new HashMap<>(encryptionContext)); + return this; + } + + public Builder keyringTrace(KeyringTrace keyringTrace) { + this.keyringTrace = keyringTrace; + return this; + } + + public DecryptionMaterials build() { + return new DecryptionMaterials(this); + } + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/keyrings/EncryptionMaterials.java b/src/main/java/com/amazonaws/encryptionsdk/keyrings/EncryptionMaterials.java new file mode 100644 index 000000000..d52f5d355 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/keyrings/EncryptionMaterials.java @@ -0,0 +1,270 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.encryptionsdk.keyrings; + +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.EncryptedDataKey; + +import javax.crypto.SecretKey; +import java.security.PrivateKey; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static java.util.Objects.requireNonNull; +import static org.apache.commons.lang3.Validate.isTrue; + +/** + * Contains the cryptographic materials needed for an encryption operation with Keyrings. + */ +public final class EncryptionMaterials { + private final CryptoAlgorithm algorithmSuite; + private final Map encryptionContext; + private final List encryptedDataKeys; + private SecretKey plaintextDataKey; + private final PrivateKey signingKey; + private final KeyringTrace keyringTrace; + + private EncryptionMaterials(Builder b) { + requireNonNull(b.algorithmSuite, "algorithmSuite is required"); + requireNonNull(b.keyringTrace, "keyringTrace is required"); + requireNonNull(b.encryptionContext, "encryptionContext is required"); + validatePlaintextDataKey(b.algorithmSuite, b.plaintextDataKey); + validateSigningKey(b.algorithmSuite, b.signingKey); + this.algorithmSuite = b.algorithmSuite; + this.encryptionContext = b.encryptionContext; + this.encryptedDataKeys = b.encryptedDataKeys; + this.plaintextDataKey = b.plaintextDataKey; + this.signingKey = b.signingKey; + this.keyringTrace = b.keyringTrace; + } + + public Builder toBuilder() { + return new Builder(this); + } + + public static Builder newBuilder(CryptoAlgorithm algorithmSuite) { + return new Builder(algorithmSuite); + } + + /** + * The algorithm suite to be used for encryption. + */ + public CryptoAlgorithm getAlgorithmSuite() { + return algorithmSuite; + } + + /** + * The encryption context associated with this encryption. + */ + public Map getEncryptionContext() { + return encryptionContext; + } + + /** + * An unmodifiable list of the encrypted data keys that correspond to the plaintext data key. + */ + public List getEncryptedDataKeys() { + return Collections.unmodifiableList(encryptedDataKeys); + } + + /** + * Add an encrypted data key to the list of encrypted data keys. + * + * @param encryptedDataKey The encrypted data key to add. + * @param keyringTraceEntry The keyring trace entry recording this action. + */ + public void addEncryptedDataKey(EncryptedDataKey encryptedDataKey, KeyringTraceEntry keyringTraceEntry) { + requireNonNull(encryptedDataKey, "encryptedDataKey is required"); + requireNonNull(keyringTraceEntry, "keyringTraceEntry is required"); + encryptedDataKeys.add(encryptedDataKey); + keyringTrace.add(keyringTraceEntry); + } + + /** + * Returns true if a plaintext data key has been populated. + * + * @return True if plaintext data key is populated, false otherwise. + */ + public boolean hasPlaintextDataKey() { + return this.plaintextDataKey != null; + } + + /** + * A data key to be used as input for encryption. + * + * @return The plaintext data key. + * @throws IllegalStateException if plain text data key has not been populated. + */ + public SecretKey getPlaintextDataKey() throws IllegalStateException { + if (!hasPlaintextDataKey()) { + throw new IllegalStateException("plaintextDataKey has not been populated"); + } + return plaintextDataKey; + } + + /** + * Sets the plaintext data key. The plaintext data key must not already be populated. + * + * @param plaintextDataKey The plaintext data key. + * @param keyringTraceEntry The keyring trace entry recording this action. + */ + public void setPlaintextDataKey(SecretKey plaintextDataKey, KeyringTraceEntry keyringTraceEntry) { + if (hasPlaintextDataKey()) { + throw new IllegalStateException("plaintextDataKey was already populated"); + } + requireNonNull(plaintextDataKey, "plaintextDataKey is required"); + requireNonNull(keyringTraceEntry, "keyringTraceEntry is required"); + validatePlaintextDataKey(algorithmSuite, plaintextDataKey); + this.plaintextDataKey = plaintextDataKey; + keyringTrace.add(keyringTraceEntry); + } + + /** + * Returns true if a signing key has been populated. + * + * @return True if signing key is populated, false otherwise. + */ + public boolean hasSigningKey() { + return this.signingKey != null; + } + + + /** + * The key to be used as the signing key for signature verification during encryption. + * + * @return The signing key. + * @throws IllegalStateException if signing key has not been populated. + */ + public PrivateKey getSigningKey() throws IllegalStateException { + if (!hasSigningKey()) { + throw new IllegalStateException(String.format( + "Signing is not supported by AlgorithmSuite %s", algorithmSuite.name())); + } + return signingKey; + } + + /** + * A keyring trace containing all of the actions that keyrings have taken on this set of encryption materials. + */ + public KeyringTrace getKeyringTrace() { + return keyringTrace; + } + + /** + * Validates that the given plaintext data key fits the specification + * for the data key algorithm specified in the given algorithm suite. + */ + private void validatePlaintextDataKey(CryptoAlgorithm algorithmSuite, SecretKey plaintextDataKey) throws IllegalArgumentException { + if (plaintextDataKey != null) { + isTrue(algorithmSuite.getDataKeyLength() == plaintextDataKey.getEncoded().length, + String.format("Incorrect data key length. Expected %s but got %s", + algorithmSuite.getDataKeyLength(), plaintextDataKey.getEncoded().length)); + isTrue(algorithmSuite.getDataKeyAlgo().equalsIgnoreCase(plaintextDataKey.getAlgorithm()), + String.format("Incorrect data key algorithm. Expected %s but got %s", + algorithmSuite.getDataKeyAlgo(), plaintextDataKey.getAlgorithm())); + } + } + + /** + * Validates that a signing key is specified only if and only if + * the given algorithm suite supports signature verification. + */ + private void validateSigningKey(CryptoAlgorithm algorithmSuite, PrivateKey signingKey) throws IllegalArgumentException { + if (algorithmSuite.getTrailingSignatureAlgo() == null && signingKey != null) { + throw new IllegalArgumentException( + String.format("Algorithm Suite %s does not support signing", algorithmSuite.name())); + } else if (algorithmSuite.getTrailingSignatureAlgo() != null && signingKey == null) { + throw new IllegalArgumentException( + String.format("Algorithm Suite %s requires a signing key for signing", algorithmSuite.name())); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EncryptionMaterials that = (EncryptionMaterials) o; + return algorithmSuite == that.algorithmSuite && + Objects.equals(encryptionContext, that.encryptionContext) && + Objects.equals(encryptedDataKeys, that.encryptedDataKeys) && + Objects.equals(plaintextDataKey, that.plaintextDataKey) && + Objects.equals(signingKey, that.signingKey) && + Objects.equals(keyringTrace, that.keyringTrace); + } + + @Override + public int hashCode() { + return Objects.hash(algorithmSuite, encryptionContext, encryptedDataKeys, plaintextDataKey, signingKey, keyringTrace); + } + + public static class Builder { + private CryptoAlgorithm algorithmSuite; + private Map encryptionContext = Collections.emptyMap(); + private List encryptedDataKeys = new ArrayList<>(); + private SecretKey plaintextDataKey; + private PrivateKey signingKey; + private KeyringTrace keyringTrace = new KeyringTrace(); + + private Builder(CryptoAlgorithm algorithmSuite) { + this.algorithmSuite = algorithmSuite; + } + + private Builder(EncryptionMaterials r) { + algorithmSuite = r.algorithmSuite; + encryptionContext = r.encryptionContext; + encryptedDataKeys = r.encryptedDataKeys; + plaintextDataKey = r.plaintextDataKey; + signingKey = r.signingKey; + keyringTrace = r.keyringTrace; + } + + public EncryptionMaterials build() { + return new EncryptionMaterials(this); + } + + public Builder algorithmSuite(CryptoAlgorithm algorithmSuite) { + this.algorithmSuite = algorithmSuite; + return this; + } + + public Builder encryptionContext(Map encryptionContext) { + this.encryptionContext = Collections.unmodifiableMap(new HashMap<>(encryptionContext)); + return this; + } + + public Builder encryptedDataKeys(List encryptedDataKeys) { + this.encryptedDataKeys = new ArrayList<>(encryptedDataKeys); + return this; + } + + public Builder plaintextDataKey(SecretKey plaintextDataKey) { + this.plaintextDataKey = plaintextDataKey; + return this; + } + + public Builder signingKey(PrivateKey signingKey) { + this.signingKey = signingKey; + return this; + } + + public Builder keyringTrace(KeyringTrace keyringTrace) { + this.keyringTrace = keyringTrace; + return this; + } + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/keyrings/Keyring.java b/src/main/java/com/amazonaws/encryptionsdk/keyrings/Keyring.java new file mode 100644 index 000000000..20f4c69f5 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/keyrings/Keyring.java @@ -0,0 +1,40 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.encryptionsdk.keyrings; + +import com.amazonaws.encryptionsdk.EncryptedDataKey; + +import java.util.List; + +/** + * Keyrings are responsible for the generation, encryption, and decryption of data keys. + */ +public interface Keyring { + + /** + * Attempt to encrypt either the given data key (if present) or one that may be generated + * + * @param encryptionMaterials Materials needed for encryption that the keyring may modify. + */ + void onEncrypt(EncryptionMaterials encryptionMaterials); + + /** + * Attempt to decrypt the encrypted data keys + * + * @param decryptionMaterials Materials needed for decryption that the keyring may modify. + * @param encryptedDataKeys List of encrypted data keys. + */ + void onDecrypt(DecryptionMaterials decryptionMaterials, List encryptedDataKeys); + +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/keyrings/KeyringTrace.java b/src/main/java/com/amazonaws/encryptionsdk/keyrings/KeyringTrace.java index 7759d70d7..c9528c08f 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/keyrings/KeyringTrace.java +++ b/src/main/java/com/amazonaws/encryptionsdk/keyrings/KeyringTrace.java @@ -17,9 +17,7 @@ import org.apache.commons.lang3.builder.ToStringStyle; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; import java.util.List; /** @@ -38,8 +36,16 @@ public class KeyringTrace { * indicating what actions were taken by a keyring. */ public void add(String keyNamespace, String keyName, KeyringTraceFlag... flags) { - entries.add(new KeyringTraceEntry(keyNamespace, keyName, - new HashSet<>(Arrays.asList(flags)))); + add(new KeyringTraceEntry(keyNamespace, keyName, flags)); + } + + /** + * Add a new entry to the keyring trace. + * + * @param entry The entry to add. + */ + public void add(KeyringTraceEntry entry) { + entries.add(entry); } /** diff --git a/src/main/java/com/amazonaws/encryptionsdk/keyrings/KeyringTraceEntry.java b/src/main/java/com/amazonaws/encryptionsdk/keyrings/KeyringTraceEntry.java index 64a14a268..4f656c7be 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/keyrings/KeyringTraceEntry.java +++ b/src/main/java/com/amazonaws/encryptionsdk/keyrings/KeyringTraceEntry.java @@ -16,7 +16,9 @@ import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; +import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.Objects; import java.util.Set; @@ -37,17 +39,17 @@ public class KeyringTraceEntry { * * @param keyNamespace The namespace for the key. * @param keyName The name of the key. - * @param flags A set of one or more KeyringTraceFlag enums + * @param flags One or more KeyringTraceFlags * indicating what actions were taken by a keyring. */ - KeyringTraceEntry(final String keyNamespace, final String keyName, final Set flags) { + public KeyringTraceEntry(final String keyNamespace, final String keyName, final KeyringTraceFlag... flags) { notBlank(keyNamespace, "keyNamespace is required"); notBlank(keyName, "keyName is required"); notEmpty(flags, "At least one flag is required"); this.keyNamespace = keyNamespace; this.keyName = keyName; - this.flags = Collections.unmodifiableSet(flags); + this.flags = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(flags))); } /** diff --git a/src/main/java/com/amazonaws/encryptionsdk/keyrings/RawAesKeyring.java b/src/main/java/com/amazonaws/encryptionsdk/keyrings/RawAesKeyring.java new file mode 100644 index 000000000..5832658a9 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/keyrings/RawAesKeyring.java @@ -0,0 +1,65 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.encryptionsdk.keyrings; + +import com.amazonaws.encryptionsdk.EncryptedDataKey; +import com.amazonaws.encryptionsdk.internal.JceKeyCipher; +import com.amazonaws.encryptionsdk.internal.Utils; + +import javax.crypto.SecretKey; + +/** + * A {@code Keyring} which does local AES-GCM encryption + * decryption of data keys using the provided wrapping key. + *

+ * Instantiate by using the {@code StandardKeyrings.rawAes(...)} factory method. + */ +class RawAesKeyring extends RawKeyring { + + RawAesKeyring(String keyNamespace, String keyName, SecretKey wrappingKey) { + super(keyNamespace, keyName, JceKeyCipher.aesGcm(wrappingKey)); + } + + @Override + boolean validToDecrypt(EncryptedDataKey encryptedDataKey) { + + // the key provider ID of the encrypted data key must + // have a value equal to this keyring's key namespace. + if (!keyNamespace.equals(encryptedDataKey.getProviderId())) { + return false; + } + + // the key name obtained from the encrypted data key's key provider + // information must have a value equal to this keyring's key name. + if (!Utils.arrayPrefixEquals(encryptedDataKey.getProviderInformation(), keyNameBytes, keyNameBytes.length)) { + return false; + } + + return true; + } + + @Override + KeyringTraceEntry traceOnEncrypt() { + return new KeyringTraceEntry(keyNamespace, keyName, + KeyringTraceFlag.ENCRYPTED_DATA_KEY, + KeyringTraceFlag.SIGNED_ENCRYPTION_CONTEXT); + } + + @Override + KeyringTraceEntry traceOnDecrypt() { + return new KeyringTraceEntry(keyNamespace, keyName, + KeyringTraceFlag.DECRYPTED_DATA_KEY, + KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT); + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/keyrings/RawKeyring.java b/src/main/java/com/amazonaws/encryptionsdk/keyrings/RawKeyring.java new file mode 100644 index 000000000..8daa567f8 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/keyrings/RawKeyring.java @@ -0,0 +1,122 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.encryptionsdk.keyrings; + +import com.amazonaws.encryptionsdk.EncryptedDataKey; +import com.amazonaws.encryptionsdk.internal.JceKeyCipher; +import com.amazonaws.encryptionsdk.internal.Utils; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import java.util.List; +import java.util.logging.Logger; + +import static com.amazonaws.encryptionsdk.EncryptedDataKey.PROVIDER_ENCODING; +import static java.util.Objects.requireNonNull; +import static org.apache.commons.lang3.Validate.notBlank; + +/** + * A keyring supporting local encryption and decryption using either RSA or AES-GCM. + */ +abstract class RawKeyring implements Keyring { + + final String keyNamespace; + final String keyName; + final byte[] keyNameBytes; + private final JceKeyCipher jceKeyCipher; + private static final Logger LOGGER = Logger.getLogger(RawKeyring.class.getName()); + + RawKeyring(final String keyNamespace, final String keyName, JceKeyCipher jceKeyCipher) { + notBlank(keyNamespace, "keyNamespace is required"); + notBlank(keyName, "keyName is required"); + requireNonNull(jceKeyCipher, "jceKeyCipher is required"); + + this.keyNamespace = keyNamespace; + this.keyName = keyName; + this.keyNameBytes = keyName.getBytes(PROVIDER_ENCODING); + this.jceKeyCipher = jceKeyCipher; + } + + /** + * Returns true if the given encrypted data key may be decrypted with this keyring. + * + * @param encryptedDataKey The encrypted data key. + * @return True if the key may be decrypted, false otherwise. + */ + abstract boolean validToDecrypt(EncryptedDataKey encryptedDataKey); + + /** + * Gets the trace entry to add the the keyring trace upon successful encryption. + * + * @return The keyring trace entry. + */ + abstract KeyringTraceEntry traceOnEncrypt(); + + /** + * Gets the trace entry to add to the keyring trace upon successful decryption. + * + * @return The keyring trace entry. + */ + abstract KeyringTraceEntry traceOnDecrypt(); + + @Override + public void onEncrypt(EncryptionMaterials encryptionMaterials) { + requireNonNull(encryptionMaterials, "encryptionMaterials are required"); + + if (!encryptionMaterials.hasPlaintextDataKey()) { + generateDataKey(encryptionMaterials); + } + + final EncryptedDataKey encryptedDataKey = jceKeyCipher.encryptKey( + encryptionMaterials.getPlaintextDataKey().getEncoded(), + keyName, keyNamespace, encryptionMaterials.getEncryptionContext()); + encryptionMaterials.addEncryptedDataKey(encryptedDataKey, traceOnEncrypt()); + } + + @Override + public void onDecrypt(DecryptionMaterials decryptionMaterials, List encryptedDataKeys) { + requireNonNull(decryptionMaterials, "decryptionMaterials are required"); + requireNonNull(encryptedDataKeys, "encryptedDataKeys are required"); + + if (decryptionMaterials.hasPlaintextDataKey() || encryptedDataKeys.isEmpty()) { + return; + } + + for (EncryptedDataKey encryptedDataKey : encryptedDataKeys) { + if (validToDecrypt(encryptedDataKey)) { + try { + final byte[] decryptedKey = jceKeyCipher.decryptKey( + encryptedDataKey, keyName, decryptionMaterials.getEncryptionContext()); + decryptionMaterials.setPlaintextDataKey( + new SecretKeySpec(decryptedKey, decryptionMaterials.getAlgorithmSuite().getDataKeyAlgo()), + traceOnDecrypt()); + return; + } catch (Exception e) { + LOGGER.info("Could not decrypt key due to: " + e.getMessage()); + } + } + } + + LOGGER.warning("Could not decrypt any data keys"); + } + + private void generateDataKey(EncryptionMaterials encryptionMaterials) { + final byte[] rawKey = new byte[encryptionMaterials.getAlgorithmSuite().getDataKeyLength()]; + Utils.getSecureRandom().nextBytes(rawKey); + final SecretKey key = new SecretKeySpec(rawKey, encryptionMaterials.getAlgorithmSuite().getDataKeyAlgo()); + + encryptionMaterials.setPlaintextDataKey(key, new KeyringTraceEntry(keyNamespace, keyName, KeyringTraceFlag.GENERATED_DATA_KEY)); + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/keyrings/RawRsaKeyring.java b/src/main/java/com/amazonaws/encryptionsdk/keyrings/RawRsaKeyring.java new file mode 100644 index 000000000..486cd26e6 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/keyrings/RawRsaKeyring.java @@ -0,0 +1,62 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.encryptionsdk.keyrings; + +import com.amazonaws.encryptionsdk.EncryptedDataKey; +import com.amazonaws.encryptionsdk.internal.JceKeyCipher; + +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Arrays; + +/** + * A {@link Keyring} which does local RSA encryption and decryption of data keys using the + * provided public and private keys. + *

+ * Instantiate by using the {@code StandardKeyrings.rawRsa(...)} factory method. + */ +class RawRsaKeyring extends RawKeyring { + + RawRsaKeyring(String keyNamespace, String keyName, PublicKey publicKey, PrivateKey privateKey, String transformation) { + super(keyNamespace, keyName, JceKeyCipher.rsa(publicKey, privateKey, transformation)); + } + + @Override + boolean validToDecrypt(EncryptedDataKey encryptedDataKey) { + + // the key provider ID of the encrypted data key must + // have a value equal to this keyring's key namespace. + if (!keyNamespace.equals(encryptedDataKey.getProviderId())) { + return false; + } + + // the encrypted data key's key provider information + // must have a value equal to this keyring's key name. + if (!Arrays.equals(encryptedDataKey.getProviderInformation(), keyNameBytes)) { + return false; + } + + return true; + } + + @Override + KeyringTraceEntry traceOnEncrypt() { + return new KeyringTraceEntry(keyNamespace, keyName, KeyringTraceFlag.ENCRYPTED_DATA_KEY); + } + + @Override + KeyringTraceEntry traceOnDecrypt() { + return new KeyringTraceEntry(keyNamespace, keyName, KeyringTraceFlag.DECRYPTED_DATA_KEY); + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/keyrings/StandardKeyrings.java b/src/main/java/com/amazonaws/encryptionsdk/keyrings/StandardKeyrings.java new file mode 100644 index 000000000..d36dae07e --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/keyrings/StandardKeyrings.java @@ -0,0 +1,56 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.encryptionsdk.keyrings; + +import javax.crypto.SecretKey; +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * Factory methods for instantiating the standard {@code Keyring}s provided by the AWS Encryption SDK. + */ +public class StandardKeyrings { + + private StandardKeyrings() { + } + + /** + * Constructs a {@code Keyring} which does local AES-GCM encryption + * decryption of data keys using the provided wrapping key. + * + * @param keyNamespace A value that, together with the key name, identifies the wrapping key. + * @param keyName A value that, together with the key namespace, identifies the wrapping key. + * @param wrappingKey The AES key input to AES-GCM to encrypt plaintext data keys. + * @return The {@link Keyring} + */ + public static Keyring rawAes(String keyNamespace, String keyName, SecretKey wrappingKey) { + return new RawAesKeyring(keyNamespace, keyName, wrappingKey); + } + + /** + * Constructs a {@code Keyring} which does local RSA encryption and decryption of data keys using the + * provided public and private keys. If {@code privateKey} is {@code null} then the returned {@code Keyring} + * can only be used for encryption. + * + * @param keyNamespace A value that, together with the key name, identifies the wrapping key. + * @param keyName A value that, together with the key namespace, identifies the wrapping key. + * @param publicKey The RSA public key used by this keyring to encrypt data keys. + * @param privateKey The RSA private key used by this keyring to decrypt data keys. + * @param wrappingAlgorithm The RSA algorithm to use with this keyring. + * @return The {@link Keyring} + */ + public static Keyring rawRsa(String keyNamespace, String keyName, PublicKey publicKey, PrivateKey privateKey, String wrappingAlgorithm) { + return new RawRsaKeyring(keyNamespace, keyName, publicKey, privateKey, wrappingAlgorithm); + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/model/DecryptionMaterials.java b/src/main/java/com/amazonaws/encryptionsdk/model/DecryptionMaterials.java index 0c0ba52c7..94423b884 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/model/DecryptionMaterials.java +++ b/src/main/java/com/amazonaws/encryptionsdk/model/DecryptionMaterials.java @@ -3,17 +3,14 @@ import java.security.PublicKey; import com.amazonaws.encryptionsdk.DataKey; -import com.amazonaws.encryptionsdk.keyrings.KeyringTrace; public final class DecryptionMaterials { private final DataKey dataKey; private final PublicKey trailingSignatureKey; - private final KeyringTrace keyringTrace; private DecryptionMaterials(Builder b) { dataKey = b.getDataKey(); trailingSignatureKey = b.getTrailingSignatureKey(); - keyringTrace = b.getKeyringTrace(); } public DataKey getDataKey() { @@ -24,10 +21,6 @@ public PublicKey getTrailingSignatureKey() { return trailingSignatureKey; } - public KeyringTrace getKeyringTrace() { - return keyringTrace; - } - public static Builder newBuilder() { return new Builder(); } @@ -39,12 +32,10 @@ public Builder toBuilder() { public static final class Builder { private DataKey dataKey; private PublicKey trailingSignatureKey; - private KeyringTrace keyringTrace; private Builder(DecryptionMaterials result) { this.dataKey = result.getDataKey(); this.trailingSignatureKey = result.getTrailingSignatureKey(); - this.keyringTrace = result.getKeyringTrace(); } private Builder() {} @@ -67,15 +58,6 @@ public Builder setTrailingSignatureKey(PublicKey trailingSignatureKey) { return this; } - public KeyringTrace getKeyringTrace() { - return keyringTrace; - } - - public Builder setKeyringTrace(KeyringTrace keyringTrace) { - this.keyringTrace = keyringTrace; - return this; - } - public DecryptionMaterials build() { return new DecryptionMaterials(this); } diff --git a/src/main/java/com/amazonaws/encryptionsdk/model/EncryptionMaterials.java b/src/main/java/com/amazonaws/encryptionsdk/model/EncryptionMaterials.java index f9b05a153..1a40d7c36 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/model/EncryptionMaterials.java +++ b/src/main/java/com/amazonaws/encryptionsdk/model/EncryptionMaterials.java @@ -11,7 +11,6 @@ import com.amazonaws.encryptionsdk.CryptoAlgorithm; import com.amazonaws.encryptionsdk.MasterKey; -import com.amazonaws.encryptionsdk.keyrings.KeyringTrace; /** * Contains the cryptographic materials needed for an encryption operation. @@ -25,7 +24,6 @@ public final class EncryptionMaterials { private final SecretKey cleartextDataKey; private final PrivateKey trailingSignatureKey; private final List masterKeys; - private final KeyringTrace keyringTrace; private EncryptionMaterials(Builder b) { this.algorithm = b.algorithm; @@ -34,7 +32,6 @@ private EncryptionMaterials(Builder b) { this.cleartextDataKey = b.cleartextDataKey; this.trailingSignatureKey = b.trailingSignatureKey; this.masterKeys = b.getMasterKeys(); - this.keyringTrace = b.keyringTrace; } public Builder toBuilder() { @@ -103,13 +100,12 @@ public List getMasterKeys() { Objects.equals(encryptedDataKeys, that.encryptedDataKeys) && Objects.equals(cleartextDataKey, that.cleartextDataKey) && Objects.equals(trailingSignatureKey, that.trailingSignatureKey) && - Objects.equals(masterKeys, that.masterKeys) && - Objects.equals(keyringTrace, that.keyringTrace); + Objects.equals(masterKeys, that.masterKeys); } @Override public int hashCode() { return Objects.hash(algorithm, encryptionContext, encryptedDataKeys, cleartextDataKey, trailingSignatureKey, - masterKeys, keyringTrace); + masterKeys); } public static class Builder { @@ -119,7 +115,6 @@ public static class Builder { private SecretKey cleartextDataKey; private PrivateKey trailingSignatureKey; private List masterKeys = Collections.emptyList(); - private KeyringTrace keyringTrace; private Builder() {} @@ -130,7 +125,6 @@ private Builder(EncryptionMaterials r) { cleartextDataKey = r.cleartextDataKey; trailingSignatureKey = r.trailingSignatureKey; setMasterKeys(r.masterKeys); - keyringTrace = r.keyringTrace; } public EncryptionMaterials build() { @@ -190,14 +184,5 @@ public Builder setMasterKeys(List masterKeys) { this.masterKeys = Collections.unmodifiableList(new ArrayList<>(masterKeys)); return this; } - - public KeyringTrace getKeyringTrace() { - return keyringTrace; - } - - public Builder setKeyringTrace(KeyringTrace keyringTrace) { - this.keyringTrace = keyringTrace; - return this; - } } } diff --git a/src/main/java/com/amazonaws/encryptionsdk/model/KeyBlob.java b/src/main/java/com/amazonaws/encryptionsdk/model/KeyBlob.java index c44fd2f8f..dbea9f6b4 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/model/KeyBlob.java +++ b/src/main/java/com/amazonaws/encryptionsdk/model/KeyBlob.java @@ -542,7 +542,7 @@ public int getKeyProviderIdLen() { */ @Override public String getProviderId() { - String s = new String(keyProviderId_, StandardCharsets.UTF_8); + String s = new String(keyProviderId_, PROVIDER_ENCODING); // The following assume statement essentially says that different // calls to the String constructor above, with the same parameters, // result in strings with the same contents. The assumption is @@ -627,7 +627,7 @@ public byte[] getEncryptedDataKey() { //@ assignable \nothing; //@ signals_only AwsCryptoException; public void setKeyProviderId(final String keyProviderId) { - final byte[] keyProviderIdBytes = keyProviderId.getBytes(StandardCharsets.UTF_8); + final byte[] keyProviderIdBytes = keyProviderId.getBytes(PROVIDER_ENCODING); //@ assume Arrays.equalArrays(keyProviderIdBytes, EncryptedDataKey.s2ba(keyProviderId)); if (keyProviderIdBytes.length > Constants.UNSIGNED_SHORT_MAX_VAL) { throw new AwsCryptoException( diff --git a/src/test/java/com/amazonaws/encryptionsdk/keyrings/DecryptionMaterialsTest.java b/src/test/java/com/amazonaws/encryptionsdk/keyrings/DecryptionMaterialsTest.java new file mode 100644 index 000000000..68687d4ad --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/keyrings/DecryptionMaterialsTest.java @@ -0,0 +1,148 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.encryptionsdk.keyrings; + +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.internal.TrailingSignatureAlgorithm; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PublicKey; +import java.util.Collections; +import java.util.Map; + +import static com.amazonaws.encryptionsdk.internal.RandomBytesGenerator.generate; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class DecryptionMaterialsTest { + + private static final CryptoAlgorithm ALGORITHM_SUITE = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384; + private static final Map ENCRYPTION_CONTEXT = Collections.singletonMap("testKey", "testValue"); + private static final KeyringTrace KEYRING_TRACE = new KeyringTrace(); + private static final KeyringTraceEntry KEYRING_TRACE_ENTRY = new KeyringTraceEntry("Namespace", "Name", KeyringTraceFlag.ENCRYPTED_DATA_KEY); + private static final SecretKey PLAINTEXT_DATA_KEY = new SecretKeySpec(generate(ALGORITHM_SUITE.getDataKeyLength()), ALGORITHM_SUITE.getDataKeyAlgo()); + private static PublicKey VERIFICATION_KEY; + + @BeforeAll + static void setup() throws Exception { + + final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + final KeyPair keyPair = TrailingSignatureAlgorithm.forCryptoAlgorithm(ALGORITHM_SUITE).generateKey(); + VERIFICATION_KEY = keyPair.getPublic(); + } + + @Test + void testBuilderNullCryptoAlgorithm() { + assertThrows(NullPointerException.class, () -> DecryptionMaterials.newBuilder(null).build()); + } + + @Test + void testBuilder() { + DecryptionMaterials result = DecryptionMaterials.newBuilder(ALGORITHM_SUITE) + .encryptionContext(ENCRYPTION_CONTEXT) + .keyringTrace(KEYRING_TRACE) + .plaintextDataKey(PLAINTEXT_DATA_KEY) + .verificationKey(VERIFICATION_KEY) + .build(); + + assertEquals(ALGORITHM_SUITE, result.getAlgorithmSuite()); + assertEquals(ENCRYPTION_CONTEXT, result.getEncryptionContext()); + assertEquals(KEYRING_TRACE, result.getKeyringTrace()); + assertEquals(PLAINTEXT_DATA_KEY, result.getPlaintextDataKey()); + assertEquals(VERIFICATION_KEY, result.getVerificationKey()); + } + + @Test + void testInvalidPlaintextDataKey() { + SecretKey wrongLength = new SecretKeySpec(generate(ALGORITHM_SUITE.getDataKeyLength() + 1), ALGORITHM_SUITE.getDataKeyAlgo()); + assertThrows(IllegalArgumentException.class, () -> DecryptionMaterials.newBuilder(ALGORITHM_SUITE) + .plaintextDataKey(wrongLength) + .verificationKey(VERIFICATION_KEY) + .build()); + + SecretKey wrongAlgorithm = new SecretKeySpec(generate(ALGORITHM_SUITE.getDataKeyLength()), "InvalidAlgorithm"); + assertThrows(IllegalArgumentException.class, () -> DecryptionMaterials.newBuilder(ALGORITHM_SUITE) + .plaintextDataKey(wrongAlgorithm) + .verificationKey(VERIFICATION_KEY) + .build()); + + DecryptionMaterials materials = DecryptionMaterials.newBuilder(ALGORITHM_SUITE) + .verificationKey(VERIFICATION_KEY) + .build(); + assertThrows(IllegalArgumentException.class, () -> materials + .setPlaintextDataKey(wrongAlgorithm, KEYRING_TRACE_ENTRY)); + assertThrows(IllegalArgumentException.class, () -> materials + .setPlaintextDataKey(wrongLength, KEYRING_TRACE_ENTRY)); + } + + @Test + void testInvalidVerificationKey() { + assertThrows(IllegalArgumentException.class, () -> DecryptionMaterials.newBuilder(ALGORITHM_SUITE) + .verificationKey(null) + .build()); + assertThrows(IllegalArgumentException.class, () -> DecryptionMaterials.newBuilder(CryptoAlgorithm.ALG_AES_128_GCM_IV12_TAG16_HKDF_SHA256) + .verificationKey(VERIFICATION_KEY) + .build()); + + } + + @Test + void testToBuilder() { + DecryptionMaterials expected = DecryptionMaterials.newBuilder(ALGORITHM_SUITE) + .encryptionContext(ENCRYPTION_CONTEXT) + .keyringTrace(KEYRING_TRACE) + .plaintextDataKey(PLAINTEXT_DATA_KEY) + .verificationKey(VERIFICATION_KEY) + .build(); + + DecryptionMaterials actual = expected.toBuilder().build(); + + assertEquals(expected, actual); + assertNotSame(expected, actual); + } + + @Test + void testSetPlaintextDataKey() { + DecryptionMaterials materials = DecryptionMaterials.newBuilder(ALGORITHM_SUITE) + .verificationKey(VERIFICATION_KEY) + .build(); + + assertThrows(NullPointerException.class, () -> materials.setPlaintextDataKey(null, KEYRING_TRACE_ENTRY)); + assertThrows(NullPointerException.class, () -> materials.setPlaintextDataKey(PLAINTEXT_DATA_KEY, null)); + + materials.setPlaintextDataKey(PLAINTEXT_DATA_KEY, KEYRING_TRACE_ENTRY); + assertEquals(PLAINTEXT_DATA_KEY, materials.getPlaintextDataKey()); + assertEquals(1, materials.getKeyringTrace().getEntries().size()); + assertEquals(KEYRING_TRACE_ENTRY, materials.getKeyringTrace().getEntries().get(0)); + + assertThrows(IllegalStateException.class, () -> materials.setPlaintextDataKey(PLAINTEXT_DATA_KEY, KEYRING_TRACE_ENTRY)); + } + + @Test + void testGetOptionalProperties() { + DecryptionMaterials materials = DecryptionMaterials.newBuilder(CryptoAlgorithm.ALG_AES_128_GCM_IV12_TAG16_HKDF_SHA256) + .build(); + + assertThrows(IllegalStateException.class, materials::getPlaintextDataKey); + assertThrows(IllegalStateException.class, materials::getVerificationKey); + } + +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/keyrings/EncryptionMaterialsTest.java b/src/test/java/com/amazonaws/encryptionsdk/keyrings/EncryptionMaterialsTest.java new file mode 100644 index 000000000..d1c207dbd --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/keyrings/EncryptionMaterialsTest.java @@ -0,0 +1,175 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.encryptionsdk.keyrings; + +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.EncryptedDataKey; +import com.amazonaws.encryptionsdk.internal.TrailingSignatureAlgorithm; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.util.Collections; +import java.util.Map; + +import static com.amazonaws.encryptionsdk.internal.RandomBytesGenerator.generate; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@ExtendWith(MockitoExtension.class) +class EncryptionMaterialsTest { + + private static final CryptoAlgorithm ALGORITHM_SUITE = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384; + private static final Map ENCRYPTION_CONTEXT = Collections.singletonMap("testKey", "testValue"); + private static final KeyringTrace KEYRING_TRACE = new KeyringTrace(); + private static final KeyringTraceEntry KEYRING_TRACE_ENTRY = new KeyringTraceEntry("Namespace", "Name", KeyringTraceFlag.ENCRYPTED_DATA_KEY); + private static final SecretKey PLAINTEXT_DATA_KEY = new SecretKeySpec(generate(ALGORITHM_SUITE.getDataKeyLength()), ALGORITHM_SUITE.getDataKeyAlgo()); + @Mock + private static EncryptedDataKey ENCRYPTED_DATA_KEY; + private static PrivateKey SIGNING_KEY; + + @BeforeAll + static void setup() throws Exception { + + final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + final KeyPair keyPair = TrailingSignatureAlgorithm.forCryptoAlgorithm(ALGORITHM_SUITE).generateKey(); + SIGNING_KEY = keyPair.getPrivate(); + } + + @Test + void testBuilderNullCryptoAlgorithm() { + assertThrows(NullPointerException.class, () -> EncryptionMaterials.newBuilder(null).build()); + } + + @Test + void testBuilder() { + EncryptionMaterials result = EncryptionMaterials.newBuilder(ALGORITHM_SUITE) + .encryptionContext(ENCRYPTION_CONTEXT) + .keyringTrace(KEYRING_TRACE) + .plaintextDataKey(PLAINTEXT_DATA_KEY) + .encryptedDataKeys(Collections.singletonList(ENCRYPTED_DATA_KEY)) + .signingKey(SIGNING_KEY) + .build(); + + assertEquals(ALGORITHM_SUITE, result.getAlgorithmSuite()); + assertEquals(ENCRYPTION_CONTEXT, result.getEncryptionContext()); + assertEquals(KEYRING_TRACE, result.getKeyringTrace()); + assertEquals(PLAINTEXT_DATA_KEY, result.getPlaintextDataKey()); + assertEquals(1, result.getEncryptedDataKeys().size()); + assertEquals(ENCRYPTED_DATA_KEY, result.getEncryptedDataKeys().get(0)); + assertEquals(SIGNING_KEY, result.getSigningKey()); + } + + @Test + void testInvalidPlaintextDataKey() { + SecretKey wrongLength = new SecretKeySpec(generate(ALGORITHM_SUITE.getDataKeyLength() + 1), ALGORITHM_SUITE.getDataKeyAlgo()); + assertThrows(IllegalArgumentException.class, () -> EncryptionMaterials.newBuilder(ALGORITHM_SUITE) + .plaintextDataKey(wrongLength) + .signingKey(SIGNING_KEY) + .build()); + + SecretKey wrongAlgorithm = new SecretKeySpec(generate(ALGORITHM_SUITE.getDataKeyLength()), "InvalidAlgorithm"); + assertThrows(IllegalArgumentException.class, () -> EncryptionMaterials.newBuilder(ALGORITHM_SUITE) + .plaintextDataKey(wrongAlgorithm) + .signingKey(SIGNING_KEY) + .build()); + + EncryptionMaterials materials = EncryptionMaterials.newBuilder(ALGORITHM_SUITE) + .signingKey(SIGNING_KEY) + .build(); + assertThrows(IllegalArgumentException.class, () -> materials + .setPlaintextDataKey(wrongAlgorithm, KEYRING_TRACE_ENTRY)); + assertThrows(IllegalArgumentException.class, () -> materials + .setPlaintextDataKey(wrongLength, KEYRING_TRACE_ENTRY)); + } + + @Test + void testInvalidSigningKey() { + assertThrows(IllegalArgumentException.class, () -> EncryptionMaterials.newBuilder(ALGORITHM_SUITE) + .signingKey(null) + .build()); + assertThrows(IllegalArgumentException.class, () -> EncryptionMaterials.newBuilder(CryptoAlgorithm.ALG_AES_128_GCM_IV12_TAG16_HKDF_SHA256) + .signingKey(SIGNING_KEY) + .build()); + + } + + @Test + void testToBuilder() { + EncryptionMaterials expected = EncryptionMaterials.newBuilder(ALGORITHM_SUITE) + .encryptionContext(ENCRYPTION_CONTEXT) + .keyringTrace(KEYRING_TRACE) + .plaintextDataKey(PLAINTEXT_DATA_KEY) + .encryptedDataKeys(Collections.singletonList(ENCRYPTED_DATA_KEY)) + .signingKey(SIGNING_KEY) + .build(); + + EncryptionMaterials actual = expected.toBuilder().build(); + + assertEquals(expected, actual); + assertNotSame(expected, actual); + } + + @Test + void testAddEncryptedDataKey() { + EncryptionMaterials materials = EncryptionMaterials.newBuilder(ALGORITHM_SUITE) + .signingKey(SIGNING_KEY) + .build(); + + assertThrows(NullPointerException.class, () -> materials.addEncryptedDataKey(null, KEYRING_TRACE_ENTRY)); + assertThrows(NullPointerException.class, () -> materials.addEncryptedDataKey(ENCRYPTED_DATA_KEY, null)); + + materials.addEncryptedDataKey(ENCRYPTED_DATA_KEY, KEYRING_TRACE_ENTRY); + assertEquals(1, materials.getEncryptedDataKeys().size()); + assertEquals(ENCRYPTED_DATA_KEY, materials.getEncryptedDataKeys().get(0)); + assertEquals(1, materials.getKeyringTrace().getEntries().size()); + assertEquals(KEYRING_TRACE_ENTRY, materials.getKeyringTrace().getEntries().get(0)); + } + + @Test + void testSetPlaintextDataKey() { + EncryptionMaterials materials = EncryptionMaterials.newBuilder(ALGORITHM_SUITE) + .signingKey(SIGNING_KEY) + .build(); + + assertThrows(NullPointerException.class, () -> materials.setPlaintextDataKey(null, KEYRING_TRACE_ENTRY)); + assertThrows(NullPointerException.class, () -> materials.setPlaintextDataKey(PLAINTEXT_DATA_KEY, null)); + + materials.setPlaintextDataKey(PLAINTEXT_DATA_KEY, KEYRING_TRACE_ENTRY); + assertEquals(PLAINTEXT_DATA_KEY, materials.getPlaintextDataKey()); + assertEquals(1, materials.getKeyringTrace().getEntries().size()); + assertEquals(KEYRING_TRACE_ENTRY, materials.getKeyringTrace().getEntries().get(0)); + + assertThrows(IllegalStateException.class, () -> materials.setPlaintextDataKey(PLAINTEXT_DATA_KEY, KEYRING_TRACE_ENTRY)); + } + + @Test + void testGetOptionalProperties() { + EncryptionMaterials materials = EncryptionMaterials.newBuilder(CryptoAlgorithm.ALG_AES_128_GCM_IV12_TAG16_HKDF_SHA256) + .build(); + + assertThrows(IllegalStateException.class, materials::getPlaintextDataKey); + assertThrows(IllegalStateException.class, materials::getSigningKey); + } + +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/keyrings/KeyringTraceTest.java b/src/test/java/com/amazonaws/encryptionsdk/keyrings/KeyringTraceTest.java index c67fcbe77..5b6105070 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/keyrings/KeyringTraceTest.java +++ b/src/test/java/com/amazonaws/encryptionsdk/keyrings/KeyringTraceTest.java @@ -13,22 +13,19 @@ package com.amazonaws.encryptionsdk.keyrings; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static java.util.Collections.singleton; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; -public class KeyringTraceTest { +class KeyringTraceTest { @Test - public void testOrderMaintained() { - KeyringTraceEntry entry1 = new KeyringTraceEntry("ns1", "name1", - singleton(KeyringTraceFlag.GENERATED_DATA_KEY)); - KeyringTraceEntry entry2 = new KeyringTraceEntry("ns2", "name2", - singleton(KeyringTraceFlag.DECRYPTED_DATA_KEY)); - KeyringTraceEntry entry3 = new KeyringTraceEntry("ns3", "name3", - singleton(KeyringTraceFlag.ENCRYPTED_DATA_KEY)); + void testOrderMaintained() { + KeyringTraceEntry entry1 = new KeyringTraceEntry("ns1", "name1", KeyringTraceFlag.GENERATED_DATA_KEY); + KeyringTraceEntry entry2 = new KeyringTraceEntry("ns2", "name2", KeyringTraceFlag.DECRYPTED_DATA_KEY); + KeyringTraceEntry entry3 = new KeyringTraceEntry("ns3", "name3", KeyringTraceFlag.ENCRYPTED_DATA_KEY); KeyringTrace trace = new KeyringTrace(); trace.add(entry1.getKeyNamespace(), entry1.getKeyName(), entry1.getFlags().iterator().next()); @@ -40,23 +37,20 @@ public void testOrderMaintained() { assertEquals(entry3, trace.getEntries().get(2)); } - @Test(expected = UnsupportedOperationException.class) - public void testImmutable() { + @Test + void testImmutable() { KeyringTrace trace = new KeyringTrace(); trace.add("namespace", "name", KeyringTraceFlag.GENERATED_DATA_KEY); - trace.getEntries().add(new KeyringTraceEntry("ns1", "name1", - singleton(KeyringTraceFlag.GENERATED_DATA_KEY))); + assertThrows(UnsupportedOperationException.class, () -> + trace.getEntries().add(new KeyringTraceEntry("ns1", "name1", KeyringTraceFlag.GENERATED_DATA_KEY))); } @Test - public void testKeyringTraceEntryEquals() { - KeyringTraceEntry entry1 = new KeyringTraceEntry("namespace", "name", - singleton(KeyringTraceFlag.GENERATED_DATA_KEY)); - KeyringTraceEntry entry2 = new KeyringTraceEntry(entry1.getKeyNamespace(), entry1.getKeyName(), - entry1.getFlags()); - KeyringTraceEntry entry3 = new KeyringTraceEntry("othernamespace", "name", - singleton(KeyringTraceFlag.GENERATED_DATA_KEY)); + void testKeyringTraceEntryEquals() { + KeyringTraceEntry entry1 = new KeyringTraceEntry("namespace", "name", KeyringTraceFlag.GENERATED_DATA_KEY); + KeyringTraceEntry entry2 = new KeyringTraceEntry(entry1.getKeyNamespace(), entry1.getKeyName(), entry1.getFlags().toArray(new KeyringTraceFlag[]{})); + KeyringTraceEntry entry3 = new KeyringTraceEntry("othernamespace", "name", KeyringTraceFlag.GENERATED_DATA_KEY); assertEquals(entry1, entry1); assertEquals(entry1, entry2); diff --git a/src/test/java/com/amazonaws/encryptionsdk/keyrings/RawAesKeyringTest.java b/src/test/java/com/amazonaws/encryptionsdk/keyrings/RawAesKeyringTest.java new file mode 100644 index 000000000..4183e9a06 --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/keyrings/RawAesKeyringTest.java @@ -0,0 +1,133 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.encryptionsdk.keyrings; + +import com.amazonaws.encryptionsdk.EncryptedDataKey; +import com.amazonaws.encryptionsdk.internal.Utils; +import com.amazonaws.encryptionsdk.model.KeyBlob; +import org.apache.commons.lang3.ArrayUtils; +import org.junit.jupiter.api.Test; + +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; + +import static com.amazonaws.encryptionsdk.internal.RandomBytesGenerator.generate; +import static com.amazonaws.encryptionsdk.keyrings.RawKeyringTest.ALGORITHM; +import static com.amazonaws.encryptionsdk.keyrings.RawKeyringTest.DATA_KEY; +import static com.amazonaws.encryptionsdk.keyrings.RawKeyringTest.ENCRYPTION_CONTEXT; +import static com.amazonaws.encryptionsdk.keyrings.RawKeyringTest.KEYNAME; +import static com.amazonaws.encryptionsdk.keyrings.RawKeyringTest.KEYNAMESPACE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class RawAesKeyringTest { + + private final RawAesKeyring keyring = new RawAesKeyring(KEYNAMESPACE, KEYNAME, new SecretKeySpec(generate(32), "AES")); + + @Test + void testValidToDecrypt() { + assertTrue(keyring.validToDecrypt(new KeyBlob( + KEYNAMESPACE, KEYNAME.getBytes(StandardCharsets.UTF_8), new byte[]{}))); + assertTrue(keyring.validToDecrypt(new KeyBlob( + KEYNAMESPACE, ArrayUtils.add(KEYNAME.getBytes(StandardCharsets.UTF_8), (byte) 5), new byte[]{}))); + //Bad namespace + assertFalse(keyring.validToDecrypt(new KeyBlob( + "WrongNamespace", KEYNAME.getBytes(StandardCharsets.UTF_8), new byte[]{}))); + //Bad provider info + assertFalse(keyring.validToDecrypt(new KeyBlob( + KEYNAMESPACE, new byte[]{1,2,3}, new byte[]{}))); + } + + @Test + void testEncryptDecryptExistingDataKey() { + EncryptionMaterials encryptionMaterials = EncryptionMaterials.newBuilder(ALGORITHM) + .plaintextDataKey(DATA_KEY) + .keyringTrace(new KeyringTrace()) + .encryptionContext(ENCRYPTION_CONTEXT) + .build(); + + keyring.onEncrypt(encryptionMaterials); + + assertEquals(1, encryptionMaterials.getEncryptedDataKeys().size()); + + final EncryptedDataKey actualEncryptedDataKey = encryptionMaterials.getEncryptedDataKeys().get(0); + assertEquals(KEYNAMESPACE, actualEncryptedDataKey.getProviderId()); + assertTrue(Utils.arrayPrefixEquals(keyring.keyNameBytes, actualEncryptedDataKey.getProviderInformation(), keyring.keyNameBytes.length)); + assertTrue(actualEncryptedDataKey.getProviderInformation().length > keyring.keyNameBytes.length); + + assertEquals(1, encryptionMaterials.getKeyringTrace().getEntries().size()); + assertEquals(KEYNAME, encryptionMaterials.getKeyringTrace().getEntries().get(0).getKeyName()); + assertEquals(KEYNAMESPACE, encryptionMaterials.getKeyringTrace().getEntries().get(0).getKeyNamespace()); + assertEquals(2, encryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().size()); + assertTrue(encryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().contains(KeyringTraceFlag.ENCRYPTED_DATA_KEY)); + assertTrue(encryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().contains(KeyringTraceFlag.SIGNED_ENCRYPTION_CONTEXT)); + + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder(ALGORITHM) + .encryptionContext(ENCRYPTION_CONTEXT) + .keyringTrace(new KeyringTrace()) + .build(); + + keyring.onDecrypt(decryptionMaterials, encryptionMaterials.getEncryptedDataKeys()); + + assertEquals(DATA_KEY, decryptionMaterials.getPlaintextDataKey()); + assertEquals(KEYNAME, decryptionMaterials.getKeyringTrace().getEntries().get(0).getKeyName()); + assertEquals(KEYNAMESPACE, decryptionMaterials.getKeyringTrace().getEntries().get(0).getKeyNamespace()); + assertEquals(2, decryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().size()); + assertTrue(decryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().contains(KeyringTraceFlag.DECRYPTED_DATA_KEY)); + assertTrue(decryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().contains(KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT)); + } + + @Test + void testEncryptDecryptGenerateDataKey() { + EncryptionMaterials encryptionMaterials = EncryptionMaterials.newBuilder(ALGORITHM) + .keyringTrace(new KeyringTrace()) + .encryptionContext(ENCRYPTION_CONTEXT) + .build(); + + keyring.onEncrypt(encryptionMaterials); + + assertNotNull(encryptionMaterials.getPlaintextDataKey()); + assertEquals(encryptionMaterials.getPlaintextDataKey().getAlgorithm(), ALGORITHM.getDataKeyAlgo()); + assertEquals(1, encryptionMaterials.getEncryptedDataKeys().size()); + + final EncryptedDataKey actualEncryptedDataKey = encryptionMaterials.getEncryptedDataKeys().get(0); + assertEquals(KEYNAMESPACE, actualEncryptedDataKey.getProviderId()); + assertTrue(Utils.arrayPrefixEquals(keyring.keyNameBytes, actualEncryptedDataKey.getProviderInformation(), keyring.keyNameBytes.length)); + assertTrue(actualEncryptedDataKey.getProviderInformation().length > keyring.keyNameBytes.length); + + assertEquals(2, encryptionMaterials.getKeyringTrace().getEntries().size()); + assertEquals(1, encryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().size()); + assertTrue(encryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().contains(KeyringTraceFlag.GENERATED_DATA_KEY)); + assertEquals(2, encryptionMaterials.getKeyringTrace().getEntries().get(1).getFlags().size()); + assertTrue(encryptionMaterials.getKeyringTrace().getEntries().get(1).getFlags().contains(KeyringTraceFlag.ENCRYPTED_DATA_KEY)); + assertTrue(encryptionMaterials.getKeyringTrace().getEntries().get(1).getFlags().contains(KeyringTraceFlag.SIGNED_ENCRYPTION_CONTEXT)); + + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder(ALGORITHM) + .encryptionContext(ENCRYPTION_CONTEXT) + .keyringTrace(new KeyringTrace()) + .build(); + + keyring.onDecrypt(decryptionMaterials, encryptionMaterials.getEncryptedDataKeys()); + + assertEquals(encryptionMaterials.getPlaintextDataKey(), decryptionMaterials.getPlaintextDataKey()); + assertEquals(KEYNAME, decryptionMaterials.getKeyringTrace().getEntries().get(0).getKeyName()); + assertEquals(KEYNAMESPACE, decryptionMaterials.getKeyringTrace().getEntries().get(0).getKeyNamespace()); + assertEquals(2, decryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().size()); + assertTrue(decryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().contains(KeyringTraceFlag.DECRYPTED_DATA_KEY)); + assertTrue(decryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().contains(KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT)); + } + +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/keyrings/RawKeyringTest.java b/src/test/java/com/amazonaws/encryptionsdk/keyrings/RawKeyringTest.java new file mode 100644 index 000000000..945aa17bc --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/keyrings/RawKeyringTest.java @@ -0,0 +1,214 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.encryptionsdk.keyrings; + +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.EncryptedDataKey; +import com.amazonaws.encryptionsdk.internal.JceKeyCipher; +import com.amazonaws.encryptionsdk.model.KeyBlob; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static com.amazonaws.encryptionsdk.internal.RandomBytesGenerator.generate; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class RawKeyringTest { + + static final String KEYNAME = "testKeyname"; + static final String KEYNAMESPACE = "testKeynamespace"; + static final CryptoAlgorithm ALGORITHM = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256; + static final SecretKey DATA_KEY = new SecretKeySpec(generate(ALGORITHM.getDataKeyLength()), ALGORITHM.getDataKeyAlgo()); + static final Map ENCRYPTION_CONTEXT = Collections.singletonMap("myKey", "myValue"); + private static final EncryptedDataKey ENCRYPTED_DATA_KEY = new KeyBlob("keyProviderId", new byte[]{1, 2, 3}, generate(ALGORITHM.getDataKeyLength())); + private static final EncryptedDataKey INVALID_DATA_KEY = new KeyBlob("invalidProviderId", new byte[]{1, 2, 3}, generate(ALGORITHM.getDataKeyLength())); + private static final KeyringTraceEntry ENCRYPTED_DATA_KEY_TRACE = new KeyringTraceEntry(KEYNAMESPACE, KEYNAME, KeyringTraceFlag.ENCRYPTED_DATA_KEY); + private static final KeyringTraceEntry DECRYPTED_DATA_KEY_TRACE = new KeyringTraceEntry(KEYNAMESPACE, KEYNAME, KeyringTraceFlag.DECRYPTED_DATA_KEY); + private static final KeyringTraceEntry GENERATED_DATA_KEY_TRACE = new KeyringTraceEntry(KEYNAMESPACE, KEYNAME, KeyringTraceFlag.GENERATED_DATA_KEY); + @Mock(lenient = true) private JceKeyCipher jceKeyCipher; + private Keyring keyring; + + @BeforeEach + void setup() throws Exception { + when(jceKeyCipher.encryptKey(DATA_KEY.getEncoded(), KEYNAME, KEYNAMESPACE, ENCRYPTION_CONTEXT)).thenReturn(ENCRYPTED_DATA_KEY); + when(jceKeyCipher.decryptKey(ENCRYPTED_DATA_KEY, KEYNAME, ENCRYPTION_CONTEXT)).thenReturn(DATA_KEY.getEncoded()); + + keyring = new RawKeyring(KEYNAMESPACE, KEYNAME, jceKeyCipher) { + @Override + boolean validToDecrypt(EncryptedDataKey encryptedDataKey) { + return !encryptedDataKey.getProviderId().equals(INVALID_DATA_KEY.getProviderId()); + } + + @Override + KeyringTraceEntry traceOnEncrypt() { + return ENCRYPTED_DATA_KEY_TRACE; + } + + @Override + KeyringTraceEntry traceOnDecrypt() { + return DECRYPTED_DATA_KEY_TRACE; + } + }; + } + + @Test + void testEncryptDecryptExistingDataKey() { + EncryptionMaterials encryptionMaterials = EncryptionMaterials.newBuilder(ALGORITHM) + .plaintextDataKey(DATA_KEY) + .keyringTrace(new KeyringTrace()) + .encryptionContext(ENCRYPTION_CONTEXT) + .build(); + + keyring.onEncrypt(encryptionMaterials); + + assertEquals(1, encryptionMaterials.getEncryptedDataKeys().size()); + assertEncryptedDataKeyEquals(ENCRYPTED_DATA_KEY, encryptionMaterials.getEncryptedDataKeys().get(0)); + assertEquals(1, encryptionMaterials.getKeyringTrace().getEntries().size()); + assertEquals(ENCRYPTED_DATA_KEY_TRACE, encryptionMaterials.getKeyringTrace().getEntries().get(0)); + + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder(ALGORITHM) + .encryptionContext(ENCRYPTION_CONTEXT) + .keyringTrace(new KeyringTrace()) + .build(); + + keyring.onDecrypt(decryptionMaterials, Collections.singletonList(ENCRYPTED_DATA_KEY)); + + assertEquals(DATA_KEY, decryptionMaterials.getPlaintextDataKey()); + assertEquals(DECRYPTED_DATA_KEY_TRACE, decryptionMaterials.getKeyringTrace().getEntries().get(0)); + } + + @Test + void testEncryptNullDataKey() { + EncryptionMaterials encryptionMaterials = EncryptionMaterials.newBuilder(ALGORITHM) + .keyringTrace(new KeyringTrace()) + .encryptionContext(ENCRYPTION_CONTEXT) + .build(); + + ArgumentCaptor dataKeyCaptor = ArgumentCaptor.forClass(byte[].class); + when(jceKeyCipher.encryptKey(dataKeyCaptor.capture(), eq(KEYNAME), eq(KEYNAMESPACE), eq(ENCRYPTION_CONTEXT))).thenReturn(ENCRYPTED_DATA_KEY); + keyring.onEncrypt(encryptionMaterials); + + assertEquals(encryptionMaterials.getPlaintextDataKey().getAlgorithm(), ALGORITHM.getDataKeyAlgo()); + assertArrayEquals(encryptionMaterials.getPlaintextDataKey().getEncoded(), dataKeyCaptor.getValue()); + assertEquals(1, encryptionMaterials.getEncryptedDataKeys().size()); + assertNotNull(encryptionMaterials.getPlaintextDataKey()); + assertEncryptedDataKeyEquals(ENCRYPTED_DATA_KEY, encryptionMaterials.getEncryptedDataKeys().get(0)); + assertEquals(2, encryptionMaterials.getKeyringTrace().getEntries().size()); + assertEquals(GENERATED_DATA_KEY_TRACE, encryptionMaterials.getKeyringTrace().getEntries().get(0)); + assertEquals(ENCRYPTED_DATA_KEY_TRACE, encryptionMaterials.getKeyringTrace().getEntries().get(1)); + } + + @Test + void testDecryptAlreadyDecryptedDataKey() { + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder(ALGORITHM) + .plaintextDataKey(DATA_KEY) + .encryptionContext(ENCRYPTION_CONTEXT) + .keyringTrace(new KeyringTrace()) + .build(); + + keyring.onDecrypt(decryptionMaterials, Collections.singletonList(ENCRYPTED_DATA_KEY)); + + assertEquals(DATA_KEY, decryptionMaterials.getPlaintextDataKey()); + assertEquals(0, decryptionMaterials.getKeyringTrace().getEntries().size()); + } + + @Test + void testDecryptNoValidDataKey() { + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder(ALGORITHM) + .encryptionContext(ENCRYPTION_CONTEXT) + .keyringTrace(new KeyringTrace()) + .build(); + + keyring.onDecrypt(decryptionMaterials, Collections.singletonList(INVALID_DATA_KEY)); + + assertFalse(decryptionMaterials.hasPlaintextDataKey()); + assertEquals(0, decryptionMaterials.getKeyringTrace().getEntries().size()); + } + + @Test + void testDecryptNoDataKey() { + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder(ALGORITHM) + .encryptionContext(ENCRYPTION_CONTEXT) + .keyringTrace(new KeyringTrace()) + .build(); + + keyring.onDecrypt(decryptionMaterials, Collections.emptyList()); + + assertFalse(decryptionMaterials.hasPlaintextDataKey()); + assertEquals(0, decryptionMaterials.getKeyringTrace().getEntries().size()); + } + + + @Test + void testDecryptMultipleKeysOneInvalid() { + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder(ALGORITHM) + .encryptionContext(ENCRYPTION_CONTEXT) + .keyringTrace(new KeyringTrace()) + .build(); + + final List edks = new ArrayList<>(); + edks.add(INVALID_DATA_KEY); + edks.add(ENCRYPTED_DATA_KEY); + + keyring.onDecrypt(decryptionMaterials, edks); + + assertEquals(DATA_KEY, decryptionMaterials.getPlaintextDataKey()); + assertEquals(DECRYPTED_DATA_KEY_TRACE, decryptionMaterials.getKeyringTrace().getEntries().get(0)); + } + + @Test + void testDecryptMultipleKeysOneException() throws GeneralSecurityException { + final EncryptedDataKey BAD_DATA_KEY = new KeyBlob("exceptionProvider", new byte[]{1, 2, 3}, new byte[]{4, 5, 6}); + + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder(ALGORITHM) + .encryptionContext(ENCRYPTION_CONTEXT) + .keyringTrace(new KeyringTrace()) + .build(); + + when(jceKeyCipher.decryptKey(BAD_DATA_KEY, KEYNAME, ENCRYPTION_CONTEXT)) + .thenThrow(new GeneralSecurityException("could not decrypt key")); + + final List edks = new ArrayList<>(); + edks.add(BAD_DATA_KEY); + edks.add(ENCRYPTED_DATA_KEY); + + keyring.onDecrypt(decryptionMaterials, edks); + + assertEquals(DATA_KEY, decryptionMaterials.getPlaintextDataKey()); + assertEquals(DECRYPTED_DATA_KEY_TRACE, decryptionMaterials.getKeyringTrace().getEntries().get(0)); + } + + private void assertEncryptedDataKeyEquals(EncryptedDataKey expected, EncryptedDataKey actual) { + assertEquals(expected.getProviderId(), actual.getProviderId()); + assertArrayEquals(expected.getProviderInformation(), actual.getProviderInformation()); + assertArrayEquals(expected.getEncryptedDataKey(), actual.getEncryptedDataKey()); + } +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/keyrings/RawRsaKeyringTest.java b/src/test/java/com/amazonaws/encryptionsdk/keyrings/RawRsaKeyringTest.java new file mode 100644 index 000000000..6e43d5d37 --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/keyrings/RawRsaKeyringTest.java @@ -0,0 +1,135 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.encryptionsdk.keyrings; + +import com.amazonaws.encryptionsdk.EncryptedDataKey; +import com.amazonaws.encryptionsdk.model.KeyBlob; +import org.apache.commons.lang3.ArrayUtils; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; + +import static com.amazonaws.encryptionsdk.keyrings.RawKeyringTest.ALGORITHM; +import static com.amazonaws.encryptionsdk.keyrings.RawKeyringTest.DATA_KEY; +import static com.amazonaws.encryptionsdk.keyrings.RawKeyringTest.ENCRYPTION_CONTEXT; +import static com.amazonaws.encryptionsdk.keyrings.RawKeyringTest.KEYNAME; +import static com.amazonaws.encryptionsdk.keyrings.RawKeyringTest.KEYNAMESPACE; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class RawRsaKeyringTest { + + private static final String TRANSFORMATION = "RSA/ECB/PKCS1Padding"; + private static RawRsaKeyring keyring; + + @BeforeAll + static void setup() throws Exception { + final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + final KeyPair keyPair = keyPairGenerator.generateKeyPair(); + keyring = new RawRsaKeyring(KEYNAMESPACE, KEYNAME, keyPair.getPublic(), keyPair.getPrivate(), TRANSFORMATION); + } + + @Test + void testValidToDecrypt() { + assertTrue(keyring.validToDecrypt(new KeyBlob( + KEYNAMESPACE, KEYNAME.getBytes(StandardCharsets.UTF_8), new byte[]{}))); + //Provider info has extra data + assertFalse(keyring.validToDecrypt(new KeyBlob( + KEYNAMESPACE, ArrayUtils.add(KEYNAME.getBytes(StandardCharsets.UTF_8), (byte)5), new byte[]{}))); + //Bad namespace + assertFalse(keyring.validToDecrypt(new KeyBlob( + "WrongNamespace", KEYNAME.getBytes(StandardCharsets.UTF_8), new byte[]{}))); + } + + @Test + void testEncryptDecryptExistingDataKey() { + EncryptionMaterials encryptionMaterials = EncryptionMaterials.newBuilder(ALGORITHM) + .plaintextDataKey(DATA_KEY) + .keyringTrace(new KeyringTrace()) + .encryptionContext(ENCRYPTION_CONTEXT) + .build(); + + keyring.onEncrypt(encryptionMaterials); + + assertEquals(1, encryptionMaterials.getEncryptedDataKeys().size()); + + final EncryptedDataKey actualEncryptedDataKey = encryptionMaterials.getEncryptedDataKeys().get(0); + assertEquals(KEYNAMESPACE, actualEncryptedDataKey.getProviderId()); + assertArrayEquals(keyring.keyNameBytes, actualEncryptedDataKey.getProviderInformation()); + + assertEquals(1, encryptionMaterials.getKeyringTrace().getEntries().size()); + assertEquals(KEYNAME, encryptionMaterials.getKeyringTrace().getEntries().get(0).getKeyName()); + assertEquals(KEYNAMESPACE, encryptionMaterials.getKeyringTrace().getEntries().get(0).getKeyNamespace()); + assertEquals(1, encryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().size()); + assertTrue(encryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().contains(KeyringTraceFlag.ENCRYPTED_DATA_KEY)); + + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder(ALGORITHM) + .encryptionContext(ENCRYPTION_CONTEXT) + .keyringTrace(new KeyringTrace()) + .build(); + + keyring.onDecrypt(decryptionMaterials, encryptionMaterials.getEncryptedDataKeys()); + + assertEquals(DATA_KEY, decryptionMaterials.getPlaintextDataKey()); + assertEquals(KEYNAME, decryptionMaterials.getKeyringTrace().getEntries().get(0).getKeyName()); + assertEquals(KEYNAMESPACE, decryptionMaterials.getKeyringTrace().getEntries().get(0).getKeyNamespace()); + assertEquals(1, decryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().size()); + assertTrue(decryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().contains(KeyringTraceFlag.DECRYPTED_DATA_KEY)); + } + + @Test + void testEncryptDecryptGenerateDataKey() { + EncryptionMaterials encryptionMaterials = EncryptionMaterials.newBuilder(ALGORITHM) + .keyringTrace(new KeyringTrace()) + .encryptionContext(ENCRYPTION_CONTEXT) + .build(); + + keyring.onEncrypt(encryptionMaterials); + + assertNotNull(encryptionMaterials.getPlaintextDataKey()); + assertEquals(encryptionMaterials.getPlaintextDataKey().getAlgorithm(), ALGORITHM.getDataKeyAlgo()); + assertEquals(1, encryptionMaterials.getEncryptedDataKeys().size()); + + final EncryptedDataKey actualEncryptedDataKey = encryptionMaterials.getEncryptedDataKeys().get(0); + assertEquals(KEYNAMESPACE, actualEncryptedDataKey.getProviderId()); + assertArrayEquals(keyring.keyNameBytes, actualEncryptedDataKey.getProviderInformation()); + + assertEquals(2, encryptionMaterials.getKeyringTrace().getEntries().size()); + assertEquals(1, encryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().size()); + assertTrue(encryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().contains(KeyringTraceFlag.GENERATED_DATA_KEY)); + assertEquals(1, encryptionMaterials.getKeyringTrace().getEntries().get(1).getFlags().size()); + assertTrue(encryptionMaterials.getKeyringTrace().getEntries().get(1).getFlags().contains(KeyringTraceFlag.ENCRYPTED_DATA_KEY)); + + DecryptionMaterials decryptionMaterials = DecryptionMaterials.newBuilder(ALGORITHM) + .encryptionContext(ENCRYPTION_CONTEXT) + .keyringTrace(new KeyringTrace()) + .build(); + + keyring.onDecrypt(decryptionMaterials, encryptionMaterials.getEncryptedDataKeys()); + + assertEquals(encryptionMaterials.getPlaintextDataKey(), decryptionMaterials.getPlaintextDataKey()); + assertEquals(KEYNAME, decryptionMaterials.getKeyringTrace().getEntries().get(0).getKeyName()); + assertEquals(KEYNAMESPACE, decryptionMaterials.getKeyringTrace().getEntries().get(0).getKeyNamespace()); + assertEquals(1, decryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().size()); + assertTrue(decryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().contains(KeyringTraceFlag.DECRYPTED_DATA_KEY)); + } + +}