diff --git a/NoiseJava/src/com/southernstorm/noise/crypto/GHASH.java b/NoiseJava/src/com/southernstorm/noise/crypto/GHASH.java new file mode 100644 index 0000000..34c5c31 --- /dev/null +++ b/NoiseJava/src/com/southernstorm/noise/crypto/GHASH.java @@ -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); + } +} diff --git a/NoiseJava/src/com/southernstorm/noise/protocol/AESGCMCipherState.java b/NoiseJava/src/com/southernstorm/noise/protocol/AESGCMCipherState.java index bfeb9c2..a9fbfcf 100644 --- a/NoiseJava/src/com/southernstorm/noise/protocol/AESGCMCipherState.java +++ b/NoiseJava/src/com/southernstorm/noise/protocol/AESGCMCipherState.java @@ -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) { diff --git a/NoiseJava/src/com/southernstorm/noise/protocol/AESGCMOnCtrCipherState.java b/NoiseJava/src/com/southernstorm/noise/protocol/AESGCMOnCtrCipherState.java new file mode 100644 index 0000000..2172b19 --- /dev/null +++ b/NoiseJava/src/com/southernstorm/noise/protocol/AESGCMOnCtrCipherState.java @@ -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; + } +} diff --git a/NoiseJava/src/com/southernstorm/noise/protocol/Noise.java b/NoiseJava/src/com/southernstorm/noise/protocol/Noise.java index d8d481c..c2fcd88 100644 --- a/NoiseJava/src/com/southernstorm/noise/protocol/Noise.java +++ b/NoiseJava/src/com/southernstorm/noise/protocol/Noise.java @@ -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); } diff --git a/NoiseJavaTests/src/com/southernstorm/noise/tests/CipherStateTests.java b/NoiseJavaTests/src/com/southernstorm/noise/tests/CipherStateTests.java index 53e6ab6..394fe44 100644 --- a/NoiseJavaTests/src/com/southernstorm/noise/tests/CipherStateTests.java +++ b/NoiseJavaTests/src/com/southernstorm/noise/tests/CipherStateTests.java @@ -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()); diff --git a/NoiseJavaTests/src/com/southernstorm/noise/tests/GHASHTests.java b/NoiseJavaTests/src/com/southernstorm/noise/tests/GHASHTests.java new file mode 100644 index 0000000..36ad0a3 --- /dev/null +++ b/NoiseJavaTests/src/com/southernstorm/noise/tests/GHASHTests.java @@ -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"); + } +}