diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..0b8f497e1 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/test/resources/aws-encryption-sdk-test-vectors"] + path = src/test/resources/aws-encryption-sdk-test-vectors + url = https://github.com/awslabs/private-aws-encryption-sdk-test-vectors-staging.git diff --git a/CHANGELOG.md b/CHANGELOG.md index fde413990..c1d196c08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.9.0 -- 2021-05-27 + +* feat: Improvements to the message decryption process. + + See https://github.com/aws/aws-encryption-sdk-java/security/advisories/GHSA-55xh-53m6-936r + ## 1.7.0 -- 2020-09-24 * feat: Updates to the AWS Encryption SDK. bdb31dc diff --git a/README.md b/README.md index 07d6d0e82..7c71b5f8e 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ You can get the latest release from Maven: com.amazonaws aws-encryption-sdk-java - 1.7.0 + 1.9.0 ``` diff --git a/codebuild/corretto11.yml b/codebuild/corretto11.yml index f6d492de7..e065929cf 100644 --- a/codebuild/corretto11.yml +++ b/codebuild/corretto11.yml @@ -6,4 +6,4 @@ phases: java: corretto11 build: commands: - - mvn install -Dgpg.skip=true '-DtestVectorZip=https://github.com/awslabs/aws-encryption-sdk-test-vectors/raw/master/vectors/awses-decrypt/python-1.3.8.zip' \ No newline at end of file + - mvn install -Dgpg.skip=true "-DtestVectorZip=file://$CODEBUILD_SRC_DIR/src/test/resources/aws-encryption-sdk-test-vectors/vectors/awses-decrypt/python-2.2.0.zip" diff --git a/codebuild/corretto8.yml b/codebuild/corretto8.yml index 1124560f0..71e236f59 100644 --- a/codebuild/corretto8.yml +++ b/codebuild/corretto8.yml @@ -6,4 +6,4 @@ phases: java: corretto8 build: commands: - - mvn install -Dgpg.skip=true '-DtestVectorZip=https://github.com/awslabs/aws-encryption-sdk-test-vectors/raw/master/vectors/awses-decrypt/python-1.3.8.zip' \ No newline at end of file + - mvn install -Dgpg.skip=true "-DtestVectorZip=file://$CODEBUILD_SRC_DIR/src/test/resources/aws-encryption-sdk-test-vectors/vectors/awses-decrypt/python-2.2.0.zip" diff --git a/codebuild/openjdk11.yml b/codebuild/openjdk11.yml index 03c8e648f..208f52a28 100644 --- a/codebuild/openjdk11.yml +++ b/codebuild/openjdk11.yml @@ -6,4 +6,4 @@ phases: java: openjdk11 build: commands: - - mvn install -Dgpg.skip=true '-DtestVectorZip=https://github.com/awslabs/aws-encryption-sdk-test-vectors/raw/master/vectors/awses-decrypt/python-1.3.8.zip' \ No newline at end of file + - mvn install -Dgpg.skip=true "-DtestVectorZip=file://$CODEBUILD_SRC_DIR/src/test/resources/aws-encryption-sdk-test-vectors/vectors/awses-decrypt/python-2.2.0.zip" diff --git a/codebuild/openjdk8.yml b/codebuild/openjdk8.yml index 7b9be0751..e80b43dd5 100644 --- a/codebuild/openjdk8.yml +++ b/codebuild/openjdk8.yml @@ -6,4 +6,4 @@ phases: java: openjdk8 build: commands: - - mvn install -Dgpg.skip=true '-DtestVectorZip=https://github.com/awslabs/aws-encryption-sdk-test-vectors/raw/master/vectors/awses-decrypt/python-1.3.8.zip' \ No newline at end of file + - mvn install -Dgpg.skip=true "-DtestVectorZip=file://$CODEBUILD_SRC_DIR/src/test/resources/aws-encryption-sdk-test-vectors/vectors/awses-decrypt/python-2.2.0.zip" diff --git a/pom.xml b/pom.xml index ddf5df164..5ccb449df 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.amazonaws aws-encryption-sdk-java - 1.7.0 + 1.9.0 jar aws-encryption-sdk-java @@ -60,9 +60,9 @@ - junit - junit - 4.13.1 + org.junit.vintage + junit-vintage-engine + 5.7.1 test diff --git a/src/examples/java/com/amazonaws/crypto/examples/EscrowedEncryptExample.java b/src/examples/java/com/amazonaws/crypto/examples/EscrowedEncryptExample.java index 8f9ac0676..dbc3f1259 100644 --- a/src/examples/java/com/amazonaws/crypto/examples/EscrowedEncryptExample.java +++ b/src/examples/java/com/amazonaws/crypto/examples/EscrowedEncryptExample.java @@ -3,6 +3,8 @@ package com.amazonaws.crypto.examples; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.security.GeneralSecurityException; @@ -125,10 +127,16 @@ private static void standardDecrypt(final String kmsArn, final String fileName) // use an encryption context. For an example, see the other SDK samples. final FileInputStream in = new FileInputStream(fileName + ".encrypted"); final FileOutputStream out = new FileOutputStream(fileName + ".decrypted"); - final CryptoOutputStream decryptingStream = crypto.createDecryptingStream(provider, out); + // Since we are using a signing algorithm suite, we avoid streaming decryption directly to the output file, + // to ensure that the trailing signature is verified before writing any untrusted plaintext to disk. + final ByteArrayOutputStream plaintextBuffer = new ByteArrayOutputStream(); + final CryptoOutputStream decryptingStream = crypto.createDecryptingStream(provider, plaintextBuffer); IOUtils.copy(in, decryptingStream); in.close(); decryptingStream.close(); + final ByteArrayInputStream plaintextReader = new ByteArrayInputStream(plaintextBuffer.toByteArray()); + IOUtils.copy(plaintextReader, out); + out.close(); } private static void escrowDecrypt(final String fileName) throws Exception { diff --git a/src/examples/java/com/amazonaws/crypto/examples/FileStreamingExample.java b/src/examples/java/com/amazonaws/crypto/examples/FileStreamingExample.java index 37c6436c6..bcf637d95 100644 --- a/src/examples/java/com/amazonaws/crypto/examples/FileStreamingExample.java +++ b/src/examples/java/com/amazonaws/crypto/examples/FileStreamingExample.java @@ -14,6 +14,7 @@ import javax.crypto.spec.SecretKeySpec; import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.CryptoAlgorithm; import com.amazonaws.encryptionsdk.CryptoInputStream; import com.amazonaws.encryptionsdk.MasterKey; import com.amazonaws.encryptionsdk.jce.JceMasterKey; @@ -49,7 +50,12 @@ public static void main(String[] args) throws IOException { // Instantiate the SDK with a specific commitment policy. // ForbidEncryptAllowDecrypt is the only available policy in 1.7.0. - final AwsCrypto crypto = AwsCrypto.builder().withCommitmentPolicy(CommitmentPolicy.ForbidEncryptAllowDecrypt).build(); + // This also chooses to encrypt with an algorithm suite that doesn't include signing for faster decryption, + // since this use case assumes that the contexts that encrypt and decrypt are equally trusted. + final AwsCrypto crypto = AwsCrypto.builder() + .withCommitmentPolicy(CommitmentPolicy.ForbidEncryptAllowDecrypt) + .withEncryptionAlgorithm(CryptoAlgorithm.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY) + .build(); // Create an encryption context to identify this ciphertext Map context = Collections.singletonMap("Example", "FileStreaming"); @@ -65,14 +71,16 @@ public static void main(String[] args) throws IOException { out.close(); // Decrypt the file. Verify the encryption context before returning the plaintext. + // Since we encrypted using an unsigned algorithm suite, we can use the recommended + // createUnsignedMessageDecryptingStream method that only accepts unsigned messages. in = new FileInputStream(srcFile + ".encrypted"); - CryptoInputStream decryptingStream = crypto.createDecryptingStream(masterKey, in); + CryptoInputStream decryptingStream = crypto.createUnsignedMessageDecryptingStream(masterKey, in); // Does it contain the expected encryption context? if (!"FileStreaming".equals(decryptingStream.getCryptoResult().getEncryptionContext().get("Example"))) { throw new IllegalStateException("Bad encryption context"); } - // Return the plaintext data + // Write the plaintext data to disk. out = new FileOutputStream(srcFile + ".decrypted"); IOUtils.copy(decryptingStream, out); decryptingStream.close(); diff --git a/src/main/java/com/amazonaws/encryptionsdk/AwsCrypto.java b/src/main/java/com/amazonaws/encryptionsdk/AwsCrypto.java index baf37a150..a4fd6f6a3 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/AwsCrypto.java +++ b/src/main/java/com/amazonaws/encryptionsdk/AwsCrypto.java @@ -11,12 +11,8 @@ import com.amazonaws.encryptionsdk.exception.AwsCryptoException; import com.amazonaws.encryptionsdk.exception.BadCiphertextException; -import com.amazonaws.encryptionsdk.internal.DecryptionHandler; -import com.amazonaws.encryptionsdk.internal.EncryptionHandler; -import com.amazonaws.encryptionsdk.internal.LazyMessageCryptoHandler; -import com.amazonaws.encryptionsdk.internal.MessageCryptoHandler; -import com.amazonaws.encryptionsdk.internal.ProcessingSummary; -import com.amazonaws.encryptionsdk.internal.Utils; +import com.amazonaws.encryptionsdk.internal.*; +import com.amazonaws.encryptionsdk.model.CiphertextHeaders; import com.amazonaws.encryptionsdk.model.EncryptionMaterials; import com.amazonaws.encryptionsdk.model.EncryptionMaterialsRequest; @@ -88,6 +84,12 @@ public class AwsCrypto { private final CommitmentPolicy commitmentPolicy_; + /** + * The maximum number of encrypted data keys to unwrap (resp. wrap) on decrypt (resp. encrypt), if positive. + * If zero, do not limit EDKs. + */ + private final int maxEncryptedDataKeys_; + /** * @deprecated This constructor implicitly configures the Aws Crypto client with a commitment policy that * allows reading encrypted messages without commitment values. @@ -97,6 +99,7 @@ public class AwsCrypto { @Deprecated public AwsCrypto() { commitmentPolicy_ = CommitmentPolicy.ForbidEncryptAllowDecrypt; + maxEncryptedDataKeys_ = CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS; } private AwsCrypto(Builder builder) { @@ -113,15 +116,24 @@ private AwsCrypto(Builder builder) { encryptionAlgorithm_ = builder.encryptionAlgorithm_; encryptionFrameSize_ = builder.encryptionFrameSize_; commitmentPolicy_ = builder.commitmentPolicy_; + maxEncryptedDataKeys_ = builder.maxEncryptedDataKeys_; } public static class Builder { private CryptoAlgorithm encryptionAlgorithm_; private int encryptionFrameSize_ = getDefaultFrameSize(); private CommitmentPolicy commitmentPolicy_; + private int maxEncryptedDataKeys_ = CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS; private Builder() {} + private Builder(final AwsCrypto client) { + encryptionAlgorithm_ = client.encryptionAlgorithm_; + encryptionFrameSize_ = client.encryptionFrameSize_; + commitmentPolicy_ = client.commitmentPolicy_; + maxEncryptedDataKeys_ = client.maxEncryptedDataKeys_; + } + /** * Sets the {@link CryptoAlgorithm} to encrypt with. * The Aws Crypto client will use the last crypto algorithm set with @@ -161,6 +173,21 @@ public Builder withCommitmentPolicy(CommitmentPolicy commitmentPolicy) { return this; } + /** + * Sets the maximum number of encrypted data keys that this Aws Crypto client will wrap when + * encrypting, or unwrap when decrypting, a single message. + * + * @param maxEncryptedDataKeys The maximum number of encrypted data keys; must be positive + * @return The Builder, for method chaining + */ + public Builder withMaxEncryptedDataKeys(int maxEncryptedDataKeys) { + if (maxEncryptedDataKeys < 1) { + throw new IllegalArgumentException("maxEncryptedDataKeys must be positive"); + } + this.maxEncryptedDataKeys_ = maxEncryptedDataKeys; + return this; + } + public AwsCrypto build() { return new AwsCrypto(this); } @@ -170,6 +197,10 @@ public static Builder builder() { return new Builder(); } + public Builder toBuilder() { + return new Builder(this); + } + /** * Returns the {@link CryptoAlgorithm} to be used for encryption when none is explicitly * selected. Currently it is {@link CryptoAlgorithm#ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384}. @@ -329,9 +360,10 @@ public > CryptoResult encryptData( .setPlaintext(plaintext) .build(); + EncryptionMaterials encryptionMaterials = checkMaxEncryptedDataKeys(checkAlgorithm(materialsManager.getMaterialsForEncrypt(request))); final MessageCryptoHandler cryptoHandler = new EncryptionHandler( getEncryptionFrameSize(), - checkAlgorithm(materialsManager.getMaterialsForEncrypt(request)) + encryptionMaterials ); final int outSizeEstimate = cryptoHandler.estimateOutputSize(plaintext.length); @@ -449,7 +481,7 @@ public > CryptoResult encryptString(final Mast public > CryptoResult decryptData(final MasterKeyProvider provider, final byte[] ciphertext) { return decryptData(Utils.assertNonNull(provider, "provider"), new - ParsedCiphertext(ciphertext)); + ParsedCiphertext(ciphertext, maxEncryptedDataKeys_)); } /** @@ -465,7 +497,7 @@ public > CryptoResult decryptData(final Master final byte[] ciphertext ) { return decryptData(Utils.assertNonNull(materialsManager, "materialsManager"), - new ParsedCiphertext(ciphertext)); + new ParsedCiphertext(ciphertext, maxEncryptedDataKeys_)); } /** @@ -487,7 +519,10 @@ public > CryptoResult decryptData( ) { Utils.assertNonNull(materialsManager, "materialsManager"); - final MessageCryptoHandler cryptoHandler = DecryptionHandler.create(materialsManager, ciphertext); + final MessageCryptoHandler cryptoHandler = + DecryptionHandler.create(materialsManager, ciphertext, commitmentPolicy_, + SignaturePolicy.AllowEncryptAllowDecrypt, + maxEncryptedDataKeys_); final byte[] ciphertextBytes = ciphertext.getCiphertext(); final int contentLen = ciphertextBytes.length - ciphertext.getOffset(); @@ -669,16 +704,92 @@ public CryptoInputStream createEncryptingStream( return createEncryptingStream(materialsManager, is, EMPTY_MAP); } + /** + * Returns a {@link CryptoOutputStream} which decrypts the data prior to passing it onto the + * underlying {@link OutputStream}. This version only accepts unsigned messages. + * + * @see #decryptData(MasterKeyProvider, byte[]) + * @see javax.crypto.CipherOutputStream + */ + public > CryptoOutputStream createUnsignedMessageDecryptingStream( + final MasterKeyProvider provider, final OutputStream os) { + final MessageCryptoHandler cryptoHandler = DecryptionHandler.create(provider, + commitmentPolicy_, + SignaturePolicy.AllowEncryptForbidDecrypt, + maxEncryptedDataKeys_); + return new CryptoOutputStream(os, cryptoHandler); + } + + /** + * Returns a {@link CryptoInputStream} which decrypts the data after reading it from the + * underlying {@link InputStream}. This version only accepts unsigned messages. + * + * @see #decryptData(MasterKeyProvider, byte[]) + * @see javax.crypto.CipherInputStream + */ + public > CryptoInputStream createUnsignedMessageDecryptingStream( + final MasterKeyProvider provider, final InputStream is) { + final MessageCryptoHandler cryptoHandler = DecryptionHandler.create(provider, + commitmentPolicy_, + SignaturePolicy.AllowEncryptForbidDecrypt, + maxEncryptedDataKeys_); + return new CryptoInputStream(is, cryptoHandler); + } + + /** + * Returns a {@link CryptoOutputStream} which decrypts the data prior to passing it onto the + * underlying {@link OutputStream}. This version only accepts unsigned messages. + * + * @see #decryptData(CryptoMaterialsManager, byte[]) + * @see javax.crypto.CipherOutputStream + */ + public CryptoOutputStream createUnsignedMessageDecryptingStream( + final CryptoMaterialsManager materialsManager, final OutputStream os + ) { + final MessageCryptoHandler cryptoHandler = DecryptionHandler.create(materialsManager, + commitmentPolicy_, + SignaturePolicy.AllowEncryptForbidDecrypt, + maxEncryptedDataKeys_); + return new CryptoOutputStream(os, cryptoHandler); + } + + /** + * Returns a {@link CryptoInputStream} which decrypts the data after reading it from the + * underlying {@link InputStream}. This version only accepts unsigned messages. + * + * @see #encryptData(CryptoMaterialsManager, byte[], Map) + * @see javax.crypto.CipherInputStream + */ + public CryptoInputStream createUnsignedMessageDecryptingStream( + final CryptoMaterialsManager materialsManager, final InputStream is + ) { + final MessageCryptoHandler cryptoHandler = DecryptionHandler.create(materialsManager, + commitmentPolicy_, + SignaturePolicy.AllowEncryptForbidDecrypt, + maxEncryptedDataKeys_); + return new CryptoInputStream(is, cryptoHandler); + } + /** * Returns a {@link CryptoOutputStream} which decrypts the data prior to passing it onto the * underlying {@link OutputStream}. + * + * Note that if the encrypted message includes a trailing signature, by necessity it cannot be verified until + * after the decrypted plaintext has been released to the underlying {@link OutputStream}! This behavior can + * be avoided by using the non-streaming #decryptData(MasterKeyProvider, byte[]) method instead, or + * #createUnsignedMessageDecryptingStream(MasterKeyProvider, OutputStream) if you do not need to decrypt + * signed messages. * * @see #decryptData(MasterKeyProvider, byte[]) + * @see #createUnsignedMessageDecryptingStream(MasterKeyProvider, OutputStream) * @see javax.crypto.CipherOutputStream */ public > CryptoOutputStream createDecryptingStream( final MasterKeyProvider provider, final OutputStream os) { - final MessageCryptoHandler cryptoHandler = DecryptionHandler.create(provider); + final MessageCryptoHandler cryptoHandler = DecryptionHandler.create(provider, + commitmentPolicy_, + SignaturePolicy.AllowEncryptAllowDecrypt, + maxEncryptedDataKeys_); return new CryptoOutputStream(os, cryptoHandler); } @@ -686,12 +797,22 @@ public > CryptoOutputStream createDecryptingStream( * Returns a {@link CryptoInputStream} which decrypts the data after reading it from the * underlying {@link InputStream}. * + * Note that if the encrypted message includes a trailing signature, by necessity it cannot be verified until + * after the decrypted plaintext has been produced from the {@link InputStream}! This behavior can + * be avoided by using the non-streaming #decryptData(MasterKeyProvider, byte[]) method instead, or + * #createUnsignedMessageDecryptingStream(MasterKeyProvider, InputStream) if you do not need to decrypt + * signed messages. + * * @see #decryptData(MasterKeyProvider, byte[]) + * @see #createUnsignedMessageDecryptingStream(MasterKeyProvider, InputStream) * @see javax.crypto.CipherInputStream */ public > CryptoInputStream createDecryptingStream( final MasterKeyProvider provider, final InputStream is) { - final MessageCryptoHandler cryptoHandler = DecryptionHandler.create(provider); + final MessageCryptoHandler cryptoHandler = DecryptionHandler.create(provider, + commitmentPolicy_, + SignaturePolicy.AllowEncryptAllowDecrypt, + maxEncryptedDataKeys_); return new CryptoInputStream(is, cryptoHandler); } @@ -699,13 +820,23 @@ public > CryptoInputStream createDecryptingStream( * Returns a {@link CryptoOutputStream} which decrypts the data prior to passing it onto the * underlying {@link OutputStream}. * + * Note that if the encrypted message includes a trailing signature, by necessity it cannot be verified until + * after the decrypted plaintext has been released to the underlying {@link OutputStream}! This behavior can + * be avoided by using the non-streaming #decryptData(CryptoMaterialsManager, byte[]) method instead, or + * #createUnsignedMessageDecryptingStream(CryptoMaterialsManager, OutputStream) if you do not need to decrypt + * signed messages. + * * @see #decryptData(CryptoMaterialsManager, byte[]) + * @see #createUnsignedMessageDecryptingStream(CryptoMaterialsManager, OutputStream) * @see javax.crypto.CipherOutputStream */ public CryptoOutputStream createDecryptingStream( final CryptoMaterialsManager materialsManager, final OutputStream os ) { - final MessageCryptoHandler cryptoHandler = DecryptionHandler.create(materialsManager); + final MessageCryptoHandler cryptoHandler = DecryptionHandler.create(materialsManager, + commitmentPolicy_, + SignaturePolicy.AllowEncryptAllowDecrypt, + maxEncryptedDataKeys_); return new CryptoOutputStream(os, cryptoHandler); } @@ -713,13 +844,23 @@ public CryptoOutputStream createDecryptingStream( * Returns a {@link CryptoInputStream} which decrypts the data after reading it from the * underlying {@link InputStream}. * - * @see #encryptData(CryptoMaterialsManager, byte[], Map) + * Note that if the encrypted message includes a trailing signature, by necessity it cannot be verified until + * after the decrypted plaintext has been produced from the {@link InputStream}! This behavior can + * be avoided by using the non-streaming #decryptData(CryptoMaterialsManager, byte[]) method instead, or + * #createUnsignedMessageDecryptingStream(CryptoMaterialsManager, InputStream) if you do not need to decrypt + * signed messages. + * + * @see #decryptData(CryptoMaterialsManager, byte[]) + * @see #createUnsignedMessageDecryptingStream(CryptoMaterialsManager, InputStream) * @see javax.crypto.CipherInputStream */ public CryptoInputStream createDecryptingStream( final CryptoMaterialsManager materialsManager, final InputStream is ) { - final MessageCryptoHandler cryptoHandler = DecryptionHandler.create(materialsManager); + final MessageCryptoHandler cryptoHandler = DecryptionHandler.create(materialsManager, + commitmentPolicy_, + SignaturePolicy.AllowEncryptAllowDecrypt, + maxEncryptedDataKeys_); return new CryptoInputStream(is, cryptoHandler); } @@ -741,7 +882,7 @@ private MessageCryptoHandler getEncryptingStreamHandler( return new EncryptionHandler( getEncryptionFrameSize(), - checkAlgorithm(materialsManager.getMaterialsForEncrypt(requestBuilder.build())) + checkMaxEncryptedDataKeys(checkAlgorithm(materialsManager.getMaterialsForEncrypt(requestBuilder.build()))) ); }); } @@ -756,4 +897,11 @@ private EncryptionMaterials checkAlgorithm(EncryptionMaterials result) { return result; } + + private EncryptionMaterials checkMaxEncryptedDataKeys(EncryptionMaterials materials) { + if (maxEncryptedDataKeys_ > 0 && materials.getEncryptedDataKeys().size() > maxEncryptedDataKeys_) { + throw new AwsCryptoException("Encrypted data keys exceed maxEncryptedDataKeys"); + } + return materials; + } } diff --git a/src/main/java/com/amazonaws/encryptionsdk/CryptoInputStream.java b/src/main/java/com/amazonaws/encryptionsdk/CryptoInputStream.java index 935e4fbc0..c71dca1e9 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/CryptoInputStream.java +++ b/src/main/java/com/amazonaws/encryptionsdk/CryptoInputStream.java @@ -230,6 +230,8 @@ public int available() throws IOException { * * If the input size set here is exceeded, an exception will be thrown, and the encyption or decryption will fail. * + * If this method is called multiple times, the smallest bound will be used. + * * @param size Maximum input size. */ public void setMaxInputLength(long size) { diff --git a/src/main/java/com/amazonaws/encryptionsdk/CryptoOutputStream.java b/src/main/java/com/amazonaws/encryptionsdk/CryptoOutputStream.java index de17f4a2f..785403152 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/CryptoOutputStream.java +++ b/src/main/java/com/amazonaws/encryptionsdk/CryptoOutputStream.java @@ -161,6 +161,8 @@ public void close() throws IOException, BadCiphertextException { * * If the size set here is exceeded, an exception will be thrown, and the encyption or decryption will fail. * + * If this method is called multiple times, the smallest bound will be used. + * * @param size Maximum input size. */ public void setMaxInputLength(long size) { diff --git a/src/main/java/com/amazonaws/encryptionsdk/DefaultCryptoMaterialsManager.java b/src/main/java/com/amazonaws/encryptionsdk/DefaultCryptoMaterialsManager.java index 7fa1584fb..eda7d95b8 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/DefaultCryptoMaterialsManager.java +++ b/src/main/java/com/amazonaws/encryptionsdk/DefaultCryptoMaterialsManager.java @@ -133,6 +133,8 @@ public DefaultCryptoMaterialsManager(MasterKeyProvider mkp) { } catch (final IllegalStateException ex) { throw new AwsCryptoException(ex); } + } else if (request.getEncryptionContext().containsKey(Constants.EC_PUBLIC_KEY_FIELD)) { + throw new AwsCryptoException("Trailing signature public key found for non-signed algorithm"); } return DecryptionMaterials.newBuilder() diff --git a/src/main/java/com/amazonaws/encryptionsdk/ParsedCiphertext.java b/src/main/java/com/amazonaws/encryptionsdk/ParsedCiphertext.java index 4be303563..13f0926b1 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/ParsedCiphertext.java +++ b/src/main/java/com/amazonaws/encryptionsdk/ParsedCiphertext.java @@ -33,15 +33,28 @@ public class ParsedCiphertext extends CiphertextHeaders { * Parses {@code ciphertext}. Please note that this does not make a defensive copy of * {@code ciphertext} and that any changes made to the backing array will be reflected here as * well. + * + * @param ciphertext The ciphertext to parse + * @param maxEncryptedDataKeys The maximum number of encrypted data keys to parse. + * Zero indicates no maximum. */ - public ParsedCiphertext(final byte[] ciphertext) { + public ParsedCiphertext(final byte[] ciphertext, final int maxEncryptedDataKeys) { ciphertext_ = Utils.assertNonNull(ciphertext, "ciphertext"); - offset_ = deserialize(ciphertext_, 0); + offset_ = deserialize(ciphertext_, 0, maxEncryptedDataKeys); if (!this.isComplete()) { throw new BadCiphertextException("Incomplete ciphertext."); } } + /** + * Parses {@code ciphertext} without enforcing a max EDK count. Please note that this does + * not make a defensive copy of {@code ciphertext} and that any changes made to the + * backing array will be reflected here as well. + */ + public ParsedCiphertext(final byte[] ciphertext) { + this(ciphertext, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); + } + /** * Returns the raw ciphertext backing this object. This is not a defensive copy and so * must not be modified by callers. diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/DecryptionHandler.java b/src/main/java/com/amazonaws/encryptionsdk/internal/DecryptionHandler.java index 8caca55a7..fadba1905 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/DecryptionHandler.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/DecryptionHandler.java @@ -16,12 +16,7 @@ import javax.crypto.Cipher; import javax.crypto.SecretKey; -import com.amazonaws.encryptionsdk.CryptoAlgorithm; -import com.amazonaws.encryptionsdk.CryptoMaterialsManager; -import com.amazonaws.encryptionsdk.DataKey; -import com.amazonaws.encryptionsdk.DefaultCryptoMaterialsManager; -import com.amazonaws.encryptionsdk.MasterKey; -import com.amazonaws.encryptionsdk.MasterKeyProvider; +import com.amazonaws.encryptionsdk.*; import com.amazonaws.encryptionsdk.exception.AwsCryptoException; import com.amazonaws.encryptionsdk.exception.BadCiphertextException; import com.amazonaws.encryptionsdk.model.CiphertextFooters; @@ -44,6 +39,13 @@ */ public class DecryptionHandler> implements MessageCryptoHandler { private final CryptoMaterialsManager materialsManager_; + /** + * The maximum number of encrypted data keys to parse, if positive. + * If zero, do not limit EDKs. + */ + private final int maxEncryptedDataKeys_; + private final CommitmentPolicy commitmentPolicy_; + private final SignaturePolicy signaturePolicy_; private final CiphertextHeaders ciphertextHeaders_; private final CiphertextFooters ciphertextFooters_; @@ -67,22 +69,43 @@ public class DecryptionHandler> implements MessageCryptoH // These ctors are private to ensure type safety - we must ensure construction using a CMM results in a // DecryptionHandler, not a DecryptionHandler, since the CryptoMaterialsManager is not itself // genericized. - private DecryptionHandler(final CryptoMaterialsManager materialsManager) { + private DecryptionHandler(final CryptoMaterialsManager materialsManager, final CommitmentPolicy commitmentPolicy, + final SignaturePolicy signaturePolicy, final int maxEncryptedDataKeys) { Utils.assertNonNull(materialsManager, "materialsManager"); + Utils.assertNonNull(commitmentPolicy, "commitmentPolicy"); + Utils.assertNonNull(signaturePolicy, "signaturePolicy"); this.materialsManager_ = materialsManager; + this.maxEncryptedDataKeys_ = maxEncryptedDataKeys; + this.commitmentPolicy_ = commitmentPolicy; + this.signaturePolicy_ = signaturePolicy; ciphertextHeaders_ = new CiphertextHeaders(); ciphertextFooters_ = new CiphertextFooters(); } - private DecryptionHandler(final CryptoMaterialsManager materialsManager, final CiphertextHeaders headers) + private DecryptionHandler(final CryptoMaterialsManager materialsManager, final CiphertextHeaders headers, + final CommitmentPolicy commitmentPolicy, final SignaturePolicy signaturePolicy, + final int maxEncryptedDataKeys) throws AwsCryptoException { Utils.assertNonNull(materialsManager, "materialsManager"); + Utils.assertNonNull(commitmentPolicy, "commitmentPolicy"); + Utils.assertNonNull(signaturePolicy, "signaturePolicy"); materialsManager_ = materialsManager; ciphertextHeaders_ = headers; + maxEncryptedDataKeys_ = maxEncryptedDataKeys; + commitmentPolicy_ = commitmentPolicy; + signaturePolicy_ = signaturePolicy; ciphertextFooters_ = new CiphertextFooters(); + if (headers instanceof ParsedCiphertext) { + ciphertextBytesSupplied_ = ((ParsedCiphertext)headers).getOffset(); + } else { + // This is a little more expensive, hence the public create(...) methods + // that take a CiphertextHeaders instead of a ParsedCiphertext are + // deprecated. + ciphertextBytesSupplied_ = headers.toByteArray().length; + } readHeaderFields(headers); updateTrailingSignature(headers); } @@ -97,16 +120,62 @@ private DecryptionHandler(final CryptoMaterialsManager materialsManager, final C * @param customerMasterKeyProvider * the master key provider to use in picking a master key from * the key blobs encoded in the provided ciphertext. + * @param commitmentPolicy The commitment policy to enforce during decryption + * @param signaturePolicy The signature policy to enforce during decryption + * @param maxEncryptedDataKeys + * The maximum number of encrypted data keys to unwrap during decryption; zero indicates no maximum * @throws AwsCryptoException * if the master key is null. */ @SuppressWarnings("unchecked") public static > DecryptionHandler create( - final MasterKeyProvider customerMasterKeyProvider + final MasterKeyProvider customerMasterKeyProvider, + final CommitmentPolicy commitmentPolicy, + final SignaturePolicy signaturePolicy, + final int maxEncryptedDataKeys + ) throws AwsCryptoException { + Utils.assertNonNull(customerMasterKeyProvider, "customerMasterKeyProvider"); + + return (DecryptionHandler)create(new DefaultCryptoMaterialsManager(customerMasterKeyProvider), + commitmentPolicy, signaturePolicy, maxEncryptedDataKeys); + } + + /** + * Create a decryption handler using the provided master key and already parsed {@code headers}. + * + *

