Skip to content
Snippets Groups Projects
Commit aa2f4e67 authored by Bartosz Przydatek's avatar Bartosz Przydatek Committed by Thai Duong
Browse files

Adding handling of null parameters in Java-AEAD-operations, and tests.

Change-Id: Iefbc4c156412749e45c4bbac0006075c1fb37247
ORIGINAL_AUTHOR=Bartosz Przydatek <przydatek@google.com>
GitOrigin-RevId: 64fe080d486c3439ab3020e432e3fb28e6aaab6c
parent 69aa8dc6
No related branches found
No related tags found
No related merge requests found
Showing
with 353 additions and 29 deletions
......@@ -23,7 +23,7 @@ AEAD primitive (Authenticated Encryption with Associated Data) provides
functionality of symmetric authenticated encryption. Implementations of this
primitive are secure against adaptive chosen ciphertext attacks. When
encrypting a plaintext one can optionally provide _associated data_ that should
be authenticated but not encrypted. That is, the encryption with additional data
be authenticated but not encrypted. That is, the encryption with associated data
ensures authenticity (ie. who the sender is) and integrity (ie. data has not
been tampered with) of that data, but not its secrecy
(see [RFC 5116](https://tools.ietf.org/html/rfc5116)).
......
......@@ -22,26 +22,37 @@ import java.security.GeneralSecurityException;
* Interface for Authenticated Encryption with Associated Data (AEAD).
*
* <p>Implementations of this interface are secure against adaptive chosen ciphertext attacks.
* Encryption with associated data ensures authenticity (who the sender is) and integrity (the data
* has not been tampered with) of that data, but not its secrecy. (see <a
* href="https://tools.ietf.org/html/rfc5116">RFC 5116</a>)
* Encryption with associated data ensures authenticity (who the sender is) and integrity
* (the data has not been tampered with) of that data, but not its secrecy.
* (see <a href="https://tools.ietf.org/html/rfc5116">RFC 5116</a> for more info)
*/
public interface Aead {
/**
* Encrypts {@code plaintext} with {@code associatedData} as associated authenticated data. The
* resulting ciphertext allows for checking authenticity and integrity of associated data ({@code
* associatedData}), but does not guarantee its secrecy.
* Encrypts {@code plaintext} with {@code associatedData} as associated authenticated data.
* The resulting ciphertext allows for checking authenticity and integrity of associated data
* ({@code associatedData}), but does not guarantee its secrecy.
*
* @param plaintext the plaintext to be encrypted. It must be non-null, but can also
* be an empty (zero-length) byte array
* @param associatedData associated data to be authenticated, but not encrypted. Associated data
* is optional, so this parameter can be null. In this case the null value
* is equivalent to an empty (zero-length) byte array.
* For successful decryption the same associatedData must be provided
* along with the ciphertext.
* @return resulting ciphertext
*/
byte[] encrypt(final byte[] plaintext, final byte[] associatedData)
throws GeneralSecurityException;
/**
* Decrypts {@code ciphertext} with {@code associatedData} as associated authenticated data. The
* decryption verifies the authenticity and integrity of the associated data, but there are no
* guarantees wrt. secrecy of that data.
* Decrypts {@code ciphertext} with {@code associatedData} as associated authenticated data.
* The decryption verifies the authenticity and integrity of the associated data, but there are
* no guarantees wrt. secrecy of that data.
*
* @param ciphertext the plaintext to be decrypted. It must be non-null.
* @param associatedData associated data to be authenticated. For successful decryption
* it must be the same as associatedData used during encryption.
* Can be null, which is equivalent to an empty (zero-length) byte array.
* @return resulting plaintext
*/
byte[] decrypt(final byte[] ciphertext, final byte[] associatedData)
......
......@@ -167,7 +167,11 @@ public final class AesEaxJce implements Aead {
Cipher ecb = Cipher.getInstance("AES/ECB/NOPADDING");
ecb.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] n = omac(ecb, 0, iv, 0, iv.length);
byte[] h = omac(ecb, 1, associatedData, 0, associatedData.length);
byte[] aad = associatedData;
if (aad == null) {
aad = new byte[0];
}
byte[] h = omac(ecb, 1, aad, 0, aad.length);
Cipher ctr = Cipher.getInstance("AES/CTR/NOPADDING");
ctr.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(n));
ctr.doFinal(plaintext, 0, plaintext.length, ciphertext, ivSizeInBytes);
......@@ -190,7 +194,11 @@ public final class AesEaxJce implements Aead {
Cipher ecb = Cipher.getInstance("AES/ECB/NOPADDING");
ecb.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] n = omac(ecb, 0, ciphertext, 0, ivSizeInBytes);
byte[] h = omac(ecb, 1, associatedData, 0, associatedData.length);
byte[] aad = associatedData;
if (aad == null) {
aad = new byte[0];
}
byte[] h = omac(ecb, 1, aad, 0, aad.length);
byte[] t = omac(ecb, 2, ciphertext, ivSizeInBytes, plaintextLength);
byte res = 0;
int offset = ciphertext.length - TAG_SIZE_IN_BYTES;
......
......@@ -41,7 +41,8 @@ public final class AesGcmJce implements Aead {
}
@Override
public byte[] encrypt(final byte[] plaintext, final byte[] aad) throws GeneralSecurityException {
public byte[] encrypt(final byte[] plaintext, final byte[] associatedData)
throws GeneralSecurityException {
// Check that ciphertext is not longer than the max. size of a Java array.
if (plaintext.length > Integer.MAX_VALUE - IV_SIZE_IN_BYTES - TAG_SIZE_IN_BYTES) {
throw new GeneralSecurityException("plaintext too long");
......@@ -53,6 +54,10 @@ public final class AesGcmJce implements Aead {
Cipher cipher = instance();
GCMParameterSpec params = new GCMParameterSpec(8 * TAG_SIZE_IN_BYTES, iv);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, params);
byte[] aad = associatedData;
if (aad == null) {
aad = new byte[0];
}
cipher.updateAAD(aad);
int unusedWritten =
cipher.doFinal(plaintext, 0, plaintext.length, ciphertext, IV_SIZE_IN_BYTES);
......@@ -60,7 +65,8 @@ public final class AesGcmJce implements Aead {
}
@Override
public byte[] decrypt(final byte[] ciphertext, final byte[] aad) throws GeneralSecurityException {
public byte[] decrypt(final byte[] ciphertext, final byte[] associatedData)
throws GeneralSecurityException {
if (ciphertext.length < IV_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES) {
throw new GeneralSecurityException("ciphertext too short");
}
......@@ -68,6 +74,10 @@ public final class AesGcmJce implements Aead {
new GCMParameterSpec(8 * TAG_SIZE_IN_BYTES, ciphertext, 0, IV_SIZE_IN_BYTES);
Cipher cipher = instance();
cipher.init(Cipher.DECRYPT_MODE, keySpec, params);
byte[] aad = associatedData;
if (aad == null) {
aad = new byte[0];
}
cipher.updateAAD(aad);
return cipher.doFinal(ciphertext, IV_SIZE_IN_BYTES, ciphertext.length - IV_SIZE_IN_BYTES);
}
......
......@@ -53,8 +53,13 @@ public final class EncryptThenAuthenticate implements Aead {
* @return resulting ciphertext.
*/
@Override
public byte[] encrypt(final byte[] plaintext, final byte[] aad) throws GeneralSecurityException {
public byte[] encrypt(final byte[] plaintext, final byte[] associatedData)
throws GeneralSecurityException {
byte[] ciphertext = cipher.encrypt(plaintext);
byte[] aad = associatedData;
if (aad == null) {
aad = new byte[0];
}
byte[] aadLengthInBits =
Arrays.copyOf(ByteBuffer.allocate(8).putLong(8L * aad.length).array(), 8);
byte[] macValue = mac.computeMac(Bytes.concat(aad, ciphertext, aadLengthInBits));
......@@ -72,13 +77,18 @@ public final class EncryptThenAuthenticate implements Aead {
* @return resulting plaintext.
*/
@Override
public byte[] decrypt(final byte[] ciphertext, final byte[] aad) throws GeneralSecurityException {
public byte[] decrypt(final byte[] ciphertext, final byte[] associatedData)
throws GeneralSecurityException {
if (ciphertext.length < macLength) {
throw new GeneralSecurityException("ciphertext too short");
}
byte[] rawCiphertext = Arrays.copyOfRange(ciphertext, 0, ciphertext.length - macLength);
byte[] macValue =
Arrays.copyOfRange(ciphertext, ciphertext.length - macLength, ciphertext.length);
byte[] aad = associatedData;
if (aad == null) {
aad = new byte[0];
}
byte[] aadLengthInBits =
Arrays.copyOf(ByteBuffer.allocate(8).putLong(8L * aad.length).array(), 8);
mac.verifyMac(macValue, Bytes.concat(aad, rawCiphertext, aadLengthInBits));
......
......@@ -50,7 +50,7 @@ abstract class SnufflePoly1305 implements Aead {
throws InvalidKeyException;
/**
* Encrypts the {@code plaintext} with Poly1305 authentication based on {@code additionalData}.
* Encrypts the {@code plaintext} with Poly1305 authentication based on {@code associatedData}.
*
* <p>Please note that nonce is randomly generated hence keys need to be rotated after encrypting
* a certain number of messages depending on the nonce size of the underlying {@link Snuffle}.
......@@ -60,11 +60,11 @@ abstract class SnufflePoly1305 implements Aead {
* collusion.
*
* @param plaintext data to encrypt
* @param additionalData additional data
* @param associatedData associated authenticated data
* @return ciphertext with the following format {@code nonce || actual_ciphertext || tag}
*/
@Override
public byte[] encrypt(final byte[] plaintext, final byte[] additionalData)
public byte[] encrypt(final byte[] plaintext, final byte[] associatedData)
throws GeneralSecurityException {
if (plaintext.length > Integer.MAX_VALUE - snuffle.nonceSizeInBytes() - MAC_TAG_SIZE_IN_BYTES) {
throw new GeneralSecurityException("plaintext too long");
......@@ -72,11 +72,11 @@ abstract class SnufflePoly1305 implements Aead {
ByteBuffer ciphertext =
ByteBuffer.allocate(plaintext.length + snuffle.nonceSizeInBytes() + MAC_TAG_SIZE_IN_BYTES);
encrypt(ciphertext, plaintext, additionalData);
encrypt(ciphertext, plaintext, associatedData);
return ciphertext.array();
}
private void encrypt(ByteBuffer output, final byte[] plaintext, final byte[] additionalData)
private void encrypt(ByteBuffer output, final byte[] plaintext, final byte[] associatedData)
throws GeneralSecurityException {
if (output.remaining()
< plaintext.length + snuffle.nonceSizeInBytes() + MAC_TAG_SIZE_IN_BYTES) {
......@@ -88,7 +88,11 @@ abstract class SnufflePoly1305 implements Aead {
byte[] nonce = new byte[snuffle.nonceSizeInBytes()];
output.get(nonce);
output.limit(output.limit() - MAC_TAG_SIZE_IN_BYTES);
byte[] tag = Poly1305.computeMac(getMacKey(nonce), macDataRfc7539(additionalData, output));
byte[] aad = associatedData;
if (aad == null) {
aad = new byte[0];
}
byte[] tag = Poly1305.computeMac(getMacKey(nonce), macDataRfc7539(aad, output));
output.limit(output.limit() + MAC_TAG_SIZE_IN_BYTES);
output.put(tag);
}
......@@ -98,16 +102,16 @@ abstract class SnufflePoly1305 implements Aead {
* tag}
*
* @param ciphertext with format {@code nonce || actual_ciphertext || tag}
* @param additionalData additional data
* @param associatedData associated authenticated data
* @return plaintext if authentication is successful.
* @throws GeneralSecurityException when ciphertext is shorter than nonce size + tag size or when
* computed tag based on {@code ciphertext} and {@code additionalData} does not match the tag
* computed tag based on {@code ciphertext} and {@code associatedData} does not match the tag
* given in {@code ciphertext}.
*/
@Override
public byte[] decrypt(final byte[] ciphertext, final byte[] additionalData)
public byte[] decrypt(final byte[] ciphertext, final byte[] associatedData)
throws GeneralSecurityException {
return decrypt(ByteBuffer.wrap(ciphertext), additionalData);
return decrypt(ByteBuffer.wrap(ciphertext), associatedData);
}
/**
......@@ -115,12 +119,12 @@ abstract class SnufflePoly1305 implements Aead {
* tag}
*
* @param ciphertext with format {@code nonce || actual_ciphertext || tag}
* @param additionalData additional data
* @param associatedData associated authenticated data
* @return plaintext if authentication is successful
* @throws GeneralSecurityException when ciphertext is shorter than nonce size + tag size
* @throws AEADBadTagException when the tag is invalid
*/
private byte[] decrypt(ByteBuffer ciphertext, final byte[] additionalData)
private byte[] decrypt(ByteBuffer ciphertext, final byte[] associatedData)
throws GeneralSecurityException {
if (ciphertext.remaining() < snuffle.nonceSizeInBytes() + MAC_TAG_SIZE_IN_BYTES) {
throw new GeneralSecurityException("ciphertext too short");
......@@ -134,8 +138,12 @@ abstract class SnufflePoly1305 implements Aead {
ciphertext.limit(ciphertext.limit() - MAC_TAG_SIZE_IN_BYTES);
byte[] nonce = new byte[snuffle.nonceSizeInBytes()];
ciphertext.get(nonce);
byte[] aad = associatedData;
if (aad == null) {
aad = new byte[0];
}
try {
Poly1305.verifyMac(getMacKey(nonce), macDataRfc7539(additionalData, ciphertext), tag);
Poly1305.verifyMac(getMacKey(nonce), macDataRfc7539(aad, ciphertext), tag);
} catch (GeneralSecurityException ex) {
throw new AEADBadTagException(ex.toString());
}
......
......@@ -194,4 +194,74 @@ public class AesEaxJceTest {
}
}
}
@Test
public void testNullPlaintextOrCiphertext() throws Exception {
AesEaxJce eax = new AesEaxJce(Random.randBytes(KEY_SIZE), IV_SIZE);
try {
byte[] aad = new byte[] {1, 2, 3};
byte[] unused = eax.encrypt(null, aad);
fail("Encrypting a null plaintext should fail");
} catch (NullPointerException ex) {
// This is expected.
}
try {
byte[] unused = eax.encrypt(null, null);
fail("Encrypting a null plaintext should fail");
} catch (NullPointerException ex) {
// This is expected.
}
try {
byte[] aad = new byte[] {1, 2, 3};
byte[] unused = eax.decrypt(null, aad);
fail("Decrypting a null ciphertext should fail");
} catch (NullPointerException ex) {
// This is expected.
}
try {
byte[] unused = eax.decrypt(null, null);
fail("Decrypting a null ciphertext should fail");
} catch (NullPointerException ex) {
// This is expected.
}
}
@Test
public void testEmptyAssociatedData() throws Exception {
byte[] aad = new byte[0];
byte[] key = Random.randBytes(KEY_SIZE);
AesEaxJce eax = new AesEaxJce(key, IV_SIZE);
for (int messageSize = 0; messageSize < 75; messageSize++) {
byte[] message = Random.randBytes(messageSize);
{ // encrypting with aad as a 0-length array
byte[] ciphertext = eax.encrypt(message, aad);
byte[] decrypted = eax.decrypt(ciphertext, aad);
assertArrayEquals(message, decrypted);
byte[] decrypted2 = eax.decrypt(ciphertext, null);
assertArrayEquals(message, decrypted2);
try {
byte[] badAad = new byte[] {1, 2, 3};
byte[] unused = eax.decrypt(ciphertext, badAad);
fail("Decrypting with modified aad should fail");
} catch (AEADBadTagException ex) {
// This is expected.
}
}
{ // encrypting with aad equal to null
byte[] ciphertext = eax.encrypt(message, null);
byte[] decrypted = eax.decrypt(ciphertext, aad);
assertArrayEquals(message, decrypted);
byte[] decrypted2 = eax.decrypt(ciphertext, null);
assertArrayEquals(message, decrypted2);
try {
byte[] badAad = new byte[] {1, 2, 3};
byte[] unused = eax.decrypt(ciphertext, badAad);
fail("Decrypting with modified aad should fail");
} catch (AEADBadTagException ex) {
// This is expected.
}
}
}
}
}
......@@ -207,6 +207,79 @@ public class AesGcmJceTest {
assertEquals(numTests, cntTests + cntSkippedTests);
}
@Test
public void testNullPlaintextOrCiphertext() throws Exception {
for (int keySize : keySizeInBytes) {
AesGcmJce gcm = new AesGcmJce(Random.randBytes(keySize));
try {
byte[] aad = new byte[] {1, 2, 3};
byte[] unused = gcm.encrypt(null, aad);
fail("Encrypting a null plaintext should fail");
} catch (NullPointerException ex) {
// This is expected.
}
try {
byte[] unused = gcm.encrypt(null, null);
fail("Encrypting a null plaintext should fail");
} catch (NullPointerException ex) {
// This is expected.
}
try {
byte[] aad = new byte[] {1, 2, 3};
byte[] unused = gcm.decrypt(null, aad);
fail("Decrypting a null ciphertext should fail");
} catch (NullPointerException ex) {
// This is expected.
}
try {
byte[] unused = gcm.decrypt(null, null);
fail("Decrypting a null ciphertext should fail");
} catch (NullPointerException ex) {
// This is expected.
}
}
}
@Test
public void testEmptyAssociatedData() throws Exception {
byte[] aad = new byte[0];
for (int keySize : keySizeInBytes) {
byte[] key = Random.randBytes(keySize);
AesGcmJce gcm = new AesGcmJce(key);
for (int messageSize = 0; messageSize < 75; messageSize++) {
byte[] message = Random.randBytes(messageSize);
{ // encrypting with aad as a 0-length array
byte[] ciphertext = gcm.encrypt(message, aad);
byte[] decrypted = gcm.decrypt(ciphertext, aad);
assertArrayEquals(message, decrypted);
byte[] decrypted2 = gcm.decrypt(ciphertext, null);
assertArrayEquals(message, decrypted2);
try {
byte[] badAad = new byte[] {1, 2, 3};
byte[] unused = gcm.decrypt(ciphertext, badAad);
fail("Decrypting with modified aad should fail");
} catch (AEADBadTagException ex) {
// This is expected.
}
}
{ // encrypting with aad equal to null
byte[] ciphertext = gcm.encrypt(message, null);
byte[] decrypted = gcm.decrypt(ciphertext, aad);
assertArrayEquals(message, decrypted);
byte[] decrypted2 = gcm.decrypt(ciphertext, null);
assertArrayEquals(message, decrypted2);
try {
byte[] badAad = new byte[] {1, 2, 3};
byte[] unused = gcm.decrypt(ciphertext, badAad);
fail("Decrypting with modified aad should fail");
} catch (AEADBadTagException ex) {
// This is expected.
}
}
}
}
}
@Test
/**
* This is a very simple test for the randomness of the nonce. The test simply checks that the
......
......@@ -162,6 +162,74 @@ public class ChaCha20Poly1305Test {
}
}
@Test
public void testNullPlaintextOrCiphertext() throws Exception {
Aead aead = createInstance(Random.randBytes(KEY_SIZE));
try {
byte[] aad = new byte[] {1, 2, 3};
byte[] unused = aead.encrypt(null, aad);
fail("Encrypting a null plaintext should fail");
} catch (NullPointerException ex) {
// This is expected.
}
try {
byte[] unused = aead.encrypt(null, null);
fail("Encrypting a null plaintext should fail");
} catch (NullPointerException ex) {
// This is expected.
}
try {
byte[] aad = new byte[] {1, 2, 3};
byte[] unused = aead.decrypt(null, aad);
fail("Decrypting a null ciphertext should fail");
} catch (NullPointerException ex) {
// This is expected.
}
try {
byte[] unused = aead.decrypt(null, null);
fail("Decrypting a null ciphertext should fail");
} catch (NullPointerException ex) {
// This is expected.
}
}
@Test
public void testEmptyAssociatedData() throws Exception {
byte[] aad = new byte[0];
Aead aead = createInstance(Random.randBytes(KEY_SIZE));
for (int messageSize = 0; messageSize < 75; messageSize++) {
byte[] message = Random.randBytes(messageSize);
{ // encrypting with aad as a 0-length array
byte[] ciphertext = aead.encrypt(message, aad);
byte[] decrypted = aead.decrypt(ciphertext, aad);
assertArrayEquals(message, decrypted);
byte[] decrypted2 = aead.decrypt(ciphertext, null);
assertArrayEquals(message, decrypted2);
try {
byte[] badAad = new byte[] {1, 2, 3};
byte[] unused = aead.decrypt(ciphertext, badAad);
fail("Decrypting with modified aad should fail");
} catch (AEADBadTagException ex) {
// This is expected.
}
}
{ // encrypting with aad equal to null
byte[] ciphertext = aead.encrypt(message, null);
byte[] decrypted = aead.decrypt(ciphertext, aad);
assertArrayEquals(message, decrypted);
byte[] decrypted2 = aead.decrypt(ciphertext, null);
assertArrayEquals(message, decrypted2);
try {
byte[] badAad = new byte[] {1, 2, 3};
byte[] unused = aead.decrypt(ciphertext, badAad);
fail("Decrypting with modified aad should fail");
} catch (AEADBadTagException ex) {
// This is expected.
}
}
}
}
/**
* This is a very simple test for the randomness of the nonce. The test simply checks that the
* multiple ciphertexts of the same message are distinct.
......
......@@ -180,6 +180,72 @@ public class EncryptThenAuthenticateTest {
}
}
@Test
public void testNullPlaintextOrCiphertext() throws Exception {
Aead aead = getAead(Random.randBytes(16), Random.randBytes(16), 16, 16, "HMACSHA256");
try {
byte[] aad = new byte[] {1, 2, 3};
byte[] unused = aead.encrypt(null, aad);
fail("Encrypting a null plaintext should fail");
} catch (NullPointerException ex) {
// This is expected.
}
try {
byte[] unused = aead.encrypt(null, null);
fail("Encrypting a null plaintext should fail");
} catch (NullPointerException ex) {
// This is expected.
}
try {
byte[] aad = new byte[] {1, 2, 3};
byte[] unused = aead.decrypt(null, aad);
fail("Decrypting a null ciphertext should fail");
} catch (NullPointerException ex) {
// This is expected.
}
try {
byte[] unused = aead.decrypt(null, null);
fail("Decrypting a null ciphertext should fail");
} catch (NullPointerException ex) {
// This is expected.
}
}
@Test
public void testEmptyAssociatedData() throws Exception {
Aead aead = getAead(Random.randBytes(16), Random.randBytes(16), 16, 16, "HMACSHA256");
byte[] aad = new byte[0];
byte[] plaintext = Random.randBytes(1001);
{ // encrypting with aad as a 0-length array
byte[] ciphertext = aead.encrypt(plaintext, aad);
byte[] decrypted = aead.decrypt(ciphertext, aad);
assertArrayEquals(plaintext, decrypted);
byte[] decrypted2 = aead.decrypt(ciphertext, null);
assertArrayEquals(plaintext, decrypted2);
try {
byte[] badAad = new byte[] {1, 2, 3};
byte[] unused = aead.decrypt(ciphertext, badAad);
fail("Decrypting with modified aad should fail");
} catch (GeneralSecurityException expected) {
// This is expected.
}
}
{ // encrypting with aad equal to null
byte[] ciphertext = aead.encrypt(plaintext, null);
byte[] decrypted = aead.decrypt(ciphertext, aad);
assertArrayEquals(plaintext, decrypted);
byte[] decrypted2 = aead.decrypt(ciphertext, null);
assertArrayEquals(plaintext, decrypted2);
try {
byte[] badAad = new byte[] {1, 2, 3};
byte[] unused = aead.decrypt(ciphertext, badAad);
fail("Decrypting with modified aad should fail");
} catch (GeneralSecurityException expected) {
// This is expected.
}
}
}
@Test
public void testTruncation() throws Exception {
Aead aead = getAead(Random.randBytes(16), Random.randBytes(16), 16, 16, "HMACSHA256");
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment