From ebdf37ed637ba0139f799c651f3b9bbddf6eec29 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sat, 14 Feb 2015 18:20:17 +0100 Subject: [PATCH] RFC 5297 AEAD_AES_SIV_CMAC_256 --- .../crypto/aes256/AesSivCipherUtil.java | 205 ++++++++++++++++++ .../crypto/aes256/AesSivCipherUtilTest.java | 150 +++++++++++++ 2 files changed, 355 insertions(+) create mode 100644 main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesSivCipherUtil.java create mode 100644 main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/AesSivCipherUtilTest.java diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesSivCipherUtil.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesSivCipherUtil.java new file mode 100644 index 000000000..790304049 --- /dev/null +++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesSivCipherUtil.java @@ -0,0 +1,205 @@ +package org.cryptomator.crypto.aes256; + +import java.nio.ByteBuffer; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.util.Arrays; + +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.lang3.ArrayUtils; +import org.bouncycastle.crypto.BlockCipher; +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.Mac; +import org.bouncycastle.crypto.engines.AESFastEngine; +import org.bouncycastle.crypto.macs.CMac; +import org.bouncycastle.crypto.paddings.ISO7816d4Padding; +import org.bouncycastle.crypto.params.KeyParameter; +import org.cryptomator.crypto.exceptions.DecryptFailedException; + +/** + * Implements the RFC 5297 SIV mode. + */ +final class AesSivCipherUtil { + + private static final byte[] BYTES_ZERO = new byte[16]; + private static final byte DOUBLING_CONST = (byte) 0x87; + + static byte[] sivEncrypt(byte[] key, byte[] plaintext, byte[]... additionalData) throws InvalidKeyException { + if (key.length != 32 && key.length != 48 && key.length != 64) { + throw new InvalidKeyException("Invalid key length " + key.length); + } + + final byte[] k1 = Arrays.copyOf(key, key.length / 2); + final byte[] k2 = Arrays.copyOfRange(key, k1.length, key.length); + + final byte[] iv = s2v(k1, plaintext, additionalData); + + final int numBlocks = (plaintext.length + 15) / 16; + + // clear out the 31st and 63rd (rightmost) bit: + final byte[] ctr = Arrays.copyOf(iv, 16); + ctr[8] = (byte) (ctr[8] & 0x7F); + ctr[12] = (byte) (ctr[12] & 0x7F); + final ByteBuffer ctrBuf = ByteBuffer.wrap(ctr); + final long initialCtrVal = ctrBuf.getLong(8); + + final byte[] x = new byte[numBlocks * 16]; + final BlockCipher aes = new AESFastEngine(); + aes.init(true, new KeyParameter(k2)); + for (int i = 0; i < numBlocks; i++) { + final long ctrVal = initialCtrVal + i; + ctrBuf.putLong(8, ctrVal); + aes.processBlock(ctrBuf.array(), 0, x, i * 16); + aes.reset(); + } + + final byte[] ciphertext = xorbegin(plaintext, x); + + return ArrayUtils.addAll(iv, ciphertext); + } + + static byte[] sivDecrypt(byte[] key, byte[] ciphertext, byte[]... additionalData) throws DecryptFailedException, InvalidKeyException { + if (key.length != 32 && key.length != 48 && key.length != 64) { + throw new InvalidKeyException("Invalid key length " + key.length); + } + + final byte[] k1 = Arrays.copyOf(key, key.length / 2); + final byte[] k2 = Arrays.copyOfRange(key, k1.length, key.length); + + final byte[] iv = Arrays.copyOf(ciphertext, 16); + + final byte[] actualCiphertext = Arrays.copyOfRange(ciphertext, 16, ciphertext.length); + final int numBlocks = (actualCiphertext.length + 15) / 16; + + // clear out the 31st and 63rd (rightmost) bit: + final byte[] ctr = Arrays.copyOf(iv, 16); + ctr[8] = (byte) (ctr[8] & 0x7F); + ctr[12] = (byte) (ctr[12] & 0x7F); + final ByteBuffer ctrBuf = ByteBuffer.wrap(ctr); + final long initialCtrVal = ctrBuf.getLong(8); + + final byte[] x = new byte[numBlocks * 16]; + final BlockCipher aes = new AESFastEngine(); + aes.init(true, new KeyParameter(k2)); + for (int i = 0; i < numBlocks; i++) { + final long ctrVal = initialCtrVal + i; + ctrBuf.putLong(8, ctrVal); + aes.processBlock(ctrBuf.array(), 0, x, i * 16); + aes.reset(); + } + + final byte[] plaintext = xorbegin(actualCiphertext, x); + + Hex.encodeHexString(actualCiphertext); + + final byte[] control = s2v(k1, plaintext, additionalData); + + if (MessageDigest.isEqual(control, iv)) { + return plaintext; + } else { + throw new DecryptFailedException("Authentication failed"); + } + } + + static byte[] s2v(byte[] key, byte[] plaintext, byte[]... additionalData) { + final CipherParameters params = new KeyParameter(key); + final BlockCipher aes = new AESFastEngine(); + final CMac mac = new CMac(aes); + mac.init(params); + + byte[] d = mac(mac, BYTES_ZERO); + + for (byte[] s : additionalData) { + d = xor(dbl(d), mac(mac, s)); + } + + final byte[] t; + if (plaintext.length >= 16) { + t = xorend(plaintext, d); + } else { + t = xor(dbl(d), pad(plaintext)); + } + + return mac(mac, t); + } + + private static byte[] mac(Mac mac, byte[] in) { + byte[] result = new byte[mac.getMacSize()]; + mac.update(in, 0, in.length); + mac.doFinal(result, 0); + return result; + } + + /** + * First bit 1, following bits 0. + */ + private static byte[] pad(byte[] in) { + final byte[] result = Arrays.copyOf(in, 16); + new ISO7816d4Padding().addPadding(result, in.length); + return result; + } + + /** + * Code taken from {@link org.bouncycastle.crypto.macs.CMac} + */ + private static int shiftLeft(byte[] block, byte[] output) { + int i = block.length; + int bit = 0; + while (--i >= 0) { + int b = block[i] & 0xff; + output[i] = (byte) ((b << 1) | bit); + bit = (b >>> 7) & 1; + } + return bit; + } + + /** + * Code taken from {@link org.bouncycastle.crypto.macs.CMac} + */ + private static byte[] dbl(byte[] in) { + byte[] ret = new byte[in.length]; + int carry = shiftLeft(in, ret); + int xor = 0xff & DOUBLING_CONST; + + /* + * NOTE: This construction is an attempt at a constant-time implementation. + */ + ret[in.length - 1] ^= (xor >>> ((1 - carry) << 3)); + + return ret; + } + + private static byte[] xor(byte[] in1, byte[] in2) { + if (in1 == null || in2 == null || in1.length != in2.length) { + throw new IllegalArgumentException("Inputs must equal in length."); + } + + return xorbegin(in1, in2); + } + + private static byte[] xorend(byte[] in1, byte[] in2) { + if (in1 == null || in2 == null || in1.length < in2.length) { + throw new IllegalArgumentException("Length of first input must be >= length of second input."); + } + + final byte[] result = new byte[in2.length]; + final int diff = in1.length - in2.length; + for (int i = in2.length - 1; i >= diff; i--) { + result[i] = (byte) (in1[i + diff] ^ in2[i]); + } + return result; + } + + private static byte[] xorbegin(byte[] in1, byte[] in2) { + if (in1 == null || in2 == null || in1.length > in2.length) { + throw new IllegalArgumentException("Length of first input must be <= length of second input."); + } + + final byte[] result = new byte[in1.length]; + for (int i = 0; i < result.length; i++) { + result[i] = (byte) (in1[i] ^ in2[i]); + } + return result; + } + +} diff --git a/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/AesSivCipherUtilTest.java b/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/AesSivCipherUtilTest.java new file mode 100644 index 000000000..c122b94b8 --- /dev/null +++ b/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/AesSivCipherUtilTest.java @@ -0,0 +1,150 @@ +package org.cryptomator.crypto.aes256; + +import java.security.InvalidKeyException; + +import org.apache.commons.codec.DecoderException; +import org.cryptomator.crypto.exceptions.DecryptFailedException; +import org.junit.Assert; +import org.junit.Test; + +/** + * Official RFC 5297 test vector taken from https://tools.ietf.org/html/rfc5297#appendix-A.1 + */ +public class AesSivCipherUtilTest { + + @Test + public void testS2v() throws DecoderException { + final byte[] key = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, // + (byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, // + (byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, // + (byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0}; + + final byte[] ad = {(byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, // + (byte) 0x14, (byte) 0x15, (byte) 0x16, (byte) 0x17, // + (byte) 0x18, (byte) 0x19, (byte) 0x1a, (byte) 0x1b, // + (byte) 0x1c, (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, // + (byte) 0x20, (byte) 0x21, (byte) 0x22, (byte) 0x23, // + (byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27}; + + final byte[] plaintext = {(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, // + (byte) 0x55, (byte) 0x66, (byte) 0x77, (byte) 0x88, // + (byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, // + (byte) 0xdd, (byte) 0xee}; + + final byte[] expected = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, // + (byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, // + (byte) 0x95, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, // + (byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93}; + + final byte[] result = AesSivCipherUtil.s2v(key, plaintext, ad); + Assert.assertArrayEquals(expected, result); + } + + @Test + public void testSivEncrypt() throws InvalidKeyException { + final byte[] key = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, // + (byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, // + (byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, // + (byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0, // + (byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, // + (byte) 0xf4, (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, // + (byte) 0xf8, (byte) 0xf9, (byte) 0xfa, (byte) 0xfb, // + (byte) 0xfc, (byte) 0xfd, (byte) 0xfe, (byte) 0xff}; + + final byte[] ad = {(byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, // + (byte) 0x14, (byte) 0x15, (byte) 0x16, (byte) 0x17, // + (byte) 0x18, (byte) 0x19, (byte) 0x1a, (byte) 0x1b, // + (byte) 0x1c, (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, // + (byte) 0x20, (byte) 0x21, (byte) 0x22, (byte) 0x23, // + (byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27}; + + final byte[] plaintext = {(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, // + (byte) 0x55, (byte) 0x66, (byte) 0x77, (byte) 0x88, // + (byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, // + (byte) 0xdd, (byte) 0xee}; + + final byte[] expected = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, // + (byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, // + (byte) 0x95, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, // + (byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93, // + (byte) 0x40, (byte) 0xc0, (byte) 0x2b, (byte) 0x96, // + (byte) 0x90, (byte) 0xc4, (byte) 0xdc, (byte) 0x04, // + (byte) 0xda, (byte) 0xef, (byte) 0x7f, (byte) 0x6a, // + (byte) 0xfe, (byte) 0x5c}; + + final byte[] result = AesSivCipherUtil.sivEncrypt(key, plaintext, ad); + Assert.assertArrayEquals(expected, result); + } + + @Test + public void testSivDecrypt() throws DecryptFailedException, InvalidKeyException { + final byte[] key = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, // + (byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, // + (byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, // + (byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0, // + (byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, // + (byte) 0xf4, (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, // + (byte) 0xf8, (byte) 0xf9, (byte) 0xfa, (byte) 0xfb, // + (byte) 0xfc, (byte) 0xfd, (byte) 0xfe, (byte) 0xff}; + + final byte[] ad = {(byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, // + (byte) 0x14, (byte) 0x15, (byte) 0x16, (byte) 0x17, // + (byte) 0x18, (byte) 0x19, (byte) 0x1a, (byte) 0x1b, // + (byte) 0x1c, (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, // + (byte) 0x20, (byte) 0x21, (byte) 0x22, (byte) 0x23, // + (byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27}; + + final byte[] ciphertext = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, // + (byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, // + (byte) 0x95, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, // + (byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93, // + (byte) 0x40, (byte) 0xc0, (byte) 0x2b, (byte) 0x96, // + (byte) 0x90, (byte) 0xc4, (byte) 0xdc, (byte) 0x04, // + (byte) 0xda, (byte) 0xef, (byte) 0x7f, (byte) 0x6a, // + (byte) 0xfe, (byte) 0x5c}; + + final byte[] expected = {(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, // + (byte) 0x55, (byte) 0x66, (byte) 0x77, (byte) 0x88, // + (byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, // + (byte) 0xdd, (byte) 0xee}; + + final byte[] result = AesSivCipherUtil.sivDecrypt(key, ciphertext, ad); + Assert.assertArrayEquals(expected, result); + } + + @Test(expected = DecryptFailedException.class) + public void testSivDecryptWithInvalidKey() throws DecryptFailedException, InvalidKeyException { + final byte[] key = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, // + (byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, // + (byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, // + (byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0, // + (byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, // + (byte) 0xf4, (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, // + (byte) 0xf8, (byte) 0xf9, (byte) 0xfa, (byte) 0xfb, // + (byte) 0xfc, (byte) 0xfd, (byte) 0xfe, (byte) 0x00}; + + final byte[] ad = {(byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, // + (byte) 0x14, (byte) 0x15, (byte) 0x16, (byte) 0x17, // + (byte) 0x18, (byte) 0x19, (byte) 0x1a, (byte) 0x1b, // + (byte) 0x1c, (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, // + (byte) 0x20, (byte) 0x21, (byte) 0x22, (byte) 0x23, // + (byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27}; + + final byte[] ciphertext = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, // + (byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, // + (byte) 0x95, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, // + (byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93, // + (byte) 0x40, (byte) 0xc0, (byte) 0x2b, (byte) 0x96, // + (byte) 0x90, (byte) 0xc4, (byte) 0xdc, (byte) 0x04, // + (byte) 0xda, (byte) 0xef, (byte) 0x7f, (byte) 0x6a, // + (byte) 0xfe, (byte) 0x5c}; + + final byte[] expected = {(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, // + (byte) 0x55, (byte) 0x66, (byte) 0x77, (byte) 0x88, // + (byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, // + (byte) 0xdd, (byte) 0xee}; + + final byte[] result = AesSivCipherUtil.sivDecrypt(key, ciphertext, ad); + Assert.assertArrayEquals(expected, result); + } +}