+ * Note the methods in the provided master key are used in decrypting the encrypted data key + * parsed from the ciphertext headers. + * + * @param customerMasterKeyProvider + * the master key provider to use in picking a master key from the key blobs encoded + * in the provided ciphertext. + * @param headers + * already parsed headers which will not be passed into + * {@link #processBytes(byte[], int, int, byte[], int)} + * @param commitmentPolicy The commitment policy to enforce during decryption + * @param signaturePolicy The signature policy to enforce during decryption + * @param maxEncryptedDataKeys + * The maximum number of encrypted data keys to unwrap during decryption; zero indicates no maximum + * @throws AwsCryptoException + * if the master key is null. + * @deprecated This version may have to recalculate the number of bytes already parsed, which adds + * a performance penalty. Use {@link #create(CryptoMaterialsManager, ParsedCiphertext, + * CommitmentPolicy, SignaturePolicy, int)} instead, which makes the parsed byte count directly + * available instead. + */ + @SuppressWarnings("unchecked") + @Deprecated + public static > DecryptionHandler create( + final MasterKeyProvider customerMasterKeyProvider, final CiphertextHeaders headers, + final CommitmentPolicy commitmentPolicy, + final SignaturePolicy signaturePolicy, + final int maxEncryptedDataKeys ) throws AwsCryptoException { Utils.assertNonNull(customerMasterKeyProvider, "customerMasterKeyProvider"); - return (DecryptionHandler)create(new DefaultCryptoMaterialsManager(customerMasterKeyProvider)); + return (DecryptionHandler) create(new DefaultCryptoMaterialsManager(customerMasterKeyProvider), headers, + commitmentPolicy, signaturePolicy, maxEncryptedDataKeys); } /** @@ -122,16 +191,24 @@ public static > DecryptionHandler create( * @param headers * already parsed headers which will not be passed into * {@link #processBytes(byte[], int, int, byte[], int)} + * @param commitmentPolicy The commitment policy to enforce during decryption + * @param signaturePolicy The signature policy to enforce during decryption + * @param maxEncryptedDataKeys + * The maximum number of encrypted data keys to unwrap during decryption; zero indicates no maximum * @throws AwsCryptoException * if the master key is null. */ @SuppressWarnings("unchecked") public static > DecryptionHandler create( - final MasterKeyProvider customerMasterKeyProvider, final CiphertextHeaders headers + final MasterKeyProvider customerMasterKeyProvider, final ParsedCiphertext headers, + final CommitmentPolicy commitmentPolicy, + final SignaturePolicy signaturePolicy, + final int maxEncryptedDataKeys ) throws AwsCryptoException { Utils.assertNonNull(customerMasterKeyProvider, "customerMasterKeyProvider"); - return (DecryptionHandler) create(new DefaultCryptoMaterialsManager(customerMasterKeyProvider), headers); + return (DecryptionHandler) create(new DefaultCryptoMaterialsManager(customerMasterKeyProvider), headers, + commitmentPolicy, signaturePolicy, maxEncryptedDataKeys); } /** @@ -144,13 +221,54 @@ public static > DecryptionHandler create( * @param materialsManager * the materials manager to use in decrypting the data key from the key blobs encoded * in the provided ciphertext. + * @param commitmentPolicy The commitment policy to enforce during decryption + * @param signaturePolicy The signature policy to enforce during decryption + * @param maxEncryptedDataKeys + * The maximum number of encrypted data keys to unwrap during decryption; zero indicates no maximum + * @throws AwsCryptoException + * if the master key is null. + */ + public static DecryptionHandler create( + final CryptoMaterialsManager materialsManager, + final CommitmentPolicy commitmentPolicy, + final SignaturePolicy signaturePolicy, + final int maxEncryptedDataKeys + ) throws AwsCryptoException { + return new DecryptionHandler(materialsManager, commitmentPolicy, signaturePolicy, maxEncryptedDataKeys); + } + + /** + * Create a decryption handler using the provided materials manager and already parsed {@code headers}. + * + *

+ * Note the methods in the provided materials manager are used in decrypting the encrypted data key + * parsed from the ciphertext headers. + * + * @param materialsManager + * the materials manager to use in decrypting the data key from the key blobs encoded + * in the provided ciphertext. + * @param headers + * already parsed headers which will not be passed into + * {@link #processBytes(byte[], int, int, byte[], int)} + * @param commitmentPolicy The commitment policy to enforce during decryption + * @param signaturePolicy The signature policy to enforce during decryption + * @param maxEncryptedDataKeys + * The maximum number of encrypted data keys to unwrap during decryption; zero indicates no maximum * @throws AwsCryptoException * if the master key is null. + * @deprecated This version may have to recalculate the number of bytes already parsed, which adds + * a performance penalty. Use {@link #create(CryptoMaterialsManager, ParsedCiphertext, + * CommitmentPolicy, SignaturePolicy, int)} instead, which makes the parsed byte count directly + * available instead. */ + @Deprecated public static DecryptionHandler create( - final CryptoMaterialsManager materialsManager + final CryptoMaterialsManager materialsManager, final CiphertextHeaders headers, + final CommitmentPolicy commitmentPolicy, + final SignaturePolicy signaturePolicy, + final int maxEncryptedDataKeys ) throws AwsCryptoException { - return new DecryptionHandler(materialsManager); + return new DecryptionHandler(materialsManager, headers, commitmentPolicy, signaturePolicy, maxEncryptedDataKeys); } /** @@ -166,13 +284,20 @@ public static DecryptionHandler create( * @param headers * already parsed headers which will not be passed into * {@link #processBytes(byte[], int, int, byte[], int)} + * @param commitmentPolicy The commitment policy to enforce during decryption + * @param signaturePolicy The signature policy to enforce during decryption + * @param maxEncryptedDataKeys + * The maximum number of encrypted data keys to unwrap during decryption; zero indicates no maximum * @throws AwsCryptoException * if the master key is null. */ public static DecryptionHandler create( - final CryptoMaterialsManager materialsManager, final CiphertextHeaders headers + final CryptoMaterialsManager materialsManager, final ParsedCiphertext headers, + final CommitmentPolicy commitmentPolicy, + final SignaturePolicy signaturePolicy, + final int maxEncryptedDataKeys ) throws AwsCryptoException { - return new DecryptionHandler(materialsManager, headers); + return new DecryptionHandler(materialsManager, headers, commitmentPolicy, signaturePolicy, maxEncryptedDataKeys); } /** @@ -208,6 +333,15 @@ public ProcessingSummary processBytes(final byte[] in, final int off, final int final int outOff) throws BadCiphertextException, AwsCryptoException { + // We should arguably check if we are already complete_ here as other handlers + // like FrameDecryptionHandler and BlockDecryptionHandler do. + // However, adding that now could potentially break customers who have extra trailing + // bytes in their decryption streams. + // The handlers are also inconsistent in general with this check. Even those that + // do raise an exception here if already complete will not complain if + // a single call to processBytes() completes the message and provides extra trailing bytes: + // in that case they will just indicate that they didn't process the extra bytes instead. + if (len < 0 || off < 0) { throw new AwsCryptoException(String.format( "Invalid values for input offset: %d and length: %d", off, len)); @@ -235,8 +369,8 @@ public ProcessingSummary processBytes(final byte[] in, final int off, final int System.arraycopy(in, off, bytesToParse, unparsedBytes_.length, len); int totalParsedBytes = 0; - if (ciphertextHeadersParsed_ == false) { - totalParsedBytes += ciphertextHeaders_.deserialize(bytesToParse, 0); + if (!ciphertextHeadersParsed_) { + totalParsedBytes += ciphertextHeaders_.deserialize(bytesToParse, 0, maxEncryptedDataKeys_); // When ciphertext headers are complete, we have the data // key and cipher mode to initialize the underlying cipher if (ciphertextHeaders_.isComplete() == true) { @@ -265,7 +399,6 @@ public ProcessingSummary processBytes(final byte[] in, final int off, final int updateTrailingSignature(bytesToParse, totalParsedBytes, contentResult.getBytesProcessed()); actualOutLen = contentResult.getBytesWritten(); totalParsedBytes += contentResult.getBytesProcessed(); - } if (contentCryptoHandler_.isComplete()) { actualOutLen += contentCryptoHandler_.doFinal(out, outOff + actualOutLen); @@ -277,7 +410,13 @@ public ProcessingSummary processBytes(final byte[] in, final int off, final int // the footer of the message. if (cryptoAlgo_.getTrailingSignatureLength() > 0) { totalParsedBytes += ciphertextFooters_.deserialize(bytesToParse, totalParsedBytes); - if (ciphertextFooters_.isComplete() && trailingSig_ != null) { + if (ciphertextFooters_.isComplete()) { + // reset unparsed bytes as parsing of the ciphertext footer is + // complete. + // This isn't strictly necessary since processing any further data + // should be an error. + unparsedBytes_ = new byte[0]; + try { if (!trailingSig_.verify(ciphertextFooters_.getMAuth())) { throw new BadCiphertextException("Bad trailing signature"); @@ -286,6 +425,12 @@ public ProcessingSummary processBytes(final byte[] in, final int off, final int throw new BadCiphertextException("Bad trailing signature", ex); } complete_ = true; + } else { + // If there aren't enough bytes to parse the ciphertext + // footer, we don't have any more bytes to continue parsing. + // But first copy the leftover bytes to unparsed bytes. + unparsedBytes_ = Arrays.copyOfRange(bytesToParse, totalParsedBytes, bytesToParse.length); + return new ProcessingSummary(actualOutLen, len); } } else { complete_ = true; @@ -308,15 +453,20 @@ public ProcessingSummary processBytes(final byte[] in, final int off, final int */ @Override public int doFinal(final byte[] out, final int outOff) throws BadCiphertextException { + // This is an unfortunate special case we have to support for backwards-compatibility. + if (ciphertextBytesSupplied_ == 0) { + return 0; + } + // check if cryptohandler for content has been created. There are cases // when it might not have been created such as when doFinal() is called // before the ciphertext headers are fully received and parsed. if (contentCryptoHandler_ == null) { - return 0; + throw new BadCiphertextException("Unable to process entire ciphertext."); } else { int result = contentCryptoHandler_.doFinal(out, outOff); - if (!ciphertextHeaders_.isComplete() || !contentCryptoHandler_.isComplete()) { + if (!complete_) { throw new BadCiphertextException("Unable to process entire ciphertext."); } @@ -385,7 +535,7 @@ public void setMaxInputLength(long size) { throw Utils.cannotBeNegative("Max input length"); } - if (ciphertextSizeBound_ != -1 && ciphertextSizeBound_ < size) { + if (ciphertextSizeBound_ == -1 || ciphertextSizeBound_ > size) { ciphertextSizeBound_ = size; } @@ -393,6 +543,10 @@ public void setMaxInputLength(long size) { checkSizeBound(0); } + long getMaxInputLength() { + return ciphertextSizeBound_; + } + /** * Check integrity of the header bytes by processing the parsed MAC tag in * the headers through the cipher. @@ -434,6 +588,17 @@ private void readHeaderFields(final CiphertextHeaders ciphertextHeaders) { final byte[] messageId = ciphertextHeaders.getMessageId(); + if (maxEncryptedDataKeys_ > 0 && ciphertextHeaders_.getEncryptedKeyBlobCount() > maxEncryptedDataKeys_) { + throw new AwsCryptoException("Ciphertext encrypted data keys exceed maxEncryptedDataKeys"); + } + + if (!signaturePolicy_.algorithmAllowedForDecrypt(cryptoAlgo_)) { + throw new AwsCryptoException("Configuration conflict. " + + "Cannot decrypt message with ID " + messageId + " because AwsCrypto.createUnsignedMessageDecryptingStream() " + + " accepts only unsigned messages. Algorithm ID was " + + cryptoAlgo_ + "."); + } + encryptionContext_ = ciphertextHeaders.getEncryptionContextMap(); DecryptionMaterialsRequest request = DecryptionMaterialsRequest.newBuilder() diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/EncryptionHandler.java b/src/main/java/com/amazonaws/encryptionsdk/internal/EncryptionHandler.java index 33ab48237..5b7cd983f 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/EncryptionHandler.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/EncryptionHandler.java @@ -354,6 +354,10 @@ private void checkPlaintextSizeLimit(long additionalBytes) { } } + long getMaxInputLength() { + return plaintextByteLimit_; + } + /** * Compute the MAC tag of the header bytes using the provided key, nonce, AAD, and crypto * algorithm identifier. diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/SignaturePolicy.java b/src/main/java/com/amazonaws/encryptionsdk/internal/SignaturePolicy.java new file mode 100644 index 000000000..767cee6d9 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/SignaturePolicy.java @@ -0,0 +1,20 @@ +package com.amazonaws.encryptionsdk.internal; + +import com.amazonaws.encryptionsdk.CryptoAlgorithm; + +public enum SignaturePolicy { + AllowEncryptAllowDecrypt { + @Override + public boolean algorithmAllowedForDecrypt(CryptoAlgorithm algorithm) { + return true; + } + }, + AllowEncryptForbidDecrypt { + @Override + public boolean algorithmAllowedForDecrypt(CryptoAlgorithm algorithm) { + return algorithm.getTrailingSignatureLength() == 0; + } + }; + + public abstract boolean algorithmAllowedForDecrypt(CryptoAlgorithm algorithm); +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/VersionInfo.java b/src/main/java/com/amazonaws/encryptionsdk/internal/VersionInfo.java index 38a16e49c..82aa224f8 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/VersionInfo.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/VersionInfo.java @@ -20,7 +20,7 @@ public class VersionInfo { // incremented for major changes to the implementation public static final String MAJOR_REVISION_NUM = "1"; // incremented for minor changes to the implementation - public static final String MINOR_REVISION_NUM = "7"; + public static final String MINOR_REVISION_NUM = "9"; // incremented for releases containing an immediate bug fix. public static final String BUGFIX_REVISION_NUM = "0"; diff --git a/src/main/java/com/amazonaws/encryptionsdk/model/CiphertextHeaders.java b/src/main/java/com/amazonaws/encryptionsdk/model/CiphertextHeaders.java index fa7dbcd16..c992f9f10 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/model/CiphertextHeaders.java +++ b/src/main/java/com/amazonaws/encryptionsdk/model/CiphertextHeaders.java @@ -35,6 +35,11 @@ * values supplied in the last two fields of the header. */ public class CiphertextHeaders { + /** + * When passed as maxEncryptedDataKeys, indicates that no maximum should be enforced (i.e., any number of EDKs are allowed). + */ + public static final int NO_MAX_ENCRYPTED_DATA_KEYS = 0; + private static final SecureRandom RND = new SecureRandom(); private byte version_ = -1; private byte typeVal_; // don't set this to -1 since Java byte is signed @@ -59,6 +64,7 @@ public class CiphertextHeaders { // internal variables private int currKeyBlobIndex_ = 0; private boolean isComplete_; + private int maxEncryptedDataKeys_ = NO_MAX_ENCRYPTED_DATA_KEYS; /** * Default constructor. @@ -339,6 +345,9 @@ private int parseEncryptedDataKeyCount(final byte[] b, final int off) throws Par if (cipherKeyCount_ < 0) { throw new BadCiphertextException("Invalid cipher key count in ciphertext"); } + if (maxEncryptedDataKeys_ > 0 && cipherKeyCount_ > maxEncryptedDataKeys_) { + throw new AwsCryptoException("Ciphertext encrypted data keys exceed maxEncryptedDataKeys"); + } cipherKeyBlobs_ = Arrays.asList(new KeyBlob[cipherKeyCount_]); return Short.SIZE / Byte.SIZE; } @@ -523,14 +532,18 @@ private int parseComplete(final byte[] b, final int off) throws ParseException { * the byte array to deserialize. * @param off * the offset in the byte array to use for deserialization. + * @param maxEncryptedDataKeys + * the maximum number of EDKs to deserialize; zero indicates no maximum * @return * the number of bytes consumed in deserialization. */ - public int deserialize(final byte[] b, final int off) throws ParseException { + public int deserialize(final byte[] b, final int off, int maxEncryptedDataKeys) throws ParseException { if (b == null) { return 0; } + maxEncryptedDataKeys_ = maxEncryptedDataKeys; + int parsedBytes = 0; try { parsedBytes += parseVersion(b, off + parsedBytes); diff --git a/src/test/java/com/amazonaws/encryptionsdk/AwsCryptoTest.java b/src/test/java/com/amazonaws/encryptionsdk/AwsCryptoTest.java index dc424c42e..ee69bce58 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/AwsCryptoTest.java +++ b/src/test/java/com/amazonaws/encryptionsdk/AwsCryptoTest.java @@ -23,13 +23,17 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import com.amazonaws.encryptionsdk.jce.JceMasterKey; +import com.amazonaws.encryptionsdk.multi.MultipleProviderFactory; +import com.amazonaws.util.IOUtils; import org.junit.Before; import org.junit.Test; @@ -49,7 +53,10 @@ public class AwsCryptoTest { private StaticMasterKey masterKeyProvider; private AwsCrypto encryptionClient_; + private AwsCrypto noMaxEdksClient_; + private AwsCrypto maxEdksClient_; private static final CommitmentPolicy commitmentPolicy = TestUtils.DEFAULT_TEST_COMMITMENT_POLICY; + private static final int MESSAGE_FORMAT_MAX_EDKS = (1 << 16) - 1; @Before public void init() { @@ -57,6 +64,18 @@ public void init() { encryptionClient_ = AwsCrypto.builder().withCommitmentPolicy(CommitmentPolicy.ForbidEncryptAllowDecrypt).build(); encryptionClient_.setEncryptionAlgorithm(CryptoAlgorithm.ALG_AES_128_GCM_IV12_TAG16_HKDF_SHA256); + + noMaxEdksClient_ = AwsCrypto + .builder() + .withCommitmentPolicy(CommitmentPolicy.ForbidEncryptAllowDecrypt) + .withEncryptionAlgorithm(CryptoAlgorithm.ALG_AES_128_GCM_IV12_TAG16_HKDF_SHA256) + .build(); + maxEdksClient_ = AwsCrypto + .builder() + .withMaxEncryptedDataKeys(3) + .withCommitmentPolicy(CommitmentPolicy.ForbidEncryptAllowDecrypt) + .withEncryptionAlgorithm(CryptoAlgorithm.ALG_AES_128_GCM_IV12_TAG16_HKDF_SHA256) + .build(); } private void doEncryptDecrypt(final CryptoAlgorithm cryptoAlg, final int byteSize, final int frameSize) { @@ -950,4 +969,147 @@ public void legacyConstructAndSetCommittingCryptoAlgorithm() throws IOException AwsCrypto client = new AwsCrypto(); client.setEncryptionAlgorithm(setCryptoAlgorithm); } + + @Test(expected = IllegalArgumentException.class) + public void setNegativeMaxEdks() { + AwsCrypto.builder().withMaxEncryptedDataKeys(-1); + } + + @Test(expected = IllegalArgumentException.class) + public void setZeroMaxEdks() { + AwsCrypto.builder().withMaxEncryptedDataKeys(0); + } + + @Test + public void setValidMaxEdks() { + for (final int i : new int[]{1, 10, MESSAGE_FORMAT_MAX_EDKS, MESSAGE_FORMAT_MAX_EDKS + 1, Integer.MAX_VALUE}) { + AwsCrypto.builder().withMaxEncryptedDataKeys(i); + } + } + + private MasterKeyProvider providerWithEdks(int numEdks) { + List> providers = new ArrayList<>(); + for (int i = 0; i < numEdks; i++) { + providers.add(masterKeyProvider); + } + return MultipleProviderFactory.buildMultiProvider(providers); + } + + @Test + public void encryptDecryptWithLessThanMaxEdks() { + MasterKeyProvider provider = providerWithEdks(2); + CryptoResult result = maxEdksClient_.encryptData(provider, new byte[] {1}); + ParsedCiphertext ciphertext = new ParsedCiphertext(result.getResult()); + assertEquals(ciphertext.getEncryptedKeyBlobCount(), 2); + maxEdksClient_.decryptData(provider, ciphertext); + } + + @Test + public void encryptDecryptWithMaxEdks() { + MasterKeyProvider provider = providerWithEdks(3); + CryptoResult result = maxEdksClient_.encryptData(provider, new byte[] {1}); + ParsedCiphertext ciphertext = new ParsedCiphertext(result.getResult()); + assertEquals(ciphertext.getEncryptedKeyBlobCount(), 3); + maxEdksClient_.decryptData(provider, ciphertext); + } + + @Test + public void noEncryptWithMoreThanMaxEdks() { + MasterKeyProvider provider = providerWithEdks(4); + assertThrows(AwsCryptoException.class, "Encrypted data keys exceed maxEncryptedDataKeys", () -> + maxEdksClient_.encryptData(provider, new byte[] {1})); + } + + @Test + public void noDecryptWithMoreThanMaxEdks() { + MasterKeyProvider provider = providerWithEdks(4); + CryptoResult result = noMaxEdksClient_.encryptData(provider, new byte[] {1}); + ParsedCiphertext ciphertext = new ParsedCiphertext(result.getResult()); + assertThrows(AwsCryptoException.class, "Ciphertext encrypted data keys exceed maxEncryptedDataKeys", () -> + maxEdksClient_.decryptData(provider, ciphertext)); + } + + @Test + public void encryptDecryptWithNoMaxEdks() { + MasterKeyProvider provider = providerWithEdks(MESSAGE_FORMAT_MAX_EDKS); + CryptoResult result = noMaxEdksClient_.encryptData(provider, new byte[] {1}); + ParsedCiphertext ciphertext = new ParsedCiphertext(result.getResult()); + assertEquals(ciphertext.getEncryptedKeyBlobCount(), MESSAGE_FORMAT_MAX_EDKS); + noMaxEdksClient_.decryptData(provider, ciphertext); + } + + @Test + public void encryptDecryptStreamWithLessThanMaxEdks() throws IOException { + MasterKeyProvider provider = providerWithEdks(2); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + CryptoOutputStream encryptStream = maxEdksClient_.createEncryptingStream(provider, byteArrayOutputStream); + IOUtils.copy(new ByteArrayInputStream(new byte[] {1}), encryptStream); + encryptStream.close(); + + byte[] ciphertext = byteArrayOutputStream.toByteArray(); + assertEquals(new ParsedCiphertext(ciphertext).getEncryptedKeyBlobCount(), 2); + + byteArrayOutputStream.reset(); + CryptoOutputStream decryptStream = maxEdksClient_.createDecryptingStream(provider, byteArrayOutputStream); + IOUtils.copy(new ByteArrayInputStream(ciphertext), decryptStream); + decryptStream.close(); + } + + @Test + public void encryptDecryptStreamWithMaxEdks() throws IOException { + MasterKeyProvider provider = providerWithEdks(3); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + CryptoOutputStream encryptStream = maxEdksClient_.createEncryptingStream(provider, byteArrayOutputStream); + IOUtils.copy(new ByteArrayInputStream(new byte[] {1}), encryptStream); + encryptStream.close(); + + byte[] ciphertext = byteArrayOutputStream.toByteArray(); + assertEquals(new ParsedCiphertext(ciphertext).getEncryptedKeyBlobCount(), 3); + + byteArrayOutputStream.reset(); + CryptoOutputStream decryptStream = maxEdksClient_.createDecryptingStream(provider, byteArrayOutputStream); + IOUtils.copy(new ByteArrayInputStream(ciphertext), decryptStream); + decryptStream.close(); + } + + @Test + public void noEncryptStreamWithMoreThanMaxEdks() { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + CryptoOutputStream encryptStream = maxEdksClient_.createEncryptingStream(providerWithEdks(4), byteArrayOutputStream); + assertThrows(AwsCryptoException.class, "Encrypted data keys exceed maxEncryptedDataKeys", () -> + IOUtils.copy(new ByteArrayInputStream(new byte[] {1}), encryptStream)); + } + + @Test + public void noDecryptStreamWithMoreThanMaxEdks() throws IOException { + MasterKeyProvider provider = providerWithEdks(4); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + CryptoOutputStream encryptStream = noMaxEdksClient_.createEncryptingStream(provider, byteArrayOutputStream); + IOUtils.copy(new ByteArrayInputStream(new byte[] {1}), encryptStream); + encryptStream.close(); + + byte[] ciphertext = byteArrayOutputStream.toByteArray(); + + byteArrayOutputStream.reset(); + CryptoOutputStream decryptStream = maxEdksClient_.createDecryptingStream(provider, byteArrayOutputStream); + assertThrows(AwsCryptoException.class, "Ciphertext encrypted data keys exceed maxEncryptedDataKeys", () -> + IOUtils.copy(new ByteArrayInputStream(ciphertext), decryptStream)); + } + + @Test + public void encryptDecryptStreamWithNoMaxEdks() throws IOException { + MasterKeyProvider provider = providerWithEdks(MESSAGE_FORMAT_MAX_EDKS); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + CryptoOutputStream encryptStream = noMaxEdksClient_.createEncryptingStream(provider, byteArrayOutputStream); + IOUtils.copy(new ByteArrayInputStream(new byte[] {1}), encryptStream); + encryptStream.close(); + + byte[] ciphertext = byteArrayOutputStream.toByteArray(); + assertEquals(new ParsedCiphertext(ciphertext).getEncryptedKeyBlobCount(), MESSAGE_FORMAT_MAX_EDKS); + + byteArrayOutputStream.reset(); + CryptoOutputStream decryptStream = noMaxEdksClient_.createDecryptingStream(provider, byteArrayOutputStream); + IOUtils.copy(new ByteArrayInputStream(ciphertext), decryptStream); + decryptStream.close(); + } } diff --git a/src/test/java/com/amazonaws/encryptionsdk/CryptoOutputStreamTest.java b/src/test/java/com/amazonaws/encryptionsdk/CryptoOutputStreamTest.java index 39ac91b5d..b23678bbd 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/CryptoOutputStreamTest.java +++ b/src/test/java/com/amazonaws/encryptionsdk/CryptoOutputStreamTest.java @@ -146,12 +146,13 @@ public static Collection encryptDecryptParams() { ArrayList cases = new ArrayList<>(); for (final CryptoAlgorithm cryptoAlg : EnumSet.allOf(CryptoAlgorithm.class)) { - // Only test with crypto algs without commitment, since those - // are the only ones we can encrypt with - if (cryptoAlg.getMessageFormatVersion() != 1) { - continue; - } - final int[] frameSizeToTest = TestUtils.getFrameSizesToTest(cryptoAlg); + // Only test with crypto algs without commitment, since those + // are the only ones we can encrypt with + if (cryptoAlg.getMessageFormatVersion() != 1) { + continue; + } + + final int[] frameSizeToTest = TestUtils.getFrameSizesToTest(cryptoAlg); // iterate over frame size to test for (int i = 0; i < frameSizeToTest.length; i++) { diff --git a/src/test/java/com/amazonaws/encryptionsdk/DecryptionMethod.java b/src/test/java/com/amazonaws/encryptionsdk/DecryptionMethod.java new file mode 100644 index 000000000..54bb4a4aa --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/DecryptionMethod.java @@ -0,0 +1,181 @@ +package com.amazonaws.encryptionsdk; + +import com.amazonaws.encryptionsdk.internal.SignaturePolicy; +import com.amazonaws.encryptionsdk.internal.TestIOUtils; +import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider; + +import java.io.*; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +enum DecryptionMethod { + OneShot { + @Override + public byte[] decryptMessage(AwsCrypto crypto, MasterKeyProvider masterKeyProvider, byte[] ciphertext) throws IOException { + return crypto.decryptData(masterKeyProvider, ciphertext).getResult(); + } + }, + // Note for the record that changing the readLen parameter of copyInStreamToOutStream has minimal + // effect on the actual data flow when copying from a CryptoInputStream: it will always read from the + // underlying input stream with a fixed chunk size (4096 bytes at the time of writing this), independently + // of how many bytes its asked to read of the decryption result. It's still useful to vary the length to + // ensure the buffering in the CryptoInputStream works correctly though. + InputStreamSingleByteChunks { + @Override + public byte[] decryptMessage(AwsCrypto crypto, MasterKeyProvider masterKeyProvider, byte[] ciphertext) throws IOException { + InputStream in = crypto.createDecryptingStream(masterKeyProvider, new ByteArrayInputStream(ciphertext)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + TestIOUtils.copyInStreamToOutStream(in, out, 1); + return out.toByteArray(); + } + }, + InputStreamSmallByteChunks { + @Override + public byte[] decryptMessage(AwsCrypto crypto, MasterKeyProvider masterKeyProvider, byte[] ciphertext) throws IOException { + InputStream in = crypto.createDecryptingStream(masterKeyProvider, new ByteArrayInputStream(ciphertext)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + TestIOUtils.copyInStreamToOutStream(in, out, SMALL_CHUNK_SIZE); + return out.toByteArray(); + } + }, + InputStreamWholeMessageChunks { + @Override + public byte[] decryptMessage(AwsCrypto crypto, MasterKeyProvider masterKeyProvider, byte[] ciphertext) throws IOException { + InputStream in = crypto.createDecryptingStream(masterKeyProvider, new ByteArrayInputStream(ciphertext)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + TestIOUtils.copyInStreamToOutStream(in, out, ciphertext.length); + return out.toByteArray(); + } + }, + UnsignedMessageInputStreamSingleByteChunks { + @Override + public byte[] decryptMessage(AwsCrypto crypto, MasterKeyProvider masterKeyProvider, byte[] ciphertext) throws IOException { + InputStream in = crypto.createUnsignedMessageDecryptingStream(masterKeyProvider, new ByteArrayInputStream(ciphertext)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + TestIOUtils.copyInStreamToOutStream(in, out, 1); + return out.toByteArray(); + } + + @Override + public SignaturePolicy signaturePolicy() { + return SignaturePolicy.AllowEncryptForbidDecrypt; + } + }, + UnsignedMessageInputStreamSmallByteChunks { + @Override + public byte[] decryptMessage(AwsCrypto crypto, MasterKeyProvider masterKeyProvider, byte[] ciphertext) throws IOException { + InputStream in = crypto.createUnsignedMessageDecryptingStream(masterKeyProvider, new ByteArrayInputStream(ciphertext)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + TestIOUtils.copyInStreamToOutStream(in, out, SMALL_CHUNK_SIZE); + return out.toByteArray(); + } + + @Override + public SignaturePolicy signaturePolicy() { + return SignaturePolicy.AllowEncryptForbidDecrypt; + } + }, + UnsignedMessageInputStreamWholeMessageChunks { + @Override + public byte[] decryptMessage(AwsCrypto crypto, MasterKeyProvider masterKeyProvider, byte[] ciphertext) throws IOException { + InputStream in = crypto.createUnsignedMessageDecryptingStream(masterKeyProvider, new ByteArrayInputStream(ciphertext)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + TestIOUtils.copyInStreamToOutStream(in, out, ciphertext.length); + return out.toByteArray(); + } + + @Override + public SignaturePolicy signaturePolicy() { + return SignaturePolicy.AllowEncryptForbidDecrypt; + } + }, + OutputStreamSingleByteChunks { + @Override + public byte[] decryptMessage(AwsCrypto crypto, MasterKeyProvider masterKeyProvider, byte[] ciphertext) throws IOException { + InputStream in = new ByteArrayInputStream(ciphertext); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + OutputStream decryptingOut = crypto.createDecryptingStream(masterKeyProvider, out); + TestIOUtils.copyInStreamToOutStream(in, decryptingOut, 1); + return out.toByteArray(); + } + }, + OutputStreamSmallByteChunks { + @Override + public byte[] decryptMessage(AwsCrypto crypto, MasterKeyProvider masterKeyProvider, byte[] ciphertext) throws IOException { + InputStream in = new ByteArrayInputStream(ciphertext); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + OutputStream decryptingOut = crypto.createDecryptingStream(masterKeyProvider, out); + TestIOUtils.copyInStreamToOutStream(in, decryptingOut, SMALL_CHUNK_SIZE); + return out.toByteArray(); + } + }, + OutputStreamWholeMessageChunks { + @Override + public byte[] decryptMessage(AwsCrypto crypto, MasterKeyProvider masterKeyProvider, byte[] ciphertext) throws IOException { + InputStream in = new ByteArrayInputStream(ciphertext); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + OutputStream decryptingOut = crypto.createDecryptingStream(masterKeyProvider, out); + TestIOUtils.copyInStreamToOutStream(in, decryptingOut, ciphertext.length); + return out.toByteArray(); + } + }, + UnsignedMessageOutputStreamSingleByteChunks { + @Override + public byte[] decryptMessage(AwsCrypto crypto, MasterKeyProvider masterKeyProvider, byte[] ciphertext) throws IOException { + InputStream in = new ByteArrayInputStream(ciphertext); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + OutputStream decryptingOut = crypto.createUnsignedMessageDecryptingStream(masterKeyProvider, out); + TestIOUtils.copyInStreamToOutStream(in, decryptingOut, 1); + return out.toByteArray(); + } + + @Override + public SignaturePolicy signaturePolicy() { + return SignaturePolicy.AllowEncryptForbidDecrypt; + } + }, + UnsignedMessageOutputStreamSmallByteChunks { + @Override + public byte[] decryptMessage(AwsCrypto crypto, MasterKeyProvider masterKeyProvider, byte[] ciphertext) throws IOException { + InputStream in = new ByteArrayInputStream(ciphertext); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + OutputStream decryptingOut = crypto.createUnsignedMessageDecryptingStream(masterKeyProvider, out); + TestIOUtils.copyInStreamToOutStream(in, decryptingOut, SMALL_CHUNK_SIZE); + return out.toByteArray(); + } + + @Override + public SignaturePolicy signaturePolicy() { + return SignaturePolicy.AllowEncryptForbidDecrypt; + } + }, + UnsignedMessageOutputStreamWholeMessageChunks { + @Override + public byte[] decryptMessage(AwsCrypto crypto, MasterKeyProvider masterKeyProvider, byte[] ciphertext) throws IOException { + InputStream in = new ByteArrayInputStream(ciphertext); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + OutputStream decryptingOut = crypto.createUnsignedMessageDecryptingStream(masterKeyProvider, out); + TestIOUtils.copyInStreamToOutStream(in, decryptingOut, ciphertext.length); + return out.toByteArray(); + } + + @Override + public SignaturePolicy signaturePolicy() { + return SignaturePolicy.AllowEncryptForbidDecrypt; + } + }; + + // A semi-arbitrary chunk size just to have at least one non-boundary input, and something + // that will span at least some message segments. + private static final int SMALL_CHUNK_SIZE = 100; + + public abstract byte[] decryptMessage(AwsCrypto crypto, MasterKeyProvider masterKeyProvider, + byte[] ciphertext) throws IOException; + + public SignaturePolicy signaturePolicy() { + return SignaturePolicy.AllowEncryptAllowDecrypt; + } +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/IntegrationTestSuite.java b/src/test/java/com/amazonaws/encryptionsdk/IntegrationTestSuite.java index 44628b0db..efa5b7a18 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/IntegrationTestSuite.java +++ b/src/test/java/com/amazonaws/encryptionsdk/IntegrationTestSuite.java @@ -1,5 +1,6 @@ package com.amazonaws.encryptionsdk; +import com.amazonaws.encryptionsdk.kms.MaxEncryptedDataKeysIntegrationTest; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -9,7 +10,8 @@ @RunWith(Suite.class) @Suite.SuiteClasses({ XCompatKmsDecryptTest.class, - KMSProviderBuilderIntegrationTests.class + KMSProviderBuilderIntegrationTests.class, + MaxEncryptedDataKeysIntegrationTest.class, }) public class IntegrationTestSuite { } diff --git a/src/test/java/com/amazonaws/encryptionsdk/ParsedCiphertextTest.java b/src/test/java/com/amazonaws/encryptionsdk/ParsedCiphertextTest.java index 92ac0145f..7e5c849d1 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/ParsedCiphertextTest.java +++ b/src/test/java/com/amazonaws/encryptionsdk/ParsedCiphertextTest.java @@ -3,21 +3,27 @@ package com.amazonaws.encryptionsdk; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; import com.amazonaws.encryptionsdk.internal.StaticMasterKey; import com.amazonaws.encryptionsdk.model.CiphertextHeaders; +import com.amazonaws.encryptionsdk.multi.MultipleProviderFactory; import org.junit.Before; import org.junit.Test; import com.amazonaws.encryptionsdk.exception.BadCiphertextException; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Arrays; +import static com.amazonaws.encryptionsdk.TestUtils.assertThrows; import static org.junit.Assert.*; import static org.mockito.Mockito.spy; public class ParsedCiphertextTest extends CiphertextHeaders { + private static final int MESSAGE_FORMAT_MAX_EDKS = (1 << 16) - 1; private StaticMasterKey masterKeyProvider; private AwsCrypto encryptionClient_; @@ -82,4 +88,50 @@ public void incompleteCiphertext() { byte[] incompleteCiphertext = Arrays.copyOf(pCt.getCiphertext(), pCt.getOffset() - 1); ParsedCiphertext badPCt = new ParsedCiphertext(incompleteCiphertext); } + + private MasterKeyProvider providerWithEdks(int numEdks) { + List> providers = new ArrayList<>(); + for (int i = 0; i < numEdks; i++) { + providers.add(masterKeyProvider); + } + return MultipleProviderFactory.buildMultiProvider(providers); + } + + @Test + public void lessThanMaxEdks() { + MasterKeyProvider provider = providerWithEdks(2); + CryptoResult result = encryptionClient_.encryptData(provider, new byte[] {1}); + ParsedCiphertext ciphertext = new ParsedCiphertext(result.getResult(), 3); + assertEquals(ciphertext.getEncryptedKeyBlobCount(), 2); + } + + @Test + public void equalToMaxEdks() { + MasterKeyProvider provider = providerWithEdks(3); + CryptoResult result = encryptionClient_.encryptData(provider, new byte[] {1}); + ParsedCiphertext ciphertext = new ParsedCiphertext(result.getResult(), 3); + assertEquals(ciphertext.getEncryptedKeyBlobCount(), 3); + } + + @Test + public void failMoreThanMaxEdks() { + MasterKeyProvider provider = providerWithEdks(4); + CryptoResult result = encryptionClient_.encryptData(provider, new byte[] {1}); + assertThrows(AwsCryptoException.class, "Ciphertext encrypted data keys exceed maxEncryptedDataKeys", () -> + new ParsedCiphertext(result.getResult(), 3)); + } + + @Test + public void noMaxEdks() { + MasterKeyProvider provider = providerWithEdks(MESSAGE_FORMAT_MAX_EDKS); + CryptoResult result = encryptionClient_.encryptData(provider, new byte[] {1}); + + // explicit no-max + ParsedCiphertext ciphertext = new ParsedCiphertext(result.getResult(), CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); + assertEquals(ciphertext.getEncryptedKeyBlobCount(), MESSAGE_FORMAT_MAX_EDKS); + + // implicit no-max + ciphertext = new ParsedCiphertext(result.getResult()); + assertEquals(ciphertext.getEncryptedKeyBlobCount(), MESSAGE_FORMAT_MAX_EDKS); + } } diff --git a/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java b/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java index d94aee148..8f1573258 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java +++ b/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java @@ -6,6 +6,7 @@ import static java.lang.String.format; import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; +import com.amazonaws.encryptionsdk.internal.SignaturePolicy; import com.amazonaws.encryptionsdk.jce.JceMasterKey; import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider; import com.amazonaws.encryptionsdk.multi.MultipleProviderFactory; @@ -38,33 +39,37 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.Callable; import java.util.jar.JarFile; import java.util.zip.ZipEntry; @RunWith(Parameterized.class) public class TestVectorRunner { + + private static final int MANIFEST_VERSION = 2; + // We save the files in memory to avoid repeatedly retrieving them. This won't work if the plaintexts are too // large or numerous private static final Map cachedData = new HashMap<>(); private final String testName; private final TestCase testCase; + private final DecryptionMethod decryptionMethod; - public TestVectorRunner(final String testName, TestCase testCase) { + public TestVectorRunner(final String testName, TestCase testCase, DecryptionMethod decryptionMethod) { this.testName = testName; this.testCase = testCase; + this.decryptionMethod = decryptionMethod; } @Test - public void decrypt() { + public void decrypt() throws Exception { AwsCrypto crypto = AwsCrypto.builder().withCommitmentPolicy(CommitmentPolicy.ForbidEncryptAllowDecrypt).build(); - byte[] plaintext = crypto.decryptData(testCase.mkp, cachedData.get(testCase.ciphertextPath)).getResult(); - final byte[] expectedPlaintext = cachedData.get(testCase.plaintextPath); - - Assert.assertArrayEquals(expectedPlaintext, plaintext); + Callable decryptor = () -> decryptionMethod.decryptMessage(crypto, testCase.mkp, cachedData.get(testCase.ciphertextPath)); + testCase.matcher.Match(decryptor); } - @Parameterized.Parameters(name="Compatibility Test: {0}") + @Parameterized.Parameters(name="Compatibility Test: {0} - {1}") @SuppressWarnings("unchecked") public static Collection data() throws Exception { final String zipPath = System.getProperty("testVectorZip"); @@ -84,7 +89,7 @@ public static Collection data() throws Exception { throw new IllegalArgumentException("Unsupported manifest type: " + metaData.get("type")); } - if (!Integer.valueOf(1).equals(metaData.get("version"))) { + if (!Integer.valueOf(MANIFEST_VERSION).equals(metaData.get("version"))) { throw new IllegalArgumentException("Unsupported manifest version: " + metaData.get("version")); } @@ -98,8 +103,13 @@ public static Collection data() throws Exception { List testCases = new ArrayList<>(); for (Map.Entry> testEntry : ((Map>) manifest.get("tests")).entrySet()) { - testCases.add(new Object[]{testEntry.getKey(), - parseTest(testEntry.getKey(), testEntry.getValue(), keys, jar, kmsProv)}); + String testName = testEntry.getKey(); + TestCase testCase = parseTest(testEntry.getKey(), testEntry.getValue(), keys, jar, kmsProv); + for (DecryptionMethod decryptionMethod : DecryptionMethod.values()) { + if (testCase.signaturePolicy.equals(decryptionMethod.signaturePolicy())) { + testCases.add(new Object[]{testName, testCase, decryptionMethod}); + } + } } return testCases; } @@ -138,8 +148,6 @@ private static void cacheData(JarFile jar, String url) throws IOException { @SuppressWarnings("unchecked") private static TestCase parseTest(String testName, Map data, Map keys, JarFile jar, KmsMasterKeyProvider kmsProv) throws IOException { - final String plaintextUrl = (String) data.get("plaintext"); - cacheData(jar, plaintextUrl); final String ciphertextURL = (String) data.get("ciphertext"); cacheData(jar, ciphertextURL); @@ -189,7 +197,40 @@ private static TestCase parseTest(String testName, Map data, Map } } - return new TestCase(testName, ciphertextURL, plaintextUrl, mks); + @SuppressWarnings("unchecked") + final Map resultSpec = (Map) data.get("result"); + final ResultMatcher matcher = parseResultMatcher(jar, resultSpec); + + String decryptionMethodSpec = (String) data.get("decryption-method"); + SignaturePolicy signaturePolicy = SignaturePolicy.AllowEncryptAllowDecrypt; + if (decryptionMethodSpec != null) { + if ("streaming-unsigned-only".equals(decryptionMethodSpec)) { + signaturePolicy = SignaturePolicy.AllowEncryptForbidDecrypt; + } else { + throw new IllegalArgumentException("Unsupported Decryption Method: " + decryptionMethodSpec); + } + } + + return new TestCase(testName, ciphertextURL, mks, matcher, signaturePolicy); + } + + private static ResultMatcher parseResultMatcher(final JarFile jar, final Map result) throws IOException { + if (result.size() != 1) { + throw new IllegalArgumentException("Unsupported result specification: " + result); + } + Map.Entry pair = result.entrySet().iterator().next(); + if (pair.getKey().equals("output")) { + Map outputSpec = (Map) pair.getValue(); + String plaintextUrl = outputSpec.get("plaintext"); + cacheData(jar, plaintextUrl); + return new OutputResultMatcher(plaintextUrl); + } else if (pair.getKey().equals("error")) { + Map errorSpec = (Map) pair.getValue(); + String errorDescription = errorSpec.get("error-description"); + return new ErrorResultMatcher(errorDescription); + } else { + throw new IllegalArgumentException("Unsupported result specification: " + result); + } } @SuppressWarnings("unchecked") @@ -280,18 +321,55 @@ private KeyEntry(String name, String keyId, String type, Key key) { private static class TestCase { private final String name; private final String ciphertextPath; - private final String plaintextPath; + private final ResultMatcher matcher; private final MasterKeyProvider mkp; + private final SignaturePolicy signaturePolicy; - private TestCase(String name, String ciphertextPath, String plaintextPath, List> mks) { - this(name, ciphertextPath, plaintextPath, MultipleProviderFactory.buildMultiProvider(mks)); + private TestCase(String name, String ciphertextPath, List> mks, ResultMatcher matcher, SignaturePolicy signaturePolicy) { + this(name, ciphertextPath, MultipleProviderFactory.buildMultiProvider(mks), matcher, signaturePolicy); } - private TestCase(String name, String ciphertextPath, String plaintextPath, MasterKeyProvider mkp) { + private TestCase(String name, String ciphertextPath, MasterKeyProvider mkp, ResultMatcher matcher, SignaturePolicy signaturePolicy) { this.name = name; this.ciphertextPath = ciphertextPath; - this.plaintextPath = plaintextPath; + this.matcher = matcher; this.mkp = mkp; + this.signaturePolicy = signaturePolicy; + } + } + + private interface ResultMatcher { + void Match(Callable decryptor) throws Exception; + } + + private static class OutputResultMatcher implements ResultMatcher { + + private final String plaintextPath; + + private OutputResultMatcher(String plaintextPath) { + this.plaintextPath = plaintextPath; + } + + @Override + public void Match(Callable decryptor) throws Exception { + final byte[] plaintext = decryptor.call(); + final byte[] expectedPlaintext = cachedData.get(plaintextPath); + Assert.assertArrayEquals(expectedPlaintext, plaintext); + } + } + + private static class ErrorResultMatcher implements ResultMatcher { + + private final String errorDescription; + + private ErrorResultMatcher(String errorDescription) { + this.errorDescription = errorDescription; + } + + @Override + public void Match(Callable decryptor) { + Assert.assertThrows("Decryption expected to fail (" + errorDescription + ") but succeeded", + Exception.class, decryptor::call); } } } diff --git a/src/test/java/com/amazonaws/encryptionsdk/internal/DecryptionHandlerTest.java b/src/test/java/com/amazonaws/encryptionsdk/internal/DecryptionHandlerTest.java index 220347784..c2a9b3992 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/internal/DecryptionHandlerTest.java +++ b/src/test/java/com/amazonaws/encryptionsdk/internal/DecryptionHandlerTest.java @@ -3,11 +3,16 @@ package com.amazonaws.encryptionsdk.internal; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Map; +import com.amazonaws.encryptionsdk.MasterKeyProvider; import com.amazonaws.encryptionsdk.jce.JceMasterKey; +import com.amazonaws.encryptionsdk.ParsedCiphertext; import com.amazonaws.encryptionsdk.model.CiphertextHeaders; +import com.amazonaws.encryptionsdk.multi.MultipleProviderFactory; import org.junit.Before; import org.junit.Test; @@ -24,10 +29,14 @@ import com.amazonaws.encryptionsdk.model.EncryptionMaterials; import static com.amazonaws.encryptionsdk.TestUtils.assertThrows; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; public class DecryptionHandlerTest { private StaticMasterKey masterKeyProvider_; private final CommitmentPolicy commitmentPolicy = TestUtils.DEFAULT_TEST_COMMITMENT_POLICY; + private final SignaturePolicy signaturePolicy = SignaturePolicy.AllowEncryptAllowDecrypt; @Before public void init() { @@ -36,12 +45,37 @@ public void init() { @Test(expected = NullPointerException.class) public void nullMasterKey() { - DecryptionHandler.create((MasterKey)null); + DecryptionHandler.create((MasterKey)null, commitmentPolicy, signaturePolicy, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); } + @Test + public void nullCommitment() { + final byte[] ciphertext = getTestHeaders(CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, CommitmentPolicy.ForbidEncryptAllowDecrypt); + + assertThrows(NullPointerException.class, () -> + DecryptionHandler.create(masterKeyProvider_, new ParsedCiphertext(ciphertext), + null, signaturePolicy, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS)); + assertThrows(NullPointerException.class, () -> + DecryptionHandler.create(masterKeyProvider_, + null, signaturePolicy, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS)); + } + + @Test + public void nullSignaturePolicy() { + final byte[] ciphertext = getTestHeaders(CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, CommitmentPolicy.ForbidEncryptAllowDecrypt); + + assertThrows(NullPointerException.class, () -> + DecryptionHandler.create(masterKeyProvider_, new ParsedCiphertext(ciphertext), + commitmentPolicy, null, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS)); + assertThrows(NullPointerException.class, () -> + DecryptionHandler.create(masterKeyProvider_, + commitmentPolicy, null, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS)); } + + @Test(expected = AwsCryptoException.class) public void invalidLenProcessBytes() { - final DecryptionHandler decryptionHandler = DecryptionHandler.create(masterKeyProvider_); + final DecryptionHandler decryptionHandler = + DecryptionHandler.create(masterKeyProvider_, commitmentPolicy, signaturePolicy, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); final byte[] in = new byte[1]; final byte[] out = new byte[1]; decryptionHandler.processBytes(in, 0, -1, out, 0); @@ -49,7 +83,7 @@ public void invalidLenProcessBytes() { @Test(expected = AwsCryptoException.class) public void maxLenProcessBytes() { - final DecryptionHandler decryptionHandler = DecryptionHandler.create(masterKeyProvider_); + final DecryptionHandler decryptionHandler = DecryptionHandler.create(masterKeyProvider_, commitmentPolicy, signaturePolicy, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); // Create input of size 3 bytes: 1 byte containing version, 1 byte // containing type, and 1 byte containing half of the algoId short // primitive. Only 1 byte of the algoId is provided because this @@ -64,6 +98,45 @@ public void maxLenProcessBytes() { decryptionHandler.processBytes(in, 0, Integer.MAX_VALUE, out, 0); } + @Test + public void maxInputLength() { + final byte[] testMessage = getTestMessage(TestUtils.DEFAULT_TEST_CRYPTO_ALG, CommitmentPolicy.ForbidEncryptAllowDecrypt); + final byte[] out = new byte[100]; + final DecryptionHandler decryptionHandler = DecryptionHandler.create(masterKeyProvider_, commitmentPolicy, signaturePolicy, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); + decryptionHandler.setMaxInputLength(testMessage.length - 1); + + assertThrows(IllegalStateException.class, () -> + decryptionHandler.processBytes(testMessage, 0, testMessage.length, out, 0)); + } + + @Test + public void maxInputLengthIncludingParsedCiphertext() { + final byte[] testMessage = getTestMessage(TestUtils.DEFAULT_TEST_CRYPTO_ALG, CommitmentPolicy.ForbidEncryptAllowDecrypt); + final byte[] out = new byte[100]; + ParsedCiphertext parsedHeaders = new ParsedCiphertext(testMessage); + final DecryptionHandler decryptionHandler = DecryptionHandler.create(masterKeyProvider_, parsedHeaders, commitmentPolicy, signaturePolicy, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); + decryptionHandler.setMaxInputLength(testMessage.length - 1); + + assertThrows(IllegalStateException.class, () -> + decryptionHandler.processBytes(testMessage, parsedHeaders.getOffset(), testMessage.length - parsedHeaders.getOffset(), + out, 0)); + } + + @Test + public void maxInputLengthIncludingCiphertextHeaders() { + final byte[] testMessage = getTestMessage(TestUtils.DEFAULT_TEST_CRYPTO_ALG, CommitmentPolicy.ForbidEncryptAllowDecrypt); + final byte[] out = new byte[100]; + ParsedCiphertext parsedHeaders = new ParsedCiphertext(testMessage); + CiphertextHeaders headers = new CiphertextHeaders(); + headers.deserialize(parsedHeaders.getCiphertext(), 0, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); + final DecryptionHandler decryptionHandler = DecryptionHandler.create(masterKeyProvider_, headers, commitmentPolicy, signaturePolicy, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); + decryptionHandler.setMaxInputLength(testMessage.length - 1); + + assertThrows(IllegalStateException.class, () -> + decryptionHandler.processBytes(testMessage, parsedHeaders.getOffset(), testMessage.length - parsedHeaders.getOffset(), + out, 0)); + } + @Test(expected = BadCiphertextException.class) public void headerIntegrityFailure() { byte[] ciphertext = getTestHeaders(); @@ -74,7 +147,7 @@ public void headerIntegrityFailure() { ciphertext[5] += 1; // attempt to decrypt with the tampered header. - final DecryptionHandler decryptionHandler = DecryptionHandler.create(masterKeyProvider_); + final DecryptionHandler decryptionHandler = DecryptionHandler.create(masterKeyProvider_, commitmentPolicy, signaturePolicy, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); final int plaintextLen = decryptionHandler.estimateOutputSize(ciphertext.length); final byte[] plaintext = new byte[plaintextLen]; decryptionHandler.processBytes(ciphertext, 0, ciphertext.length, plaintext, 0); @@ -88,7 +161,7 @@ public void invalidVersion() { ciphertext[0] = 0; // NOTE: This will need to be updated should 0 ever be a valid version // attempt to decrypt with the tampered header. - final DecryptionHandler decryptionHandler = DecryptionHandler.create(masterKeyProvider_); + final DecryptionHandler decryptionHandler = DecryptionHandler.create(masterKeyProvider_, commitmentPolicy, signaturePolicy, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); final int plaintextLen = decryptionHandler.estimateOutputSize(ciphertext.length); final byte[] plaintext = new byte[plaintextLen]; decryptionHandler.processBytes(ciphertext, 0, ciphertext.length, plaintext, 0); @@ -101,23 +174,87 @@ public void invalidCMK() { masterKeyProvider_.setKeyId(masterKeyProvider_.getKeyId() + "nonsense"); // attempt to decrypt with the tampered header. - final DecryptionHandler decryptionHandler = DecryptionHandler.create(masterKeyProvider_); + final DecryptionHandler decryptionHandler = DecryptionHandler.create(masterKeyProvider_, commitmentPolicy, signaturePolicy, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); final int plaintextLen = decryptionHandler.estimateOutputSize(ciphertext.length); final byte[] plaintext = new byte[plaintextLen]; decryptionHandler.processBytes(ciphertext, 0, ciphertext.length, plaintext, 0); } + @Test + public void validAlgForSignaturePolicyCreate() { + // ensure we can decrypt non-signing algs with the policy that allows it + final CryptoAlgorithm nonSigningAlg = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256; + final byte[] ciphertext = getTestHeaders(nonSigningAlg, commitmentPolicy); + final DecryptionHandler decryptionHandler = + DecryptionHandler.create(masterKeyProvider_, commitmentPolicy, SignaturePolicy.AllowEncryptAllowDecrypt, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); + // expected plaintext is zero length + final byte[] plaintext = new byte[0]; + ProcessingSummary processingSummary = decryptionHandler.processBytes(ciphertext, 0, ciphertext.length, plaintext, 0); + assertEquals(ciphertext.length, processingSummary.getBytesProcessed()); + assertArrayEquals(new byte[0], plaintext); + } + + @Test + public void invalidAlgForSignaturePolicyCreateWithoutHeaders() { + final CryptoAlgorithm signingAlg = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384; + final byte[] ciphertext = getTestHeaders(signingAlg, commitmentPolicy); + + final DecryptionHandler decryptionHandler = + DecryptionHandler.create(masterKeyProvider_, commitmentPolicy, SignaturePolicy.AllowEncryptForbidDecrypt, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); + final int plaintextLen = decryptionHandler.estimateOutputSize(ciphertext.length); + final byte[] plaintext = new byte[plaintextLen]; + + assertThrows(AwsCryptoException.class, () -> decryptionHandler.processBytes(ciphertext, 0, ciphertext.length, plaintext, 0)); + } + + @Test + public void invalidAlgForSignaturePolicyCreateWithHeaders() { + final CryptoAlgorithm signingAlg = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384; + final byte[] ciphertext = getTestHeaders(signingAlg, commitmentPolicy); + + assertThrows(AwsCryptoException.class, + () -> DecryptionHandler.create(masterKeyProvider_, new ParsedCiphertext(ciphertext), + commitmentPolicy, SignaturePolicy.AllowEncryptForbidDecrypt, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS)); + } + private byte[] getTestHeaders() { - final CryptoAlgorithm cryptoAlgorithm_ = TestUtils.DEFAULT_TEST_CRYPTO_ALG; + return getTestHeaders(1); + } + + private byte[] getTestHeaders(CryptoAlgorithm cryptoAlgorithm, CommitmentPolicy policy) { + return getTestHeaders(cryptoAlgorithm, policy, 1); + } + + private byte[] getTestHeaders(int numEdks) { + return getTestHeaders(TestUtils.DEFAULT_TEST_CRYPTO_ALG, TestUtils.DEFAULT_TEST_COMMITMENT_POLICY, numEdks); + } + + private byte[] getTestHeaders(CryptoAlgorithm cryptoAlgorithm, CommitmentPolicy policy, int numEdks) { + // Note that it's questionable to assume that failing to call doFinal() on the encryption handler + // always results in only outputting the header! + return getTestMessage(cryptoAlgorithm, policy, numEdks, false); + } + + private byte[] getTestMessage(CryptoAlgorithm cryptoAlgorithm, CommitmentPolicy policy) { + return getTestMessage(cryptoAlgorithm, policy, 1, true); + } + + private byte[] getTestMessage(CryptoAlgorithm cryptoAlgorithm, CommitmentPolicy policy, int numEdks, boolean doFinal) { final int frameSize_ = AwsCrypto.getDefaultFrameSize(); final Map encryptionContext = Collections. emptyMap(); final EncryptionMaterialsRequest encryptionMaterialsRequest = EncryptionMaterialsRequest.newBuilder() .setContext(encryptionContext) - .setRequestedAlgorithm(cryptoAlgorithm_) + .setRequestedAlgorithm(cryptoAlgorithm) .build(); - final EncryptionMaterials encryptionMaterials = new DefaultCryptoMaterialsManager(masterKeyProvider_) + List> providers = new ArrayList<>(); + for (int i = 0; i < numEdks; i++) { + providers.add(masterKeyProvider_); + } + MasterKeyProvider provider = MultipleProviderFactory.buildMultiProvider(providers); + + final EncryptionMaterials encryptionMaterials = new DefaultCryptoMaterialsManager(provider) .getMaterialsForEncrypt(encryptionMaterialsRequest); final EncryptionHandler encryptionHandler = new EncryptionHandler(frameSize_, encryptionMaterials); @@ -126,13 +263,16 @@ private byte[] getTestHeaders() { final byte[] in = new byte[0]; final int ciphertextLen = encryptionHandler.estimateOutputSize(in.length); final byte[] ciphertext = new byte[ciphertextLen]; - encryptionHandler.processBytes(in, 0, in.length, ciphertext, 0); + ProcessingSummary summary = encryptionHandler.processBytes(in, 0, in.length, ciphertext, 0); + if (doFinal) { + encryptionHandler.doFinal(ciphertext, summary.getBytesWritten()); + } return ciphertext; } @Test(expected = AwsCryptoException.class) public void invalidOffsetProcessBytes() { - final DecryptionHandler decryptionHandler = DecryptionHandler.create(masterKeyProvider_); + final DecryptionHandler decryptionHandler = DecryptionHandler.create(masterKeyProvider_, commitmentPolicy, signaturePolicy, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); final byte[] in = new byte[1]; final byte[] out = new byte[1]; decryptionHandler.processBytes(in, -1, in.length, out, 0); @@ -143,11 +283,13 @@ public void incompleteCiphertext() { byte[] ciphertext = getTestHeaders(); CiphertextHeaders h = new CiphertextHeaders(); - h.deserialize(ciphertext, 0); + h.deserialize(ciphertext, 0, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); - final DecryptionHandler decryptionHandler = DecryptionHandler.create(masterKeyProvider_); + final DecryptionHandler decryptionHandler = DecryptionHandler.create(masterKeyProvider_, commitmentPolicy, signaturePolicy, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); final byte[] out = new byte[1]; + // Note the " - 1" is a bit deceptive: the ciphertext SHOULD already be incomplete because we + // called getTestHeaders() above, so the whole body is missing! decryptionHandler.processBytes(ciphertext, 0, ciphertext.length - 1, out, 0); decryptionHandler.doFinal(out, 0); } @@ -155,7 +297,27 @@ public void incompleteCiphertext() { @Test public void incompleteCiphertextV2() { byte[] ciphertext = Utils.decodeBase64String(TestUtils.messageWithCommitKeyBase64); - final DecryptionHandler decryptionHandler = DecryptionHandler.create(TestUtils.messageWithCommitKeyMasterKey); + final DecryptionHandler decryptionHandler = DecryptionHandler.create( + TestUtils.messageWithCommitKeyMasterKey, + CommitmentPolicy.ForbidEncryptAllowDecrypt, + signaturePolicy, + CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); + final byte[] out = new byte[1]; + + decryptionHandler.processBytes(ciphertext, 0, ciphertext.length - 1, out, 0); + assertThrows(BadCiphertextException.class, "Unable to process entire ciphertext.", + () -> decryptionHandler.doFinal(out, 0)); + } + + @Test + public void incompleteCiphertextSigned() { + byte[] ciphertext = getTestMessage(CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + CommitmentPolicy.ForbidEncryptAllowDecrypt); + final DecryptionHandler decryptionHandler = DecryptionHandler.create( + masterKeyProvider_, + CommitmentPolicy.ForbidEncryptAllowDecrypt, + signaturePolicy, + CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); final byte[] out = new byte[1]; decryptionHandler.processBytes(ciphertext, 0, ciphertext.length - 1, out, 0); @@ -173,7 +335,11 @@ public void headerV2HeaderIntegrityFailure() { ciphertext[134] += 1; // attempt to decrypt with the tampered header. - final DecryptionHandler decryptionHandler = DecryptionHandler.create(TestUtils.messageWithCommitKeyMasterKey); + final DecryptionHandler decryptionHandler = DecryptionHandler.create( + TestUtils.messageWithCommitKeyMasterKey, + CommitmentPolicy.ForbidEncryptAllowDecrypt, + signaturePolicy, + CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); final int plaintextLen = decryptionHandler.estimateOutputSize(ciphertext.length); final byte[] plaintext = new byte[plaintextLen]; assertThrows(BadCiphertextException.class, "Header integrity check failed", () -> @@ -188,10 +354,170 @@ public void headerV2BodyIntegrityFailure() { ciphertext[ciphertext.length - 1] += 1; // attempt to decrypt with the tampered header. - final DecryptionHandler decryptionHandler = DecryptionHandler.create(TestUtils.messageWithCommitKeyMasterKey); + final DecryptionHandler decryptionHandler = DecryptionHandler.create( + TestUtils.messageWithCommitKeyMasterKey, + CommitmentPolicy.ForbidEncryptAllowDecrypt, + signaturePolicy, + CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); final int plaintextLen = decryptionHandler.estimateOutputSize(ciphertext.length); final byte[] plaintext = new byte[plaintextLen]; assertThrows(BadCiphertextException.class, "Tag mismatch", () -> decryptionHandler.processBytes(ciphertext, 0, ciphertext.length, plaintext, 0)); } + + @Test + public void withLessThanMaxEdks() { + byte[] header = getTestHeaders(2); + final DecryptionHandler decryptionHandler = DecryptionHandler.create(masterKeyProvider_, commitmentPolicy, signaturePolicy, 3); + final int plaintextLen = decryptionHandler.estimateOutputSize(header.length); + final byte[] plaintext = new byte[plaintextLen]; + decryptionHandler.processBytes(header, 0, header.length, plaintext, 0); + } + + @Test + public void withMaxEdks() { + byte[] header = getTestHeaders(3); + final DecryptionHandler decryptionHandler = DecryptionHandler.create(masterKeyProvider_, commitmentPolicy, signaturePolicy, 3); + final int plaintextLen = decryptionHandler.estimateOutputSize(header.length); + final byte[] plaintext = new byte[plaintextLen]; + decryptionHandler.processBytes(header, 0, header.length, plaintext, 0); + } + + @Test + public void withMoreThanMaxEdks() { + byte[] header = getTestHeaders(4); + final DecryptionHandler decryptionHandler = DecryptionHandler.create(masterKeyProvider_, commitmentPolicy, signaturePolicy, 3); + final int plaintextLen = decryptionHandler.estimateOutputSize(header.length); + final byte[] plaintext = new byte[plaintextLen]; + assertThrows(AwsCryptoException.class, "Ciphertext encrypted data keys exceed maxEncryptedDataKeys", () -> + decryptionHandler.processBytes(header, 0, header.length, plaintext, 0) + ); + } + + @Test + public void withNoMaxEdks() { + byte[] header = getTestHeaders(1 << 16 - 1); + final DecryptionHandler decryptionHandler = DecryptionHandler.create(masterKeyProvider_, commitmentPolicy, signaturePolicy, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); + final int plaintextLen = decryptionHandler.estimateOutputSize(header.length); + final byte[] plaintext = new byte[plaintextLen]; + decryptionHandler.processBytes(header, 0, header.length, plaintext, 0); + } + + public void validSignatureAcrossMultipleBlocks() { + byte[] ciphertext = getTestMessage(CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + CommitmentPolicy.ForbidEncryptAllowDecrypt); + final DecryptionHandler decryptionHandler = DecryptionHandler.create( + masterKeyProvider_, + CommitmentPolicy.ForbidEncryptAllowDecrypt, + signaturePolicy, + CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); + final byte[] out = new byte[1]; + + // Parse the header and body + final int headerLength = 388; + decryptionHandler.processBytes(ciphertext, 0, headerLength, out, 0); + + // Parse the footer across two calls + // This used to fail to verify the signature because partial bytes were dropped instead. + // The overall decryption would still succeed because the completeness check in doFinal + // used to not include the footer. + // The number of bytes read in the first chunk is completely arbitrary. The + // parameterized CryptoOutputStreamTest tests covers lots of possible chunk + // sizes much more thoroughly. This is just a very explicit regression unit test for a known + // issue that is now fixed. + final int firstChunkLength = 12; + final int firstChunkOffset = headerLength; + final int secondChunkOffset = headerLength + firstChunkLength; + final int secondChunkLength = ciphertext.length - secondChunkOffset; + decryptionHandler.processBytes(ciphertext, firstChunkOffset, firstChunkLength, out, 0); + decryptionHandler.processBytes(ciphertext, secondChunkOffset, secondChunkLength, out, 0); + decryptionHandler.doFinal(out, 0); + } + + @Test + public void invalidSignatureAcrossMultipleBlocks() { + byte[] ciphertext = getTestMessage(CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + CommitmentPolicy.ForbidEncryptAllowDecrypt); + final DecryptionHandler decryptionHandler = DecryptionHandler.create( + masterKeyProvider_, + CommitmentPolicy.ForbidEncryptAllowDecrypt, + signaturePolicy, + CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); + final byte[] out = new byte[1]; + + // Parse the header and body + decryptionHandler.processBytes(ciphertext, 0, 388, out, 0); + + // Process extra bytes before processing the actual signature bytes. + // This used to actually work because the handler failed to buffer the unparsed bytes + // across calls. To regression test this properly we have to parse the two bytes for the length... + decryptionHandler.processBytes(ciphertext, 388, 2, out, 0); + // ...and after that any bytes fewer than that length would previously be dropped. + decryptionHandler.processBytes(new byte[10], 0, 10, out, 0); + assertThrows(BadCiphertextException.class, "Bad trailing signature", () -> + decryptionHandler.processBytes(ciphertext, 390, ciphertext.length - 390, out, 0)); + } + + @Test + public void setMaxInputLength() { + byte[] ciphertext = getTestMessage(CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + CommitmentPolicy.ForbidEncryptAllowDecrypt); + final DecryptionHandler decryptionHandler = DecryptionHandler.create( + masterKeyProvider_, + CommitmentPolicy.ForbidEncryptAllowDecrypt, + signaturePolicy, + CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); + decryptionHandler.setMaxInputLength(ciphertext.length - 1); + + assertEquals(decryptionHandler.getMaxInputLength(), (long)ciphertext.length - 1); + + final byte[] out = new byte[1]; + assertThrows(IllegalStateException.class, "Ciphertext size exceeds size bound", () -> + decryptionHandler.processBytes(ciphertext, 0, ciphertext.length, out, 0)); + } + + @Test + public void setMaxInputLengthThrowsIfAlreadyOver() { + byte[] ciphertext = getTestMessage(CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + CommitmentPolicy.ForbidEncryptAllowDecrypt); + final DecryptionHandler decryptionHandler = DecryptionHandler.create( + masterKeyProvider_, + CommitmentPolicy.ForbidEncryptAllowDecrypt, + signaturePolicy, + CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); + final byte[] out = new byte[1]; + decryptionHandler.processBytes(ciphertext, 0, ciphertext.length - 1, out, 0); + assertFalse(decryptionHandler.isComplete()); + + assertThrows(IllegalStateException.class, "Ciphertext size exceeds size bound", () -> + decryptionHandler.setMaxInputLength(ciphertext.length - 2)); + } + + @Test + public void setMaxInputLengthAcceptsSmallerValue() { + final DecryptionHandler decryptionHandler = DecryptionHandler.create( + masterKeyProvider_, + CommitmentPolicy.ForbidEncryptAllowDecrypt, + signaturePolicy, + CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); + decryptionHandler.setMaxInputLength(100); + assertEquals(decryptionHandler.getMaxInputLength(), 100); + + decryptionHandler.setMaxInputLength(10); + assertEquals(decryptionHandler.getMaxInputLength(), 10); + } + + @Test + public void setMaxInputLengthIgnoresLargerValue() { + final DecryptionHandler decryptionHandler = DecryptionHandler.create( + masterKeyProvider_, + CommitmentPolicy.ForbidEncryptAllowDecrypt, + signaturePolicy, + CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); + decryptionHandler.setMaxInputLength(10); + assertEquals(decryptionHandler.getMaxInputLength(), 10); + + decryptionHandler.setMaxInputLength(100); + assertEquals(decryptionHandler.getMaxInputLength(), 10); + } } diff --git a/src/test/java/com/amazonaws/encryptionsdk/internal/EncryptionHandlerTest.java b/src/test/java/com/amazonaws/encryptionsdk/internal/EncryptionHandlerTest.java index a124dc919..ec16a5b12 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/internal/EncryptionHandlerTest.java +++ b/src/test/java/com/amazonaws/encryptionsdk/internal/EncryptionHandlerTest.java @@ -7,6 +7,7 @@ import static java.util.Collections.emptyList; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import java.util.Collections; import java.util.List; @@ -103,4 +104,49 @@ public void whenEncryptingV2Algorithm_fails() throws Exception { final EncryptionMaterials resultWithV2Alg = testResult.toBuilder().setAlgorithm(TestUtils.KEY_COMMIT_CRYPTO_ALG).build(); final EncryptionHandler encryptionHandler = new EncryptionHandler(frameSize_, resultWithV2Alg); } + + @Test + public void setMaxInputLength() { + byte[] plaintext = "Don't warn the tadpoles".getBytes(); + final EncryptionHandler encryptionHandler = new EncryptionHandler(frameSize_, testResult); + encryptionHandler.setMaxInputLength(plaintext.length - 1); + + assertEquals(encryptionHandler.getMaxInputLength(), (long)plaintext.length - 1); + + final byte[] out = new byte[1]; + assertThrows(IllegalStateException.class, "Plaintext size exceeds max input size limit", () -> + encryptionHandler.processBytes(plaintext, 0, plaintext.length, out, 0)); + } + + @Test + public void setMaxInputLengthThrowsIfAlreadyOver() { + byte[] plaintext = "Don't warn the tadpoles".getBytes(); + final EncryptionHandler encryptionHandler = new EncryptionHandler(frameSize_, testResult); + final byte[] out = new byte[1024]; + encryptionHandler.processBytes(plaintext, 0, plaintext.length - 1, out, 0); + assertFalse(encryptionHandler.isComplete()); + + assertThrows(IllegalStateException.class, "Plaintext size exceeds max input size limit", () -> + encryptionHandler.setMaxInputLength(plaintext.length - 2)); + } + + @Test + public void setMaxInputLengthAcceptsSmallerValue() { + final EncryptionHandler encryptionHandler = new EncryptionHandler(frameSize_, testResult); + encryptionHandler.setMaxInputLength(100); + assertEquals(encryptionHandler.getMaxInputLength(), 100); + + encryptionHandler.setMaxInputLength(10); + assertEquals(encryptionHandler.getMaxInputLength(), 10); + } + + @Test + public void setMaxInputLengthIgnoresLargerValue() { + final EncryptionHandler encryptionHandler = new EncryptionHandler(frameSize_, testResult); + encryptionHandler.setMaxInputLength(10); + assertEquals(encryptionHandler.getMaxInputLength(), 10); + + encryptionHandler.setMaxInputLength(100); + assertEquals(encryptionHandler.getMaxInputLength(), 10); + } } diff --git a/src/test/java/com/amazonaws/encryptionsdk/kms/MaxEncryptedDataKeysIntegrationTest.java b/src/test/java/com/amazonaws/encryptionsdk/kms/MaxEncryptedDataKeysIntegrationTest.java new file mode 100644 index 000000000..49fea1653 --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/kms/MaxEncryptedDataKeysIntegrationTest.java @@ -0,0 +1,92 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.encryptionsdk.kms; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.CommitmentPolicy; +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.TestUtils; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import com.amazonaws.services.kms.AWSKMS; +import com.amazonaws.services.kms.AWSKMSClientBuilder; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +public class MaxEncryptedDataKeysIntegrationTest { + private static final byte[] PLAINTEXT = {1, 2, 3, 4}; + private static final int MAX_EDKS = 3; + + private AWSKMS testClient_; + private KmsMasterKeyProvider.RegionalClientSupplier testClientSupplier_; + private AwsCrypto testCryptoClient_; + + @Before + public void setup() { + testClient_ = spy(AWSKMSClientBuilder.standard().withRegion("us-west-2").build()); + testClientSupplier_ = regionName -> { + if (regionName.equals("us-west-2")) { + return testClient_; + } + throw new AwsCryptoException("test supplier only configured for us-west-2 and eu-central-1"); + }; + testCryptoClient_ = AwsCrypto.builder() + .withCommitmentPolicy(CommitmentPolicy.ForbidEncryptAllowDecrypt) + .withEncryptionAlgorithm(CryptoAlgorithm.ALG_AES_128_GCM_IV12_TAG16_HKDF_SHA256) + .withMaxEncryptedDataKeys(MAX_EDKS).build(); + } + + private KmsMasterKeyProvider providerWithEdks(int numKeys) { + List keyIds = new ArrayList<>(numKeys); + for (int i = 0; i < numKeys; i++) { + keyIds.add(KMSTestFixtures.US_WEST_2_KEY_ID); + } + return KmsMasterKeyProvider.builder() + .withCustomClientFactory(testClientSupplier_) + .buildStrict(keyIds); + } + + @Test + public void encryptDecryptWithLessThanMaxEdks() { + KmsMasterKeyProvider provider = providerWithEdks(MAX_EDKS - 1); + byte[] ciphertext = testCryptoClient_.encryptData(provider, PLAINTEXT).getResult(); + byte[] decrypted = testCryptoClient_.decryptData(provider, ciphertext).getResult(); + assertArrayEquals(decrypted, PLAINTEXT); + } + + @Test + public void encryptDecryptWithMaxEdks() { + KmsMasterKeyProvider provider = providerWithEdks(MAX_EDKS); + byte[] ciphertext = testCryptoClient_.encryptData(provider, PLAINTEXT).getResult(); + byte[] decrypted = testCryptoClient_.decryptData(provider, ciphertext).getResult(); + assertArrayEquals(decrypted, PLAINTEXT); + } + + @Test + public void noEncryptWithMoreThanMaxEdks() { + KmsMasterKeyProvider provider = providerWithEdks(MAX_EDKS + 1); + TestUtils.assertThrows(AwsCryptoException.class, "Encrypted data keys exceed maxEncryptedDataKeys", () -> + testCryptoClient_.encryptData(provider, PLAINTEXT)); + } + + @Test + public void noDecryptWithMoreThanMaxEdks() { + KmsMasterKeyProvider provider = providerWithEdks(MAX_EDKS + 1); + byte[] ciphertext = AwsCrypto.builder() + .withCommitmentPolicy(CommitmentPolicy.ForbidEncryptAllowDecrypt) + .withEncryptionAlgorithm(CryptoAlgorithm.ALG_AES_128_GCM_IV12_TAG16_HKDF_SHA256) + .build() + .encryptData(provider, PLAINTEXT) + .getResult(); + TestUtils.assertThrows(AwsCryptoException.class, "Ciphertext encrypted data keys exceed maxEncryptedDataKeys", () -> + testCryptoClient_.decryptData(provider, ciphertext)); + verify(testClient_, never()).decrypt(any()); + } + +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/model/CiphertextHeadersTest.java b/src/test/java/com/amazonaws/encryptionsdk/model/CiphertextHeadersTest.java index c5218827e..ce2338e0a 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/model/CiphertextHeadersTest.java +++ b/src/test/java/com/amazonaws/encryptionsdk/model/CiphertextHeadersTest.java @@ -59,7 +59,7 @@ public void serializeDeserialize() { final byte[] headerBytes = ciphertextHeaders.toByteArray(); final CiphertextHeaders reconstructedHeaders = new CiphertextHeaders(); - reconstructedHeaders.deserialize(headerBytes, 0); + reconstructedHeaders.deserialize(headerBytes, 0, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); final byte[] reconstructedHeaderBytes = reconstructedHeaders.toByteArray(); assertArrayEquals(headerBytes, reconstructedHeaderBytes); @@ -85,7 +85,7 @@ public void serializeDeserializeStreaming() { final byte[] bytesToParse = new byte[bytesToParseLen]; System.arraycopy(headerBytes, totalParsedBytes, bytesToParse, 0, bytesToParse.length); - bytesParsed = reconstructedHeaders.deserialize(bytesToParse, 0); + bytesParsed = reconstructedHeaders.deserialize(bytesToParse, 0, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); if (bytesParsed == 0) { bytesToParseLen++; } else { @@ -103,7 +103,7 @@ public void serializeDeserializeStreaming() { @Test public void deserializeNull() { final CiphertextHeaders ciphertextHeaders = new CiphertextHeaders(); - final int deserializedBytes = ciphertextHeaders.deserialize(null, 0); + final int deserializedBytes = ciphertextHeaders.deserialize(null, 0, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); assertEquals(0, deserializedBytes); } @@ -276,7 +276,7 @@ public void checkEncContextLen() { final byte[] headerBytes = ciphertextHeaders.toByteArray(); final CiphertextHeaders reconstructedHeaders = new CiphertextHeaders(); - reconstructedHeaders.deserialize(headerBytes, 0); + reconstructedHeaders.deserialize(headerBytes, 0, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); assertEquals(encryptionContextLen, reconstructedHeaders.getEncryptionContextLen()); } @@ -292,7 +292,7 @@ public void checkKeyBlobCount() { final byte[] headerBytes = ciphertextHeaders.toByteArray(); final CiphertextHeaders reconstructedHeaders = new CiphertextHeaders(); - reconstructedHeaders.deserialize(headerBytes, 0); + reconstructedHeaders.deserialize(headerBytes, 0, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS); assertEquals(1, reconstructedHeaders.getEncryptedKeyBlobCount()); } @@ -423,7 +423,7 @@ public void invalidVersion(){ final CiphertextHeaders reconstructedHeaders = new CiphertextHeaders(); assertThrows(BadCiphertextException.class, "Invalid version", - () -> reconstructedHeaders.deserialize(headerBuff.array(), 0)); + () -> reconstructedHeaders.deserialize(headerBuff.array(), 0, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS)); } } @@ -445,7 +445,7 @@ public void invalidType() { final CiphertextHeaders reconstructedHeaders = new CiphertextHeaders(); assertThrows(BadCiphertextException.class, "Invalid ciphertext type", - () -> reconstructedHeaders.deserialize(headerBuff.array(), 0)); + () -> reconstructedHeaders.deserialize(headerBuff.array(), 0, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS)); } @Test @@ -465,7 +465,7 @@ public void invalidAlgoId() { final CiphertextHeaders reconstructedHeaders = new CiphertextHeaders(); assertThrows(BadCiphertextException.class, "Invalid algorithm identifier in ciphertext", - () -> reconstructedHeaders.deserialize(headerBuff.array(), 0)); + () -> reconstructedHeaders.deserialize(headerBuff.array(), 0, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS)); } } @@ -486,7 +486,7 @@ public void invalidContentType() { final CiphertextHeaders reconstructedHeaders = new CiphertextHeaders(); assertThrows(BadCiphertextException.class, "Invalid content type in ciphertext.", - () -> reconstructedHeaders.deserialize(headerBuff.array(), 0)); + () -> reconstructedHeaders.deserialize(headerBuff.array(), 0, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS)); } } @@ -508,7 +508,7 @@ public void invalidReservedFieldLen() { final CiphertextHeaders reconstructedHeaders = new CiphertextHeaders(); assertThrows(BadCiphertextException.class, "Invalid value for reserved field in ciphertext", - () -> reconstructedHeaders.deserialize(headerBuff.array(), 0)); + () -> reconstructedHeaders.deserialize(headerBuff.array(), 0, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS)); } @Test @@ -529,7 +529,7 @@ public void invalidNonceLen() { final CiphertextHeaders reconstructedHeaders = new CiphertextHeaders(); assertThrows(BadCiphertextException.class, "Invalid nonce length in ciphertext", - () -> reconstructedHeaders.deserialize(headerBuff.array(), 0)); + () -> reconstructedHeaders.deserialize(headerBuff.array(), 0, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS)); } @Test @@ -549,7 +549,7 @@ public void invalidFrameLength() { final CiphertextHeaders reconstructedHeaders = new CiphertextHeaders(); assertThrows(BadCiphertextException.class, "Invalid frame length in ciphertext", - () -> reconstructedHeaders.deserialize(headerBuff.array(), 0)); + () -> reconstructedHeaders.deserialize(headerBuff.array(), 0, CiphertextHeaders.NO_MAX_ENCRYPTED_DATA_KEYS)); } } } diff --git a/src/test/resources/aws-encryption-sdk-test-vectors b/src/test/resources/aws-encryption-sdk-test-vectors new file mode 160000 index 000000000..3fdc7c682 --- /dev/null +++ b/src/test/resources/aws-encryption-sdk-test-vectors @@ -0,0 +1 @@ +Subproject commit 3fdc7c682184a3c7214686550fafb490c14930c4