diff options
author | Kenny Root <kroot@google.com> | 2013-05-13 14:47:30 -0700 |
---|---|---|
committer | Kenny Root <kroot@google.com> | 2013-05-14 17:33:18 -0700 |
commit | d416195acbc08f2b3bdd5d5532d40438465d99e9 (patch) | |
tree | 531d20377c7bb7471798b0f4e622ac882bf0d291 | |
parent | e95612e19f51b0fbcc9196e7a07737388010a4ea (diff) | |
download | libcore-d416195acbc08f2b3bdd5d5532d40438465d99e9.zip libcore-d416195acbc08f2b3bdd5d5532d40438465d99e9.tar.gz libcore-d416195acbc08f2b3bdd5d5532d40438465d99e9.tar.bz2 |
Add classes for AEAD encryption
New classes in Java 7 for Authenicated Encryption with Additional Data
(AEAD). This allows the use of encryption modes such as Galois/Counter
Mode with performs the equivalent of MAC and encryption simultaneously
and consequently makes encryption safer to use for implementors.
Change-Id: I6302826b096044ade5f62a667dc240e3ab07b351
6 files changed, 482 insertions, 1 deletions
diff --git a/luni/src/main/java/javax/crypto/AEADBadTagException.java b/luni/src/main/java/javax/crypto/AEADBadTagException.java new file mode 100644 index 0000000..d5d1b11 --- /dev/null +++ b/luni/src/main/java/javax/crypto/AEADBadTagException.java @@ -0,0 +1,43 @@ +/* + * Copyright 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package javax.crypto; + +/** + * Thrown by a {@link Cipher} that is using an Authenticated Encryption with + * Additional Data (AEAD) mode such as Galois/Counter Mode (GCM) and the tag + * failed verification. + * + * @since 1.7 + */ +public class AEADBadTagException extends BadPaddingException { + private static final long serialVersionUID = -488059093241685509L; + + /** + * Constructs an instance of {@code AEADBadTagException}. + */ + public AEADBadTagException() { + super(); + } + + /** + * Constructs an instance of {@code AEADBadTagException} with the given + * {@code message}. + */ + public AEADBadTagException(String message) { + super(message); + } +} diff --git a/luni/src/main/java/javax/crypto/Cipher.java b/luni/src/main/java/javax/crypto/Cipher.java index aeb5def..af58a22 100644 --- a/luni/src/main/java/javax/crypto/Cipher.java +++ b/luni/src/main/java/javax/crypto/Cipher.java @@ -875,7 +875,7 @@ public class Cipher { * if this cipher instance is not initialized for encryption or * decryption. * @throws IllegalArgumentException - * if the input is {@code null}, or if {@code inputOffset} and + * if {@code input} is {@code null}, or if {@code inputOffset} and * {@code inputLen} do not specify a valid chunk in the input * buffer. */ @@ -1026,6 +1026,99 @@ public class Cipher { } /** + * Continues a multi-part transformation (encryption or decryption) with + * Authenticated Additional Data (AAD). AAD may only be added after the + * {@code Cipher} is initialized and before any data is passed to the + * instance. + * <p> + * This is only usable with cipher modes that support Authenticated + * Encryption with Additional Data (AEAD) such as Galois/Counter Mode (GCM). + * + * @param input bytes of AAD to use with the cipher + * @throws IllegalStateException + * if this cipher instance is not initialized for encryption or + * decryption. + * @throws IllegalArgumentException + * if {@code input} is {@code null} + * @throws UnsupportedOperationException if the cipher does not support AEAD + * @since 1.7 + */ + public final void updateAAD(byte[] input) { + if (input == null) { + throw new IllegalArgumentException("input == null"); + } + if (mode != ENCRYPT_MODE && mode != DECRYPT_MODE) { + throw new IllegalStateException(); + } + if (input.length == 0) { + return; + } + spiImpl.engineUpdateAAD(input, 0, input.length); + } + + /** + * Continues a multi-part transformation (encryption or decryption) with + * Authenticated Additional Data (AAD). AAD may only be added after the + * {@code Cipher} is initialized and before any data is passed to the + * instance. + * <p> + * This is only usable with cipher modes that support Authenticated + * Encryption with Additional Data (AEAD) such as Galois/Counter Mode (GCM). + * + * @param input bytes of AAD to use with the cipher + * @param inputOffset offset within bytes of additional data to add to cipher + * @param inputLen length of bytes of additional data to add to cipher + * @throws IllegalStateException + * if this cipher instance is not initialized for encryption or + * decryption. + * @throws IllegalArgumentException + * if {@code input} is {@code null}, or if {@code inputOffset} and + * {@code inputLen} do not specify a valid chunk in the input + * buffer. + * @throws UnsupportedOperationException if the cipher does not support AEAD + * @since 1.7 + */ + public final void updateAAD(byte[] input, int inputOffset, int inputLen) { + if (input == null) { + throw new IllegalArgumentException("input == null"); + } + if (mode != ENCRYPT_MODE && mode != DECRYPT_MODE) { + throw new IllegalStateException(); + } + checkInputOffsetAndCount(input.length, inputOffset, inputLen); + if (input.length == 0) { + return; + } + spiImpl.engineUpdateAAD(input, inputOffset, inputLen); + } + + /** + * Continues a multi-part transformation (encryption or decryption) with + * Authenticated Additional Data (AAD). AAD may only be added after the + * {@code Cipher} is initialized and before any data is passed to the + * instance. + * <p> + * This is only usable with cipher modes that support Authenticated + * Encryption with Additional Data (AEAD) such as Galois/Counter Mode (GCM). + * + * @param input buffer of AAD to be used + * @throws IllegalStateException + * if this cipher instance is not initialized for encryption or + * decryption. + * @throws UnsupportedOperationException if the cipher does not support AEAD + * @since 1.7 + */ + public final void updateAAD(ByteBuffer input) { + if (mode != ENCRYPT_MODE && mode != DECRYPT_MODE) { + throw new IllegalStateException("Cipher is not initialized"); + } + if (input == null) { + throw new IllegalArgumentException("input == null"); + } + spiImpl.engineUpdateAAD(input); + } + + /** * Finishes a multi-part transformation (encryption or decryption). * <p> * Processes any bytes that may have been buffered in previous {@code diff --git a/luni/src/main/java/javax/crypto/CipherSpi.java b/luni/src/main/java/javax/crypto/CipherSpi.java index 1f91ba8..70e06b1 100644 --- a/luni/src/main/java/javax/crypto/CipherSpi.java +++ b/luni/src/main/java/javax/crypto/CipherSpi.java @@ -368,6 +368,69 @@ public abstract class CipherSpi { } /** + * Continues a multi-part transformation (encryption or decryption) with + * Authenticated Additional Data (AAD). AAD may only be added after the + * {@code Cipher} is initialized and before any data is passed to the + * instance. + * <p> + * This is only usable with cipher modes that support Authenticated + * Encryption with Additional Data (AEAD) such as Galois/Counter Mode (GCM). + * + * @param input bytes of AAD to use with the cipher + * @param inputOffset offset within bytes of additional data to add to cipher + * @param inputLen length of bytes of additional data to add to cipher + * @throws IllegalStateException + * if this cipher instance is not initialized for encryption or + * decryption. + * @throws IllegalArgumentException + * if {@code input} is {@code null}, or if {@code inputOffset} and + * {@code inputLen} do not specify a valid chunk in the input + * buffer. + * @throws UnsupportedOperationException if the cipher does not support AEAD + * @since 1.7 + */ + protected void engineUpdateAAD(byte[] input, int inputOffset, int inputLen) { + throw new UnsupportedOperationException( + "This cipher does not support Authenticated Encryption with Additional Data"); + } + + /** + * Continues a multi-part transformation (encryption or decryption). The + * {@code input.remaining()} bytes starting at {@code input.position()} are + * used for the Additional Authenticated Data (AAD). AAD may only be added + * after the {@code Cipher} is initialized and before any data is passed to + * the instance. + * <p> + * This is only usable with cipher modes that support Authenticated + * Encryption with Additional Data (AEAD) such as Galois/Counter Mode (GCM). + * + * @param input the input buffer to transform. + * @since 1.7 + */ + protected void engineUpdateAAD(ByteBuffer input) { + if (input == null) { + throw new NullPointerException("input == null"); + } + int position = input.position(); + int limit = input.limit(); + if ((limit - position) <= 0) { + return; + } + byte[] bInput; + if (input.hasArray()) { + bInput = input.array(); + int offset = input.arrayOffset(); + engineUpdateAAD(bInput, offset + position, limit - position); + input.position(limit); + } else { + int len = limit - position; + bInput = new byte[len]; + input.get(bInput); + engineUpdateAAD(bInput, 0, len); + } + } + + /** * Finishes a multi-part transformation (encryption or decryption). * <p> * Processes the {@code inputLen} bytes in {@code input} buffer at {@code diff --git a/luni/src/main/java/javax/crypto/spec/GCMParameterSpec.java b/luni/src/main/java/javax/crypto/spec/GCMParameterSpec.java new file mode 100644 index 0000000..a781697 --- /dev/null +++ b/luni/src/main/java/javax/crypto/spec/GCMParameterSpec.java @@ -0,0 +1,97 @@ +/* + * Copyright 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package javax.crypto.spec; + +import java.security.spec.AlgorithmParameterSpec; +import java.util.Arrays; + +/** + * Provides a the parameters for an instance of a {@link javax.crypto.Cipher} + * using Galois/Counter Mode (GCM). This is an Authenticated Encryption with + * Associated Data (AEAD) mode for a cipher which allows you to use the + * {@link javax.crypto.Cipher#updateAAD(byte[])} method to provide data that is + * transmitted in the clear but authenticated using a cryptographic Message + * Authentication Code (MAC). + * + * @since 1.7 + */ +public class GCMParameterSpec implements AlgorithmParameterSpec { + private final int tagLen; + + private final byte[] iv; + + /** + * Creates a new {@code GCMParameterSpec} instance from the specified + * Initial Vector (IV) from buffer {@code iv} and a tag length of + * {@code tagLen} in bits. + * + * @throws IllegalArgumentException if the specified {@code iv} is null or + * {@code offset} and {@code byteCount} do not specify a valid + * chunk in the specified buffer. + */ + public GCMParameterSpec(int tagLen, byte[] iv) { + if (tagLen < 0) { + throw new IllegalArgumentException("tag should be a non-negative integer"); + } + if (iv == null) { + throw new IllegalArgumentException("iv == null"); + } + this.tagLen = tagLen; + this.iv = iv.clone(); + } + + /** + * Creates a new {@code GCMParameterSpec} instance with the Initial Vector + * (IV) of {@code byteCount} bytes from the specified buffer {@code iv} + * starting at {@code offset} and a tag length of {@code tagLen} in bits. + * + * @throws IllegalArgumentException if the specified {@code iv} is null or + * {@code offset} and {@code byteCount} do not specify a valid + * chunk in the specified buffer. + * @throws ArrayIndexOutOfBoundsException if {@code offset} or + * {@code byteCount} are negative. + */ + public GCMParameterSpec(int tagLen, byte[] iv, int offset, int byteCount) { + if (tagLen < 0) { + throw new IllegalArgumentException("tag should be a non-negative integer"); + } + if (iv == null) { + throw new IllegalArgumentException("iv == null"); + } + try { + Arrays.checkOffsetAndCount(iv.length, offset, byteCount); + } catch (ArrayIndexOutOfBoundsException e) { + throw new IllegalArgumentException(e); + } + this.tagLen = tagLen; + this.iv = Arrays.copyOfRange(iv, offset, offset + byteCount); + } + + /** + * Returns the size of the tag in bits. + */ + public int getTLen() { + return tagLen; + } + + /** + * Returns the Initial Vector (IV) used by this parameter spec. + */ + public byte[] getIV() { + return iv.clone(); + } +} diff --git a/luni/src/test/java/libcore/javax/crypto/CipherTest.java b/luni/src/test/java/libcore/javax/crypto/CipherTest.java index 9f19eef..d349919 100644 --- a/luni/src/test/java/libcore/javax/crypto/CipherTest.java +++ b/luni/src/test/java/libcore/javax/crypto/CipherTest.java @@ -20,6 +20,7 @@ import com.android.org.bouncycastle.asn1.x509.KeyUsage; import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.math.BigInteger; +import java.nio.ByteBuffer; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.Key; @@ -2032,6 +2033,12 @@ public final class CipherTest extends TestCase { c.init(Cipher.DECRYPT_MODE, key, spec); + try { + c.updateAAD(new byte[8]); + fail("Cipher should not support AAD"); + } catch (UnsupportedOperationException expected) { + } + byte[] emptyPlainText = c.doFinal(emptyCipherText); assertEquals(Arrays.toString(EmptyArray.BYTE), Arrays.toString(emptyPlainText)); @@ -2125,6 +2132,82 @@ public final class CipherTest extends TestCase { } } + public void testCipher_updateAAD_BeforeInit_Failure() throws Exception { + Cipher c = Cipher.getInstance("AES/ECB/NoPadding"); + + try { + c.updateAAD((byte[]) null); + fail("should not be able to call updateAAD before Cipher is initialized"); + } catch (IllegalArgumentException expected) { + } + + try { + c.updateAAD((ByteBuffer) null); + fail("should not be able to call updateAAD before Cipher is initialized"); + } catch (IllegalStateException expected) { + } + + try { + c.updateAAD(new byte[8]); + fail("should not be able to call updateAAD before Cipher is initialized"); + } catch (IllegalStateException expected) { + } + + try { + c.updateAAD(null, 0, 8); + fail("should not be able to call updateAAD before Cipher is initialized"); + } catch (IllegalStateException expected) { + } + + ByteBuffer bb = ByteBuffer.allocate(8); + try { + c.updateAAD(bb); + fail("should not be able to call updateAAD before Cipher is initialized"); + } catch (IllegalStateException expected) { + } + } + + public void testCipher_updateAAD_AfterInit_Failure() throws Exception { + Cipher c = Cipher.getInstance("AES/ECB/NoPadding"); + c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(new byte[128 / 8], "AES")); + + try { + c.updateAAD((byte[]) null); + fail("should not be able to call updateAAD with null input"); + } catch (IllegalArgumentException expected) { + } + + try { + c.updateAAD((ByteBuffer) null); + fail("should not be able to call updateAAD with null input"); + } catch (IllegalArgumentException expected) { + } + + try { + c.updateAAD(null, 0, 8); + fail("should not be able to call updateAAD with null input"); + } catch (IllegalArgumentException expected) { + } + + try { + c.updateAAD(new byte[8], -1, 7); + fail("should not be able to call updateAAD with invalid offset"); + } catch (IllegalArgumentException expected) { + } + + try { + c.updateAAD(new byte[8], 0, -1); + fail("should not be able to call updateAAD with negative length"); + } catch (IllegalArgumentException expected) { + } + + try { + c.updateAAD(new byte[8], 0, 8 + 1); + fail("should not be able to call updateAAD with too large length"); + } catch (IllegalArgumentException expected) { + } + } + public void testCipher_ShortBlock_Failure() throws Exception { for (String provider : AES_PROVIDERS) { testCipher_ShortBlock_Failure(provider); diff --git a/luni/src/test/java/org/apache/harmony/crypto/tests/javax/crypto/spec/GCMParameterSpecTest.java b/luni/src/test/java/org/apache/harmony/crypto/tests/javax/crypto/spec/GCMParameterSpecTest.java new file mode 100644 index 0000000..28b57fd --- /dev/null +++ b/luni/src/test/java/org/apache/harmony/crypto/tests/javax/crypto/spec/GCMParameterSpecTest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.harmony.crypto.tests.javax.crypto.spec; + +import java.util.Arrays; + +import javax.crypto.spec.GCMParameterSpec; + +import junit.framework.TestCase; + +public class GCMParameterSpecTest extends TestCase { + private static final byte[] TEST_IV = new byte[8]; + + public void testConstructor_IntByteArray_Success() throws Exception { + new GCMParameterSpec(8, TEST_IV); + } + + public void testConstructor_IntByteArray_NegativeTLen_Failure() throws Exception { + try { + new GCMParameterSpec(-1, TEST_IV); + fail("Should throw IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + } + } + + public void testConstructor_IntByteArray_NullIv_Failure() throws Exception { + try { + new GCMParameterSpec(8, null); + fail("Should throw IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + } + } + + public void testConstructor_IntByteArrayWithOffsets_Success() throws Exception { + new GCMParameterSpec(8, TEST_IV, 0, TEST_IV.length); + } + + public void testConstructor_IntByteArrayWithOffsets_NullIv_Failure() throws Exception { + try { + new GCMParameterSpec(8, null, 0, TEST_IV.length); + fail("Should throw IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + } + } + + public void testConstructor_IntByteArrayWithOffsets_NegativeOffset_Failure() throws Exception { + try { + new GCMParameterSpec(8, TEST_IV, -1, TEST_IV.length); + fail("Should throw IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + } + } + + public void testConstructor_IntByteArrayWithOffsets_TooLongLength_Failure() throws Exception { + try { + new GCMParameterSpec(8, TEST_IV, 0, TEST_IV.length + 1); + fail("Should throw IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + } + } + + public void testGetIV_Success() throws Exception { + GCMParameterSpec spec = new GCMParameterSpec(8, TEST_IV); + + byte[] actual = spec.getIV(); + assertEquals(Arrays.toString(TEST_IV), Arrays.toString(actual)); + + // XOR with 0xFF so we're sure we changed the array + for (int i = 0; i < actual.length; i++) { + actual[i] ^= 0xFF; + } + + assertFalse("Changing the IV returned shouldn't change the parameter spec", + Arrays.equals(spec.getIV(), actual)); + assertEquals(Arrays.toString(TEST_IV), Arrays.toString(spec.getIV())); + } + + public void testGetIV_Subarray_Success() throws Exception { + GCMParameterSpec spec = new GCMParameterSpec(8, TEST_IV, 2, 4); + assertEquals(Arrays.toString(Arrays.copyOfRange(TEST_IV, 2, 6)), + Arrays.toString(spec.getIV())); + } + + public void testGetTLen_Success() throws Exception { + GCMParameterSpec spec = new GCMParameterSpec(8, TEST_IV); + assertEquals(8, spec.getTLen()); + } +} |