From 04423a3d6ff974db664245b6a6fc5e9ee397efd6 Mon Sep 17 00:00:00 2001 From: Wesley Rosenblum <55108558+WesleyRosenblum@users.noreply.github.com> Date: Mon, 28 Oct 2019 19:12:49 -0700 Subject: [PATCH 1/4] Add a basic example for encrypting and decrypting with a KMS CMK (#136) * *Issue #, if available:* #108 *Description of changes:* Add a basic example for encrypting and decrypting with a KMS CMK. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. # Check any applicable: - [ ] Were any files moved? Moving files changes their URL, which breaks all hyperlinks to the files. * Add test and Maven plugin to include examples directory as test source --- pom.xml | 20 +++++ .../examples/BasicEncryptionExample.java | 88 +++++++++++++++++++ .../examples/BasicEncryptionExampleTest.java | 25 ++++++ .../encryptionsdk/kms/KMSTestFixtures.java | 4 +- 4 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 src/examples/java/com/amazonaws/crypto/examples/BasicEncryptionExample.java create mode 100644 src/test/java/com/amazonaws/crypto/examples/BasicEncryptionExampleTest.java diff --git a/pom.xml b/pom.xml index 3a2f2d32c..b94985a60 100644 --- a/pom.xml +++ b/pom.xml @@ -118,6 +118,26 @@ 8 + + + org.codehaus.mojo + build-helper-maven-plugin + 3.0.0 + + + add-test-source + generate-test-sources + + add-test-source + + + + src/examples/java + + + + + diff --git a/src/examples/java/com/amazonaws/crypto/examples/BasicEncryptionExample.java b/src/examples/java/com/amazonaws/crypto/examples/BasicEncryptionExample.java new file mode 100644 index 000000000..748cea536 --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/BasicEncryptionExample.java @@ -0,0 +1,88 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.crypto.examples; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.CryptoResult; +import com.amazonaws.encryptionsdk.kms.KmsMasterKey; +import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider; + +/** + *

+ * Encrypts and then decrypts data using an AWS KMS customer master key. + * + *

+ * Arguments: + *

    + *
  1. Key ARN: For help finding the Amazon Resource Name (ARN) of your KMS customer master + * key (CMK), see 'Viewing Keys' at http://docs.aws.amazon.com/kms/latest/developerguide/viewing-keys.html + *
+ */ +public class BasicEncryptionExample { + + private static final byte[] EXAMPLE_DATA = "Hello World".getBytes(StandardCharsets.UTF_8); + + public static void main(final String[] args) { + final String keyArn = args[0]; + + encryptAndDecrypt(keyArn); + } + + static void encryptAndDecrypt(final String keyArn) { + // 1. Instantiate the SDK + final AwsCrypto crypto = new AwsCrypto(); + + // 2. Instantiate a KMS master key provider + final KmsMasterKeyProvider masterKeyProvider = KmsMasterKeyProvider.builder().withKeysForEncryption(keyArn).build(); + + // 3. Create an encryption context + // + // Most encrypted data should have an associated encryption context + // to protect integrity. This sample uses placeholder values. + // + // For more information see: + // blogs.aws.amazon.com/security/post/Tx2LZ6WBJJANTNW/How-to-Protect-the-Integrity-of-Your-Encrypted-Data-by-Using-AWS-Key-Management + final Map encryptionContext = Collections.singletonMap("ExampleContextKey", "ExampleContextValue"); + + // 4. Encrypt the data + final CryptoResult encryptResult = crypto.encryptData(masterKeyProvider, EXAMPLE_DATA, encryptionContext); + final byte[] ciphertext = encryptResult.getResult(); + + // 5. Decrypt the data + final CryptoResult decryptResult = crypto.decryptData(masterKeyProvider, ciphertext); + + // 6. Before verifying the plaintext, verify that the customer master key that + // was used in the encryption operation was the one supplied to the master key provider. + if (!decryptResult.getMasterKeyIds().get(0).equals(keyArn)) { + throw new IllegalStateException("Wrong key ID!"); + } + + // 7. Also, verify that the encryption context in the result contains the + // encryption context supplied to the encryptData method. Because the + // SDK can add values to the encryption context, don't require that + // the entire context matches. + if (!encryptionContext.entrySet().stream() + .allMatch(e -> e.getValue().equals(decryptResult.getEncryptionContext().get(e.getKey())))) { + throw new IllegalStateException("Wrong Encryption Context!"); + } + + // 8. Verify that the decrypted plaintext matches the original plaintext + assert Arrays.equals(decryptResult.getResult(), EXAMPLE_DATA); + } +} diff --git a/src/test/java/com/amazonaws/crypto/examples/BasicEncryptionExampleTest.java b/src/test/java/com/amazonaws/crypto/examples/BasicEncryptionExampleTest.java new file mode 100644 index 000000000..5d162679d --- /dev/null +++ b/src/test/java/com/amazonaws/crypto/examples/BasicEncryptionExampleTest.java @@ -0,0 +1,25 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.crypto.examples; + +import com.amazonaws.encryptionsdk.kms.KMSTestFixtures; +import org.junit.Test; + +public class BasicEncryptionExampleTest { + + @Test + public void testEncryptAndDecrypt() { + BasicEncryptionExample.encryptAndDecrypt(KMSTestFixtures.TEST_KEY_IDS[0]); + } +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/kms/KMSTestFixtures.java b/src/test/java/com/amazonaws/encryptionsdk/kms/KMSTestFixtures.java index 486c52579..1cd53370b 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/kms/KMSTestFixtures.java +++ b/src/test/java/com/amazonaws/encryptionsdk/kms/KMSTestFixtures.java @@ -1,6 +1,6 @@ package com.amazonaws.encryptionsdk.kms; -final class KMSTestFixtures { +public final class KMSTestFixtures { private KMSTestFixtures() { throw new UnsupportedOperationException( "This class exists to hold static constants and cannot be instantiated." @@ -14,7 +14,7 @@ private KMSTestFixtures() { * This should go without saying, but never use these keys for production purposes (as anyone in the world can * decrypt data encrypted using them). */ - static final String[] TEST_KEY_IDS = new String[] { + public static final String[] TEST_KEY_IDS = new String[] { "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f", "arn:aws:kms:eu-central-1:658956600833:key/75414c93-5285-4b57-99c9-30c1cf0a22c2" }; From 74c85cb7160662011de84dc26f975ce14af8aae0 Mon Sep 17 00:00:00 2001 From: SalusaSecondus Date: Tue, 29 Oct 2019 14:20:37 -0700 Subject: [PATCH 2/4] Update docs in prep for 1.6.1 (#133) * Update docs in prep for 1.6.1 * Actually bump version for release --- CHANGELOG.md | 27 ++++++++++++++++++++++----- README.md | 52 +++++++++++++++++++++------------------------------- pom.xml | 2 +- 3 files changed, 44 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ccfc619b..1458c3127 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,27 @@ # Changelog -## 1.6.1 -- Unreleased +## 1.6.1 -- 2019-10-29 + +### Deprecation Warnings +* Deprecated `AwsCrypto.encryptString()` and `AwsCrypto.decryptString()`. + Replace your calls to these methods with calls to AwsCrypto.encryptData() and AwsCrypto.decryptData(). + Unlike the deprecated methods, these methods don't perform any Base64 encoding or decoding, so they are fully compatible with other language implementations of the AWS Encryption SDK. + + If you need Base64 encoding or decoding for your application, you can add it outside of the AWS Encryption SDK. + [PR #120](https://github.com/aws/aws-encryption-sdk-java/pull/120) + +### Patches +* Correctly validate version [PR #116](https://github.com/aws/aws-encryption-sdk-java/pull/116) +* `ParsedCiphertext` now handles truncated input properly [PR #119](https://github.com/aws/aws-encryption-sdk-java/pull/119) + ### Maintenance -* Add support for standard test vectors via `testVectorZip` system property. -* No longer require use of BouncyCastle with RSA `JceMasterKey`s -* No longer use BouncyCastle for Elliptic Curve key generation and point compression/decompression +* Add support for standard test vectors via `testVectorZip` system property. [PR #127](https://github.com/aws/aws-encryption-sdk-java/pull/127) +* Remove all explicit cryptographic dependencies on BouncyCastle. The AWS Encryption SDK for Java still uses Bouncy Castle for other tasks. PRs + [#128](https://github.com/aws/aws-encryption-sdk-java/pull/128), + [#129](https://github.com/aws/aws-encryption-sdk-java/pull/129), + [#130](https://github.com/aws/aws-encryption-sdk-java/pull/130), + [#131](https://github.com/aws/aws-encryption-sdk-java/pull/131), + and [#132](https://github.com/aws/aws-encryption-sdk-java/pull/132). ## 1.6.0 -- 2019-05-31 @@ -17,7 +34,7 @@ ## 1.5.0 -- 2019-05-30 ### Minor Changes -* Add dependency on Apache Commons Codec 1.12. +* Added dependency on Apache Commons Codec 1.12. * Use org.apache.commons.codec.binary.Base64 instead of java.util.Base64 so that the SDK can be used on systems that do not have java.util.Base64 but support Java 8 language features. diff --git a/README.md b/README.md index c8ad880d0..0387070cc 100644 --- a/README.md +++ b/README.md @@ -9,26 +9,31 @@ For more details about the design and architecture of the SDK, see the [official ### Required Prerequisites To use this SDK you must have: -* **A Java 8 development environment** +* **A Java 8 or newer development environment** - If you do not have one, go to [Java SE Downloads](https://www.oracle.com/technetwork/java/javase/downloads/index.html) on the Oracle website, then download and install the Java SE Development Kit (JDK). Java 8 or higher is required. + If you do not have one, we recommend [Amazon Corretto](https://aws.amazon.com/corretto/). **Note:** If you use the Oracle JDK, you must also download and install the [Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files](http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html). -* **Bouncy Castle** +* **Bouncy Castle** or **Bouncy Castle FIPS** - Bouncy Castle provides a cryptography API for Java. If you do not have Bouncy Castle, go to https://bouncycastle.org/latest_releases.html, then download the provider file that corresponds to your JDK. Or, you can pick it up from Maven: + The AWS Encryption SDK for Java uses Bouncy Castle to serialize and deserialize cryptographic objects. + It does not explicitly use Bouncy Castle (or any other [JCA Provider](https://docs.oracle.com/javase/8/docs/api/java/security/Provider.html)) for the underlying cryptography. + Instead, it uses the platform default, which you can configure or override as documented in the + [Java Cryptography Architecture (JCA) Reference Guide](https://docs.oracle.com/javase/9/security/java-cryptography-architecture-jca-reference-guide.htm#JSSEC-GUID-2BCFDD85-D533-4E6C-8CE9-29990DEB0190). - ```xml - - org.bouncycastle - bcprov-ext-jdk15on - 1.61 - - ``` + If you do not have Bouncy Castle, go to https://bouncycastle.org/latest_releases.html, then download the provider file that corresponds to your JDK. + Or, you can pick it up from Maven (groupId: `org.bouncycastle`, artifactId: `bcprov-ext-jdk15on`). + + Beginning in version 1.6.1, + the AWS Encryption SDK also works with Bouncy Castle FIPS (groupId: `org.bouncycastle`, artifactId: `bc-fips`) + as an alternative to non-FIPS Bouncy Castle. + For help installing and configuring Bouncy Castle FIPS properly, see [BC FIPS documentation](https://www.bouncycastle.org/documentation.html), + in particular, **User Guides** and **Security Policy**. ### Optional Prerequisites +#### AWS Integration You don't need an Amazon Web Services (AWS) account to use this SDK, but some of the [example code][examples] requires an AWS account, a customer master key (CMK) in AWS KMS, and the AWS SDK for Java. * **To create an AWS account**, go to [Sign In or Create an AWS Account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) and then choose **I am a new user.** Follow the instructions to create an AWS account. @@ -37,6 +42,10 @@ You don't need an Amazon Web Services (AWS) account to use this SDK, but some of * **To download and install the AWS SDK for Java**, go to [Installing the AWS SDK for Java](https://docs.aws.amazon.com/AWSSdkDocsJava/latest/DeveloperGuide/java-dg-install-sdk.html) in the AWS SDK for Java documentation and then follow the instructions on that page. +#### Amazon Corretto Crypto Provider +Many users find that the Amazon Corretto Crypto Provider (ACCP) significantly improves the performance of the AWS Encryption SDK. +For help installing and using ACCP, see the [ACCP GitHub Respository](https://github.com/corretto/amazon-corretto-crypto-provider) . + ### Download You can get the latest release from Maven: @@ -45,29 +54,10 @@ You can get the latest release from Maven: com.amazonaws aws-encryption-sdk-java - 1.6.0 + 1.6.1 ``` -Don't forget to enable the download of snapshot jars from Maven: - -```xml - - - allow-snapshots - true - - - snapshots-repo - https://oss.sonatype.org/content/repositories/snapshots - false - true - - - - -``` - ### Get Started The following code sample demonstrates how to get started: diff --git a/pom.xml b/pom.xml index b94985a60..3cab674e2 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.amazonaws aws-encryption-sdk-java - 1.6.0 + 1.6.1 jar aws-encryption-sdk-java From 47fe973af0a8df443cbb767ff8edd566f32bb4b7 Mon Sep 17 00:00:00 2001 From: Greg Rubin Date: Tue, 29 Oct 2019 22:32:39 +0000 Subject: [PATCH 3/4] Fix for new versions of gpg --- pom.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pom.xml b/pom.xml index 3cab674e2..6c485750c 100644 --- a/pom.xml +++ b/pom.xml @@ -165,6 +165,12 @@ sign + + + --pinentry-mode + loopback + + From 4fdc30982421186df857d6a5e8628036d2317b9d Mon Sep 17 00:00:00 2001 From: Wesley Rosenblum <55108558+WesleyRosenblum@users.noreply.github.com> Date: Fri, 8 Nov 2019 14:38:08 -0800 Subject: [PATCH 4/4] Refactor JceMasterKey to extract logic to be shared by raw keyrings. (#139) * Refactor JceMasterKey to extract logic to be shared by raw keyrings. *Issue #, if available:* #102 *Description of changes:* In anticipation of the RawAesKeyring and RawRsaKeyring needing logic currently embedded in the JceMasterKey, this change extracts that logic into the JceKeyCipher class so it may be shared. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. - [ ] Were any files moved? Moving files changes their URL, which breaks all hyperlinks to the files. --- .../internal/AesGcmJceKeyCipher.java | 94 ++++++ .../encryptionsdk/internal/JceKeyCipher.java | 136 +++++++++ .../internal/RsaJceKeyCipher.java | 109 +++++++ .../encryptionsdk/internal/Utils.java | 21 ++ .../encryptionsdk/jce/JceMasterKey.java | 288 ++---------------- .../amazonaws/encryptionsdk/UtilsTest.java | 13 + 6 files changed, 403 insertions(+), 258 deletions(-) create mode 100644 src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java create mode 100644 src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java create mode 100644 src/main/java/com/amazonaws/encryptionsdk/internal/RsaJceKeyCipher.java diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java b/src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java new file mode 100644 index 000000000..7a4511f01 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/AesGcmJceKeyCipher.java @@ -0,0 +1,94 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.internal; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.util.Map; + +/** + * A JceKeyCipher based on the Advanced Encryption Standard in Galois/Counter Mode. + */ +class AesGcmJceKeyCipher extends JceKeyCipher { + private static final int NONCE_LENGTH = 12; + private static final int TAG_LENGTH = 128; + private static final String TRANSFORMATION = "AES/GCM/NoPadding"; + private static final int SPEC_LENGTH = Integer.BYTES + Integer.BYTES + NONCE_LENGTH; + + AesGcmJceKeyCipher(SecretKey key) { + super(key, key); + } + + private static byte[] specToBytes(final GCMParameterSpec spec) { + final byte[] nonce = spec.getIV(); + final byte[] result = new byte[SPEC_LENGTH]; + final ByteBuffer buffer = ByteBuffer.wrap(result); + buffer.putInt(spec.getTLen()); + buffer.putInt(nonce.length); + buffer.put(nonce); + return result; + } + + private static GCMParameterSpec bytesToSpec(final byte[] data, final int offset) throws InvalidKeyException { + if (data.length - offset != SPEC_LENGTH) { + throw new InvalidKeyException("Algorithm specification was an invalid data size"); + } + + final ByteBuffer buffer = ByteBuffer.wrap(data, offset, SPEC_LENGTH); + final int tagLen = buffer.getInt(); + final int nonceLen = buffer.getInt(); + + if (tagLen != TAG_LENGTH) { + throw new InvalidKeyException(String.format("Authentication tag length must be %s", TAG_LENGTH)); + } + + if (nonceLen != NONCE_LENGTH) { + throw new InvalidKeyException(String.format("Initialization vector (IV) length must be %s", NONCE_LENGTH)); + } + + final byte[] nonce = new byte[nonceLen]; + buffer.get(nonce); + + return new GCMParameterSpec(tagLen, nonce); + } + + @Override + WrappingData buildWrappingCipher(final Key key, final Map encryptionContext) + throws GeneralSecurityException { + final byte[] nonce = new byte[NONCE_LENGTH]; + Utils.getSecureRandom().nextBytes(nonce); + final GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH, nonce); + final Cipher cipher = Cipher.getInstance(TRANSFORMATION); + cipher.init(Cipher.ENCRYPT_MODE, key, spec); + final byte[] aad = EncryptionContextSerializer.serialize(encryptionContext); + cipher.updateAAD(aad); + return new WrappingData(cipher, specToBytes(spec)); + } + + @Override + Cipher buildUnwrappingCipher(final Key key, final byte[] extraInfo, final int offset, + final Map encryptionContext) throws GeneralSecurityException { + final GCMParameterSpec spec = bytesToSpec(extraInfo, offset); + final Cipher cipher = Cipher.getInstance(TRANSFORMATION); + cipher.init(Cipher.DECRYPT_MODE, key, spec); + final byte[] aad = EncryptionContextSerializer.serialize(encryptionContext); + cipher.updateAAD(aad); + return cipher; + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java b/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java new file mode 100644 index 000000000..643278a71 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/JceKeyCipher.java @@ -0,0 +1,136 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.internal; + +import com.amazonaws.encryptionsdk.EncryptedDataKey; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import com.amazonaws.encryptionsdk.model.KeyBlob; +import org.apache.commons.lang3.ArrayUtils; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Map; + +/** + * Abstract class for encrypting and decrypting JCE data keys. + */ +public abstract class JceKeyCipher { + + private final Key wrappingKey; + private final Key unwrappingKey; + private static final Charset KEY_NAME_ENCODING = StandardCharsets.UTF_8; + + /** + * Returns a new instance of a JceKeyCipher based on the + * Advanced Encryption Standard in Galois/Counter Mode. + * + * @param secretKey The secret key to use for encrypt/decrypt operations. + * @return The JceKeyCipher. + */ + public static JceKeyCipher aesGcm(SecretKey secretKey) { + return new AesGcmJceKeyCipher(secretKey); + } + + /** + * Returns a new instance of a JceKeyCipher based on RSA. + * + * @param wrappingKey The public key to use for encrypting the key. + * @param unwrappingKey The private key to use for decrypting the key. + * @param transformation The transformation. + * @return The JceKeyCipher. + */ + public static JceKeyCipher rsa(PublicKey wrappingKey, PrivateKey unwrappingKey, String transformation) { + return new RsaJceKeyCipher(wrappingKey, unwrappingKey, transformation); + } + + JceKeyCipher(Key wrappingKey, Key unwrappingKey) { + this.wrappingKey = wrappingKey; + this.unwrappingKey = unwrappingKey; + } + + abstract WrappingData buildWrappingCipher(Key key, Map encryptionContext) throws GeneralSecurityException; + + abstract Cipher buildUnwrappingCipher(Key key, byte[] extraInfo, int offset, + Map encryptionContext) throws GeneralSecurityException; + + + /** + * Encrypts the given key, incorporating the given keyName and encryptionContext. + * @param key The key to encrypt. + * @param keyName A UTF-8 encoded representing a name for the key. + * @param keyNamespace A UTF-8 encoded value that namespaces the key. + * @param encryptionContext A key-value mapping of arbitrary, non-secret, UTF-8 encoded strings used + * during encryption and decryption to provide additional authenticated data (AAD). + * @return The encrypted data key. + */ + public EncryptedDataKey encryptKey(final byte[] key, final String keyName, final String keyNamespace, + final Map encryptionContext) { + + final byte[] keyNameBytes = keyName.getBytes(KEY_NAME_ENCODING); + + try { + final JceKeyCipher.WrappingData wData = buildWrappingCipher(wrappingKey, encryptionContext); + final Cipher cipher = wData.cipher; + final byte[] encryptedKey = cipher.doFinal(key); + + final byte[] provInfo; + if (wData.extraInfo.length == 0) { + provInfo = keyNameBytes; + } else { + provInfo = new byte[keyNameBytes.length + wData.extraInfo.length]; + System.arraycopy(keyNameBytes, 0, provInfo, 0, keyNameBytes.length); + System.arraycopy(wData.extraInfo, 0, provInfo, keyNameBytes.length, wData.extraInfo.length); + } + + return new KeyBlob(keyNamespace, provInfo, encryptedKey); + } catch (final GeneralSecurityException gsex) { + throw new AwsCryptoException(gsex); + } + } + + /** + * Decrypts the given encrypted data key. + * + * @param edk The encrypted data key. + * @param keyName A UTF-8 encoded String representing a name for the key. + * @param encryptionContext A key-value mapping of arbitrary, non-secret, UTF-8 encoded strings used + * during encryption and decryption to provide additional authenticated data (AAD). + * @return The decrypted key. + * @throws GeneralSecurityException If a problem occurred decrypting the key. + */ + public byte[] decryptKey(final EncryptedDataKey edk, final String keyName, + final Map encryptionContext) throws GeneralSecurityException { + final byte[] keyNameBytes = keyName.getBytes(KEY_NAME_ENCODING); + + final Cipher cipher = buildUnwrappingCipher(unwrappingKey, edk.getProviderInformation(), + keyNameBytes.length, encryptionContext); + return cipher.doFinal(edk.getEncryptedDataKey()); + } + + static class WrappingData { + public final Cipher cipher; + public final byte[] extraInfo; + + WrappingData(final Cipher cipher, final byte[] extraInfo) { + this.cipher = cipher; + this.extraInfo = extraInfo != null ? extraInfo : ArrayUtils.EMPTY_BYTE_ARRAY; + } + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/RsaJceKeyCipher.java b/src/main/java/com/amazonaws/encryptionsdk/internal/RsaJceKeyCipher.java new file mode 100644 index 000000000..c830f5487 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/RsaJceKeyCipher.java @@ -0,0 +1,109 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.amazonaws.encryptionsdk.internal; + +import org.apache.commons.lang3.ArrayUtils; + +import javax.crypto.Cipher; +import javax.crypto.spec.OAEPParameterSpec; +import javax.crypto.spec.PSource; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.MGF1ParameterSpec; +import java.util.Map; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A JceKeyCipher based on RSA. + */ +class RsaJceKeyCipher extends JceKeyCipher { + + private static final Logger LOGGER = Logger.getLogger(RsaJceKeyCipher.class.getName()); + // MGF1 with SHA-224 isn't really supported, but we include it in the regex because we need it + // for proper handling of the algorithm. + private static final Pattern SUPPORTED_TRANSFORMATIONS = + Pattern.compile("RSA/ECB/(?:PKCS1Padding|OAEPWith(SHA-(?:1|224|256|384|512))AndMGF1Padding)", + Pattern.CASE_INSENSITIVE); + private final AlgorithmParameterSpec parameterSpec_; + private final String transformation_; + + RsaJceKeyCipher(PublicKey wrappingKey, PrivateKey unwrappingKey, String transformation) { + super(wrappingKey, unwrappingKey); + + final Matcher matcher = SUPPORTED_TRANSFORMATIONS.matcher(transformation); + if (matcher.matches()) { + final String hashUnknownCase = matcher.group(1); + if (hashUnknownCase != null) { + // OAEP mode a.k.a PKCS #1v2 + final String hash = hashUnknownCase.toUpperCase(); + transformation_ = "RSA/ECB/OAEPPadding"; + + final MGF1ParameterSpec mgf1Spec; + switch (hash) { + case "SHA-1": + mgf1Spec = MGF1ParameterSpec.SHA1; + break; + case "SHA-224": + LOGGER.warning(transformation + " is not officially supported by the JceMasterKey"); + mgf1Spec = MGF1ParameterSpec.SHA224; + break; + case "SHA-256": + mgf1Spec = MGF1ParameterSpec.SHA256; + break; + case "SHA-384": + mgf1Spec = MGF1ParameterSpec.SHA384; + break; + case "SHA-512": + mgf1Spec = MGF1ParameterSpec.SHA512; + break; + default: + throw new IllegalArgumentException("Unsupported algorithm: " + transformation); + } + parameterSpec_ = new OAEPParameterSpec(hash, "MGF1", mgf1Spec, PSource.PSpecified.DEFAULT); + } else { + // PKCS #1 v1.x + transformation_ = transformation; + parameterSpec_ = null; + } + } else { + LOGGER.warning(transformation + " is not officially supported by the JceMasterKey"); + // Unsupported transformation, just use exactly what we are given + transformation_ = transformation; + parameterSpec_ = null; + } + } + + @Override + WrappingData buildWrappingCipher(Key key, Map encryptionContext) throws GeneralSecurityException { + final Cipher cipher = Cipher.getInstance(transformation_); + cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec_); + return new WrappingData(cipher, ArrayUtils.EMPTY_BYTE_ARRAY); + } + + @Override + Cipher buildUnwrappingCipher(Key key, byte[] extraInfo, int offset, Map encryptionContext) throws GeneralSecurityException { + if (extraInfo.length != offset) { + throw new IllegalArgumentException("Extra info must be empty for RSA keys"); + } + + final Cipher cipher = Cipher.getInstance(transformation_); + cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec_); + return cipher; + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/Utils.java b/src/main/java/com/amazonaws/encryptionsdk/internal/Utils.java index adedea54a..5761d03f9 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/Utils.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/Utils.java @@ -311,4 +311,25 @@ public static byte[] bigIntegerToByteArray(final BigInteger bigInteger, final in System.arraycopy(rawBytes, 0, paddedResult, length - rawBytes.length, rawBytes.length); return paddedResult; } + + /** + * Returns true if the prefix of the given length for the input arrays are equal. + * This method will return as soon as the first difference is found, and is thus not constant-time. + * + * @param a The first array. + * @param b The second array. + * @param length The length of the prefix to compare. + * @return True if the prefixes are equal, false otherwise. + */ + public static boolean arrayPrefixEquals(final byte[] a, final byte[] b, final int length) { + if (a == null || b == null || a.length < length || b.length < length) { + return false; + } + for (int x = 0; x < length; x++) { + if (a[x] != b[x]) { + return false; + } + } + return true; + } } diff --git a/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java b/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java index 70d289ddd..4995066cb 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java +++ b/src/main/java/com/amazonaws/encryptionsdk/jce/JceMasterKey.java @@ -13,58 +13,36 @@ package com.amazonaws.encryptionsdk.jce; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.DataKey; +import com.amazonaws.encryptionsdk.EncryptedDataKey; +import com.amazonaws.encryptionsdk.MasterKey; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import com.amazonaws.encryptionsdk.exception.UnsupportedProviderException; +import com.amazonaws.encryptionsdk.internal.JceKeyCipher; +import com.amazonaws.encryptionsdk.internal.Utils; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; -import java.security.GeneralSecurityException; import java.security.Key; import java.security.PrivateKey; import java.security.PublicKey; -import java.security.SecureRandom; -import java.security.spec.AlgorithmParameterSpec; -import java.security.spec.MGF1ParameterSpec; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.logging.Logger; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.crypto.Cipher; -import javax.crypto.SecretKey; -import javax.crypto.spec.GCMParameterSpec; -import javax.crypto.spec.OAEPParameterSpec; -import javax.crypto.spec.PSource; -import javax.crypto.spec.SecretKeySpec; - -import com.amazonaws.encryptionsdk.CryptoAlgorithm; -import com.amazonaws.encryptionsdk.DataKey; -import com.amazonaws.encryptionsdk.EncryptedDataKey; -import com.amazonaws.encryptionsdk.MasterKey; -import com.amazonaws.encryptionsdk.exception.AwsCryptoException; -import com.amazonaws.encryptionsdk.exception.UnsupportedProviderException; -import com.amazonaws.encryptionsdk.internal.EncryptionContextSerializer; /** * Represents a {@link MasterKey} backed by one (or more) JCE {@link Key}s. Instances of this should * only be acquired using {@link #getInstance(SecretKey, String, String, String)} or * {@link #getInstance(PublicKey, PrivateKey, String, String, String)}. */ -public abstract class JceMasterKey extends MasterKey { - private static final Logger LOGGER = Logger.getLogger(JceMasterKey.class.getName()); - private static final byte[] EMPTY_ARRAY = new byte[0]; - - private final SecureRandom rnd = new SecureRandom(); - private final Key wrappingKey_; - private final Key unwrappingKey_; +public class JceMasterKey extends MasterKey { private final String providerName_; private final String keyId_; private final byte[] keyIdBytes_; + private final JceKeyCipher jceKeyCipher_; /** * Returns a {@code JceMasterKey} backed by {@code key} using {@code wrappingAlgorithm}. @@ -82,7 +60,7 @@ public static JceMasterKey getInstance(final SecretKey key, final String provide final String wrappingAlgorithm) { switch (wrappingAlgorithm.toUpperCase()) { case "AES/GCM/NOPADDING": - return new AesGcm(key, provider, keyId); + return new JceMasterKey(provider, keyId, JceKeyCipher.aesGcm(key)); default: throw new IllegalArgumentException("Right now only AES/GCM/NoPadding is supported"); @@ -104,18 +82,16 @@ public static JceMasterKey getInstance(final PublicKey wrappingKey, final Privat final String provider, final String keyId, final String wrappingAlgorithm) { if (wrappingAlgorithm.toUpperCase().startsWith("RSA/ECB/")) { - return new Rsa(wrappingKey, unwrappingKey, provider, keyId, wrappingAlgorithm); + return new JceMasterKey(provider, keyId, JceKeyCipher.rsa(wrappingKey, unwrappingKey, wrappingAlgorithm)); } throw new UnsupportedOperationException("Currently only RSA asymmetric algorithms are supported"); } - protected JceMasterKey(final Key wrappingKey, final Key unwrappingKey, final String providerName, - final String keyId) { - wrappingKey_ = wrappingKey; - unwrappingKey_ = unwrappingKey; + protected JceMasterKey(final String providerName, final String keyId, final JceKeyCipher jceKeyCipher) { providerName_ = providerName; keyId_ = keyId; keyIdBytes_ = keyId_.getBytes(StandardCharsets.UTF_8); + jceKeyCipher_ = jceKeyCipher; } @Override @@ -132,9 +108,10 @@ public String getKeyId() { public DataKey generateDataKey(final CryptoAlgorithm algorithm, final Map encryptionContext) { final byte[] rawKey = new byte[algorithm.getDataKeyLength()]; - rnd.nextBytes(rawKey); - final SecretKeySpec key = new SecretKeySpec(rawKey, algorithm.getDataKeyAlgo()); - return encryptRawKey(key, rawKey, encryptionContext); + Utils.getSecureRandom().nextBytes(rawKey); + EncryptedDataKey encryptedDataKey = jceKeyCipher_.encryptKey(rawKey, keyId_, providerName_, encryptionContext); + return new DataKey<>(new SecretKeySpec(rawKey, algorithm.getDataKeyAlgo()), + encryptedDataKey.getEncryptedDataKey(), encryptedDataKey.getProviderInformation(), this); } @Override @@ -150,26 +127,8 @@ public DataKey encryptDataKey(final CryptoAlgorithm algorithm, throw new IllegalArgumentException("Incorrect key algorithm. Expected " + key.getAlgorithm() + " but got " + algorithm.getKeyAlgo()); } - final byte[] rawKey = key.getEncoded(); - final DataKey result = encryptRawKey(key, rawKey, encryptionContext); - Arrays.fill(rawKey, (byte) 0); - return result; - } - - protected DataKey encryptRawKey(final SecretKey key, final byte[] rawKey, - final Map encryptionContext) { - try { - final WrappingData wData = buildWrappingCipher(wrappingKey_, encryptionContext); - final Cipher cipher = wData.cipher; - final byte[] encryptedKey = cipher.doFinal(rawKey); - - final byte[] provInfo = new byte[keyIdBytes_.length + wData.extraInfo.length]; - System.arraycopy(keyIdBytes_, 0, provInfo, 0, keyIdBytes_.length); - System.arraycopy(wData.extraInfo, 0, provInfo, keyIdBytes_.length, wData.extraInfo.length); - return new DataKey<>(key, encryptedKey, provInfo, this); - } catch (final GeneralSecurityException gsex) { - throw new AwsCryptoException(gsex); - } + EncryptedDataKey encryptedDataKey = jceKeyCipher_.encryptKey(key.getEncoded(), keyId_, providerName_, encryptionContext); + return new DataKey<>(key, encryptedDataKey.getEncryptedDataKey(), encryptedDataKey.getProviderInformation(), this); } @Override @@ -182,10 +141,13 @@ public DataKey decryptDataKey(final CryptoAlgorithm algorithm, for (final EncryptedDataKey edk : encryptedDataKeys) { try { if (edk.getProviderId().equals(getProviderId()) - && arrayPrefixEquals(edk.getProviderInformation(), keyIdBytes_, keyIdBytes_.length)) { - final DataKey result = actualDecrypt(algorithm, edk, encryptionContext); - if (result != null) { - return result; + && Utils.arrayPrefixEquals(edk.getProviderInformation(), keyIdBytes_, keyIdBytes_.length)) { + final byte[] decryptedKey = jceKeyCipher_.decryptKey(edk, keyId_, encryptionContext); + + // Validate that the decrypted key length is as expected + if (decryptedKey.length == algorithm.getDataKeyLength()) { + return new DataKey<>(new SecretKeySpec(decryptedKey, algorithm.getDataKeyAlgo()), + edk.getEncryptedDataKey(), edk.getProviderInformation(), this); } } } catch (final Exception ex) { @@ -194,194 +156,4 @@ && arrayPrefixEquals(edk.getProviderInformation(), keyIdBytes_, keyIdBytes_.leng } throw buildCannotDecryptDksException(exceptions); } - - protected DataKey actualDecrypt(final CryptoAlgorithm algorithm, final EncryptedDataKey edk, - final Map encryptionContext) throws GeneralSecurityException { - final Cipher cipher = buildUnwrappingCipher(unwrappingKey_, edk.getProviderInformation(), - keyIdBytes_.length, - encryptionContext); - final byte[] rawKey = cipher.doFinal(edk.getEncryptedDataKey()); - if (rawKey.length != algorithm.getDataKeyLength()) { - // Something's wrong here. Assume that the decryption is invalid. - return null; - } - return new DataKey<>( - new SecretKeySpec(rawKey, algorithm.getDataKeyAlgo()), - edk.getEncryptedDataKey(), - edk.getProviderInformation(), this); - - } - - protected static boolean arrayPrefixEquals(final byte[] a, final byte[] b, final int len) { - if (a == null || b == null || a.length < len || b.length < len) { - return false; - } - for (int x = 0; x < len; x++) { - if (a[x] != b[x]) { - return false; - } - } - return true; - } - - protected abstract WrappingData buildWrappingCipher(Key key, Map encryptionContext) - throws GeneralSecurityException; - - protected abstract Cipher buildUnwrappingCipher(Key key, byte[] extraInfo, int offset, - Map encryptionContext) throws GeneralSecurityException; - - private static class WrappingData { - public final Cipher cipher; - public final byte[] extraInfo; - - public WrappingData(final Cipher cipher, final byte[] extraInfo) { - super(); - this.cipher = cipher; - this.extraInfo = extraInfo != null ? extraInfo : EMPTY_ARRAY; - } - } - - private static class Rsa extends JceMasterKey { - // MGF1 with SHA-224 isn't really supported, but we include it in the regex because we need it - // for proper handling of the algorithm. - private static final Pattern SUPPORTED_TRANSFORMATIONS = - Pattern.compile("RSA/ECB/(?:PKCS1Padding|OAEPWith(SHA-(?:1|224|256|384|512))AndMGF1Padding)", - Pattern.CASE_INSENSITIVE); - private final AlgorithmParameterSpec parameterSpec_; - private final String transformation_; - - private Rsa(PublicKey wrappingKey, PrivateKey unwrappingKey, String providerName, String keyId, - String transformation) { - super(wrappingKey, unwrappingKey, providerName, keyId); - - final Matcher matcher = SUPPORTED_TRANSFORMATIONS.matcher(transformation); - if (matcher.matches()) { - final String hashUnknownCase = matcher.group(1); - if (hashUnknownCase != null) { - // OAEP mode a.k.a PKCS #1v2 - final String hash = hashUnknownCase.toUpperCase(); - transformation_ = "RSA/ECB/OAEPPadding"; - - final MGF1ParameterSpec mgf1Spec; - switch (hash) { - case "SHA-1": - mgf1Spec = MGF1ParameterSpec.SHA1; - break; - case "SHA-224": - LOGGER.warning(transformation + " is not officially supported by the JceMasterKey"); - mgf1Spec = MGF1ParameterSpec.SHA224; - break; - case "SHA-256": - mgf1Spec = MGF1ParameterSpec.SHA256; - break; - case "SHA-384": - mgf1Spec = MGF1ParameterSpec.SHA384; - break; - case "SHA-512": - mgf1Spec = MGF1ParameterSpec.SHA512; - break; - default: - throw new IllegalArgumentException("Unsupported algorithm: " + transformation); - } - parameterSpec_ = new OAEPParameterSpec(hash, "MGF1", mgf1Spec, PSource.PSpecified.DEFAULT); - } else { - // PKCS #1 v1.x - transformation_ = transformation; - parameterSpec_ = null; - } - } else { - LOGGER.warning(transformation + " is not officially supported by the JceMasterKey"); - // Unsupported transformation, just use exactly what we are given - transformation_ = transformation; - parameterSpec_ = null; - } - } - - @Override - protected WrappingData buildWrappingCipher(Key key, Map encryptionContext) - throws GeneralSecurityException { - // We require BouncyCastle to avoid some bugs in the default Java implementation - // of OAEP. - final Cipher cipher = Cipher.getInstance(transformation_); - cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec_); - return new WrappingData(cipher, EMPTY_ARRAY); - } - - @Override - protected Cipher buildUnwrappingCipher(Key key, byte[] extraInfo, int offset, - Map encryptionContext) throws GeneralSecurityException { - if (extraInfo.length != offset) { - throw new IllegalArgumentException("Extra info must be empty for RSA keys"); - } - // We require BouncyCastle to avoid some bugs in the default Java implementation - // of OAEP. - final Cipher cipher = Cipher.getInstance(transformation_); - cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec_); - return cipher; - } - } - - private static class AesGcm extends JceMasterKey { - private static final int NONCE_LENGTH = 12; - private static final int TAG_LENGTH = 128; - private static final String TRANSFORMATION = "AES/GCM/NoPadding"; - - private final SecureRandom rnd = new SecureRandom(); - - public AesGcm(final SecretKey key, final String providerName, final String keyId) { - super(key, key, providerName, keyId); - } - - private static byte[] specToBytes(final GCMParameterSpec spec) { - final byte[] nonce = spec.getIV(); - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try (final DataOutputStream dos = new DataOutputStream(baos)) { - dos.writeInt(spec.getTLen()); - dos.writeInt(nonce.length); - dos.write(nonce); - dos.close(); - baos.close(); - } catch (final IOException ex) { - throw new AssertionError("Impossible exception", ex); - } - return baos.toByteArray(); - } - - private static GCMParameterSpec bytesToSpec(final byte[] data, final int offset) { - final ByteArrayInputStream bais = new ByteArrayInputStream(data, offset, data.length - offset); - try (final DataInputStream dis = new DataInputStream(bais)) { - final int tagLen = dis.readInt(); - final int nonceLen = dis.readInt(); - final byte[] nonce = new byte[nonceLen]; - dis.readFully(nonce); - return new GCMParameterSpec(tagLen, nonce); - } catch (final IOException ex) { - throw new AssertionError("Impossible exception", ex); - } - } - - @Override - protected WrappingData buildWrappingCipher(final Key key, final Map encryptionContext) - throws GeneralSecurityException { - final byte[] nonce = new byte[NONCE_LENGTH]; - rnd.nextBytes(nonce); - final GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH, nonce); - final Cipher cipher = Cipher.getInstance(TRANSFORMATION); - cipher.init(Cipher.ENCRYPT_MODE, key, spec); - final byte[] aad = EncryptionContextSerializer.serialize(encryptionContext); - cipher.updateAAD(aad); - return new WrappingData(cipher, specToBytes(spec)); - } - - @Override - protected Cipher buildUnwrappingCipher(final Key key, final byte[] extraInfo, final int offset, - final Map encryptionContext) throws GeneralSecurityException { - final GCMParameterSpec spec = bytesToSpec(extraInfo, offset); - final Cipher cipher = Cipher.getInstance(TRANSFORMATION); - cipher.init(Cipher.DECRYPT_MODE, key, spec); - final byte[] aad = EncryptionContextSerializer.serialize(encryptionContext); - cipher.updateAAD(aad); - return cipher; - } - } } diff --git a/src/test/java/com/amazonaws/encryptionsdk/UtilsTest.java b/src/test/java/com/amazonaws/encryptionsdk/UtilsTest.java index 50987611f..7a2013023 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/UtilsTest.java +++ b/src/test/java/com/amazonaws/encryptionsdk/UtilsTest.java @@ -2,6 +2,7 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; @@ -121,5 +122,17 @@ public void testBigIntegerToByteArray_InvalidLength() { Utils.bigIntegerToByteArray(new BigInteger(bytes), 3)); } + @Test + public void testArrayPrefixEquals() { + byte[] a = new byte[] {10, 11, 12, 13, 14, 15}; + byte[] b = new byte[] {10, 11, 12, 13, 20, 21, 22}; + + assertFalse(Utils.arrayPrefixEquals(null, b, 4)); + assertFalse(Utils.arrayPrefixEquals(a, null, 4)); + assertFalse(Utils.arrayPrefixEquals(a, b, a.length + 1)); + assertTrue(Utils.arrayPrefixEquals(a, b, 4)); + assertFalse(Utils.arrayPrefixEquals(a, b, 5)); + } + }