Implement AES/GCM on top of AES/CTR

Older JDK's do not have built-in AES/GCM but they do have AES/CTR.
This commit is contained in:
Rhys Weatherley 2016-06-27 13:35:12 +10:00
parent 7901c7df23
commit 8b83fc5c27
6 changed files with 617 additions and 13 deletions

View File

@ -0,0 +1,204 @@
/*
* Copyright (C) 2016 Southern Storm Software, Pty Ltd.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
package com.southernstorm.noise.crypto;
import java.util.Arrays;
import com.southernstorm.noise.protocol.Destroyable;
/**
* Implementation of the GHASH primitive for GCM.
*/
public final class GHASH implements Destroyable {
private long[] H;
private byte[] Y;
int posn;
/**
* Constructs a new GHASH object.
*/
public GHASH()
{
H = new long [2];
Y = new byte [16];
posn = 0;
}
/**
* Resets this GHASH object with a new key.
*
* @param key The key, which must contain at least 16 bytes.
* @param offset The offset of the first key byte.
*/
public void reset(byte[] key, int offset)
{
H[0] = readBigEndian(key, offset);
H[1] = readBigEndian(key, offset + 8);
Arrays.fill(Y, (byte)0);
posn = 0;
}
/**
* Resets the GHASH object but retains the previous key.
*/
public void reset()
{
Arrays.fill(Y, (byte)0);
posn = 0;
}
/**
* Updates this GHASH object with more data.
*
* @param data Buffer containing the data.
* @param offset Offset of the first data byte in the buffer.
* @param length The number of bytes from the buffer to hash.
*/
public void update(byte[] data, int offset, int length)
{
while (length > 0) {
int size = 16 - posn;
if (size > length)
size = length;
for (int index = 0; index < size; ++index)
Y[posn + index] ^= data[offset + index];
posn += size;
length -= size;
offset += size;
if (posn == 16) {
GF128_mul(Y, H);
posn = 0;
}
}
}
/**
* Finishes the GHASH process and returns the tag.
*
* @param tag Buffer to receive the tag.
* @param offset Offset of the first byte of the tag.
* @param length The length of the tag, which must be less
* than or equal to 16.
*/
public void finish(byte[] tag, int offset, int length)
{
pad();
System.arraycopy(Y, 0, tag, offset, length);
}
/**
* Pads the input to a 16-byte boundary.
*/
public void pad()
{
if (posn != 0) {
// Padding involves XOR'ing the rest of state->Y with zeroes,
// which does nothing. Immediately process the next chunk.
GF128_mul(Y, H);
posn = 0;
}
}
/**
* Pads the input to a 16-byte boundary and then adds a block
* containing the AD and data lengths.
*
* @param adLen Length of the associated data in bytes.
* @param dataLen Length of the data in bytes.
*/
public void pad(long adLen, long dataLen)
{
byte[] temp = new byte [16];
try {
pad();
writeBigEndian(temp, 0, adLen * 8);
writeBigEndian(temp, 8, dataLen * 8);
update(temp, 0, 16);
} finally {
Arrays.fill(temp, (byte)0);
}
}
@Override
public void destroy() {
Arrays.fill(H, 0L);
Arrays.fill(Y, (byte)0);
}
private static long readBigEndian(byte[] buf, int offset)
{
return ((buf[offset] & 0xFFL) << 56) |
((buf[offset + 1] & 0xFFL) << 48) |
((buf[offset + 2] & 0xFFL) << 40) |
((buf[offset + 3] & 0xFFL) << 32) |
((buf[offset + 4] & 0xFFL) << 24) |
((buf[offset + 5] & 0xFFL) << 16) |
((buf[offset + 6] & 0xFFL) << 8) |
(buf[offset + 7] & 0xFFL);
}
private static void writeBigEndian(byte[] buf, int offset, long value)
{
buf[offset] = (byte)(value >> 56);
buf[offset + 1] = (byte)(value >> 48);
buf[offset + 2] = (byte)(value >> 40);
buf[offset + 3] = (byte)(value >> 32);
buf[offset + 4] = (byte)(value >> 24);
buf[offset + 5] = (byte)(value >> 16);
buf[offset + 6] = (byte)(value >> 8);
buf[offset + 7] = (byte)value;
}
private static void GF128_mul(byte[] Y, long[] H)
{
long Z0 = 0; // Z = 0
long Z1 = 0;
long V0 = H[0]; // V = H
long V1 = H[1];
// Multiply Z by V for the set bits in Y, starting at the top.
// This is a very simple bit by bit version that may not be very
// fast but it should be resistant to cache timing attacks.
for (int posn = 0; posn < 16; ++posn) {
int value = Y[posn] & 0xFF;
for (int bit = 7; bit >= 0; --bit) {
// Extract the high bit of "value" and turn it into a mask.
long mask = -((long)((value >> bit) & 0x01));
// XOR V with Z if the bit is 1.
Z0 ^= (V0 & mask);
Z1 ^= (V1 & mask);
// Rotate V right by 1 bit.
mask = ((~(V1 & 0x01)) + 1) & 0xE100000000000000L;
V1 = (V1 >>> 1) | (V0 << 63);
V0 = (V0 >>> 1) ^ mask;
}
}
// We have finished the block so copy Z into Y and byte-swap.
writeBigEndian(Y, 0, Z0);
writeBigEndian(Y, 8, Z1);
}
}

View File

@ -176,20 +176,29 @@ class AESGCMCipherState implements CipherState {
int ciphertextOffset, byte[] plaintext, int plaintextOffset,
int length) throws ShortBufferException, AEADBadTagException {
int space;
if (ciphertextOffset > ciphertext.length)
space = 0;
else
space = ciphertext.length - ciphertextOffset;
if (length > space)
throw new ShortBufferException();
if (plaintextOffset > plaintext.length)
space = 0;
else
space = plaintext.length - plaintextOffset;
if (length > space)
throw new ShortBufferException();
if (n < 0)
throw new IllegalStateException("Nonce has wrapped around");
if (keySpec == null) {
// The key is not set yet - return the ciphertext as-is.
if (plaintext != ciphertext || plaintextOffset != ciphertextOffset)
System.arraycopy(ciphertext, ciphertextOffset, plaintext, plaintextOffset, length);
return length;
}
if (length < 16)
throw new AEADBadTagException();
int dataLen = length - 16;
if (dataLen > space)
throw new ShortBufferException();
if (n < 0)
throw new IllegalStateException("Nonce has wrapped around");
try {
cipher.init(Cipher.DECRYPT_MODE, keySpec, createGCMParams());
} catch (InvalidKeyException e) {

View File

@ -0,0 +1,301 @@
/*
* Copyright (C) 2016 Southern Storm Software, Pty Ltd.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
package com.southernstorm.noise.protocol;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.AEADBadTagException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import com.southernstorm.noise.crypto.GHASH;
/**
* Emulates the "AESGCM" cipher for Noise using the "AES/CTR/NoPadding"
* transformation from JCA/JCE.
*
* This class is used on platforms that don't have "AES/GCM/NoPadding",
* but which do have the older "AES/CTR/NoPadding".
*/
class AESGCMOnCtrCipherState implements CipherState {
private Cipher cipher;
private SecretKeySpec keySpec;
private long n;
private byte[] iv;
private byte[] hashKey;
private GHASH ghash;
/**
* Constructs a new cipher state for the "AESGCM" algorithm.
*
* @throws NoSuchAlgorithmException The system does not have a
* provider for this algorithm.
*/
public AESGCMOnCtrCipherState() throws NoSuchAlgorithmException
{
try {
cipher = Cipher.getInstance("AES/CTR/NoPadding");
} catch (NoSuchPaddingException e) {
// AES/CTR is available, but not the unpadded version? Huh?
throw new NoSuchAlgorithmException("AES/CTR/NoPadding not available", e);
}
keySpec = null;
n = 0;
iv = new byte [16];
hashKey = new byte [16];
ghash = new GHASH();
}
@Override
public void destroy() {
// There doesn't seem to be a standard API to clean out a Cipher.
// So we instead set the key and IV to all-zeroes to hopefully
// destroy the sensitive data in the cipher instance.
ghash.destroy();
Noise.destroy(hashKey);
Noise.destroy(iv);
keySpec = new SecretKeySpec(new byte [32], "AES");
IvParameterSpec params = new IvParameterSpec(iv);
try {
cipher.init(Cipher.ENCRYPT_MODE, keySpec, params);
} catch (InvalidKeyException e) {
// Shouldn't happen.
} catch (InvalidAlgorithmParameterException e) {
// Shouldn't happen.
}
}
@Override
public String getCipherName() {
return "AESGCM";
}
@Override
public int getKeyLength() {
return 32;
}
@Override
public int getMACLength() {
return keySpec != null ? 16 : 0;
}
@Override
public void initializeKey(byte[] key, int offset) {
// Set the encryption key.
keySpec = new SecretKeySpec(key, offset, 32, "AES");
// Generate the hashing key by encrypting a block of zeroes.
Arrays.fill(iv, (byte)0);
Arrays.fill(hashKey, (byte)0);
try {
cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(iv));
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
// Shouldn't happen.
throw new IllegalStateException(e);
}
try {
int result = cipher.update(hashKey, 0, 16, hashKey, 0);
cipher.doFinal(hashKey, result);
} catch (ShortBufferException | IllegalBlockSizeException | BadPaddingException e) {
// Shouldn't happen.
throw new IllegalStateException(e);
}
ghash.reset(hashKey, 0);
// Reset the nonce.
n = 0;
}
@Override
public boolean hasKey() {
return keySpec != null;
}
/**
* Set up to encrypt or decrypt the next packet.
*
* @param ad The associated data for the packet.
*/
private void setup(byte[] ad) throws InvalidKeyException, InvalidAlgorithmParameterException
{
// Check for nonce wrap-around.
if (n < 0)
throw new IllegalStateException("Nonce has wrapped around");
// Format the counter/IV block for AES/CTR/NoPadding.
iv[0] = 0;
iv[1] = 0;
iv[2] = 0;
iv[3] = 0;
iv[4] = (byte)(n >> 56);
iv[5] = (byte)(n >> 48);
iv[6] = (byte)(n >> 40);
iv[7] = (byte)(n >> 32);
iv[8] = (byte)(n >> 24);
iv[9] = (byte)(n >> 16);
iv[10] = (byte)(n >> 8);
iv[11] = (byte)n;
iv[12] = 0;
iv[13] = 0;
iv[14] = 0;
iv[15] = 1;
++n;
// Initialize the CTR mode cipher with the key and IV.
cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(iv));
// Encrypt a block of zeroes to generate the hash key to XOR
// the GHASH tag with at the end of the encrypt/decrypt operation.
Arrays.fill(hashKey, (byte)0);
try {
cipher.update(hashKey, 0, 16, hashKey, 0);
} catch (ShortBufferException e) {
// Shouldn't happen.
throw new IllegalStateException(e);
}
// Initialize the GHASH with the associated data value.
ghash.reset();
if (ad != null) {
ghash.update(ad, 0, ad.length);
ghash.pad();
}
}
@Override
public int encryptWithAd(byte[] ad, byte[] plaintext, int plaintextOffset,
byte[] ciphertext, int ciphertextOffset, int length)
throws ShortBufferException {
int space;
if (ciphertextOffset > ciphertext.length)
space = 0;
else
space = ciphertext.length - ciphertextOffset;
if (keySpec == null) {
// The key is not set yet - return the plaintext as-is.
if (length > space)
throw new ShortBufferException();
if (plaintext != ciphertext || plaintextOffset != ciphertextOffset)
System.arraycopy(plaintext, plaintextOffset, ciphertext, ciphertextOffset, length);
return length;
}
if (space < 16 || length > (space - 16))
throw new ShortBufferException();
try {
setup(ad);
int result = cipher.update(plaintext, plaintextOffset, length, ciphertext, ciphertextOffset);
cipher.doFinal(ciphertext, ciphertextOffset + result);
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
// Shouldn't happen.
throw new IllegalStateException(e);
}
ghash.update(ciphertext, ciphertextOffset, length);
ghash.pad(ad != null ? ad.length : 0, length);
ghash.finish(ciphertext, ciphertextOffset + length, 16);
for (int index = 0; index < 16; ++index)
ciphertext[ciphertextOffset + length + index] ^= hashKey[index];
return length + 16;
}
@Override
public int decryptWithAd(byte[] ad, byte[] ciphertext,
int ciphertextOffset, byte[] plaintext, int plaintextOffset,
int length) throws ShortBufferException, AEADBadTagException {
int space;
if (ciphertextOffset > ciphertext.length)
space = 0;
else
space = ciphertext.length - ciphertextOffset;
if (length > space)
throw new ShortBufferException();
if (plaintextOffset > plaintext.length)
space = 0;
else
space = plaintext.length - plaintextOffset;
if (keySpec == null) {
// The key is not set yet - return the ciphertext as-is.
if (length > space)
throw new ShortBufferException();
if (plaintext != ciphertext || plaintextOffset != ciphertextOffset)
System.arraycopy(ciphertext, ciphertextOffset, plaintext, plaintextOffset, length);
return length;
}
if (length < 16)
throw new AEADBadTagException();
int dataLen = length - 16;
if (dataLen > space)
throw new ShortBufferException();
try {
setup(ad);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
// Shouldn't happen.
throw new IllegalStateException(e);
}
ghash.update(ciphertext, ciphertextOffset, dataLen);
ghash.pad(ad != null ? ad.length : 0, dataLen);
ghash.finish(iv, 0, 16);
int temp = 0;
for (int index = 0; index < 16; ++index)
temp |= (hashKey[index] ^ iv[index] ^ ciphertext[ciphertextOffset + dataLen + index]);
if (temp != 0)
throw new AEADBadTagException();
try {
int result = cipher.update(ciphertext, ciphertextOffset, dataLen, plaintext, plaintextOffset);
cipher.doFinal(plaintext, plaintextOffset + result);
} catch (IllegalBlockSizeException | BadPaddingException e) {
// Shouldn't happen.
throw new IllegalStateException(e);
}
return dataLen;
}
@Override
public CipherState fork(byte[] key, int offset) {
CipherState cipher;
try {
cipher = new AESGCMOnCtrCipherState();
} catch (NoSuchAlgorithmException e) {
// Shouldn't happen.
return null;
}
cipher.initializeKey(key, offset);
return cipher;
}
@Override
public void setNonce(long nonce) {
if (nonce < n)
throw new IllegalArgumentException("Nonce values cannot go backwards");
n = nonce;
}
}

View File

@ -83,10 +83,22 @@ public final class Noise {
*/
public static CipherState createCipher(String name) throws NoSuchAlgorithmException
{
if (name.equals("AESGCM"))
return new AESGCMCipherState();
else if (name.equals("ChaChaPoly"))
if (name.equals("AESGCM")) {
try {
return new AESGCMCipherState();
} catch (NoSuchAlgorithmException e) {
// The JCA/JCE does not have "AES/GCM/NoPadding" so try
// emulating it on top of "AES/CTR/NoPadding" instead.
try {
return new AESGCMOnCtrCipherState();
} catch (NoSuchAlgorithmException e1) {
// Re-throw the original "GCM not found" exception".
throw e;
}
}
} else if (name.equals("ChaChaPoly")) {
return new ChaChaPolyCipherState();
}
throw new NoSuchAlgorithmException("Unknown Noise cipher algorithm name: " + name);
}

View File

@ -23,7 +23,6 @@
package com.southernstorm.noise.tests;
import static org.junit.Assert.*;
import static org.junit.Assume.*;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
@ -49,18 +48,19 @@ public class CipherStateTests {
byte[] keyBytes = TestUtils.stringToData(key);
byte[] adBytes = TestUtils.stringToData(ad);
byte[] plaintextBytes = TestUtils.stringToData(plaintext);
byte[] ciphertextBytes = TestUtils.stringToData(ciphertext + mac.substring(2));
byte[] ciphertextBytes;
byte[] buffer;
if (ciphertext.length() > 0)
ciphertextBytes = TestUtils.stringToData(ciphertext + mac.substring(2));
else
ciphertextBytes = TestUtils.stringToData(mac);
// Create the cipher object and check its properties.
CipherState cipher = null;
try {
cipher = Noise.createCipher(name);
} catch (NoSuchAlgorithmException e) {
// FIXME: AES/GCM/NoPadding not supported in JavaSE-1.7.
// Remove this once we have a suitable replacement.
assumeNoException(e);
//fail(name + " cipher is not supported");
fail(name + " cipher is not supported");
}
assertEquals(name, cipher.getCipherName());
assertEquals(keyLen, cipher.getKeyLength());

View File

@ -0,0 +1,78 @@
/*
* Copyright (C) 2016 Southern Storm Software, Pty Ltd.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
package com.southernstorm.noise.tests;
import static org.junit.Assert.*;
import java.util.Arrays;
import org.junit.Test;
import com.southernstorm.noise.crypto.GHASH;
public class GHASHTests {
private void testGHASH(String key, String data, String hash)
{
byte[] keyBytes = TestUtils.stringToData(key);
byte[] dataBytes = TestUtils.stringToData(data);
byte[] hashBytes = TestUtils.stringToData(hash);
byte[] tag = new byte [16];
GHASH ghash = new GHASH();
ghash.reset(keyBytes, 0);
ghash.update(dataBytes, 0, dataBytes.length);
Arrays.fill(tag, (byte)0xAA);
ghash.finish(tag, 0, 16);
assertArrayEquals(hashBytes, tag);
ghash.reset();
ghash.update(dataBytes, 0, dataBytes.length / 3);
ghash.update(dataBytes, dataBytes.length / 3, dataBytes.length - (dataBytes.length / 3));
Arrays.fill(tag, (byte)0xAA);
ghash.finish(tag, 0, 16);
assertArrayEquals(hashBytes, tag);
}
@Test
public void ghash() {
// Test vectors from Appendix B of:
// http://csrc.nist.gov/groups/ST/toolkit/BCM/documents/proposedmodes/gcm/gcm-revised-spec.pdf
testGHASH("0x66e94bd4ef8a2c3b884cfa59ca342b2e",
"0x00000000000000000000000000000000",
"0x00000000000000000000000000000000");
testGHASH("0x66e94bd4ef8a2c3b884cfa59ca342b2e",
"0x0388dace60b6a392f328c2b971b2fe7800000000000000000000000000000080",
"0xf38cbb1ad69223dcc3457ae5b6b0f885");
testGHASH("0xb83b533708bf535d0aa6e52980d53b78",
"0x42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e21d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e091473f598500000000000000000000000000000200",
"0x7f1b32b81b820d02614f8895ac1d4eac");
testGHASH("0xb83b533708bf535d0aa6e52980d53b78",
"0xfeedfacedeadbeeffeedfacedeadbeefabaddad200000000000000000000000042831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e21d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e0910000000000000000000000a000000000000001e0",
"0x698e57f70e6ecc7fd9463b7260a9ae5f");
}
}