From f3f0c223c4d38827849df8561f0543b9a0095989 Mon Sep 17 00:00:00 2001 From: akwizgran Date: Tue, 9 Aug 2011 17:50:54 +0100 Subject: [PATCH] PacketWriter is implemented by two classes: PacketWriterImpl and PacketEncrypter. The separation allows authentication and encryption to be tested separately. --- .../sf/briar/api/crypto/CryptoComponent.java | 5 ++ .../sf/briar/api/transport/PacketWriter.java | 4 +- .../sf/briar/crypto/CryptoComponentImpl.java | 28 ++++++ .../sf/briar/transport/PacketEncrypter.java | 13 +++ .../briar/transport/PacketEncrypterImpl.java | 86 +++++++++++++++++++ .../sf/briar/transport/PacketWriterImpl.java | 19 ++-- test/build.xml | 1 + .../transport/PacketEncrypterImplTest.java | 76 ++++++++++++++++ .../briar/transport/PacketWriterImplTest.java | 55 +++++++++++- 9 files changed, 274 insertions(+), 13 deletions(-) create mode 100644 components/net/sf/briar/transport/PacketEncrypter.java create mode 100644 components/net/sf/briar/transport/PacketEncrypterImpl.java create mode 100644 test/net/sf/briar/transport/PacketEncrypterImplTest.java diff --git a/api/net/sf/briar/api/crypto/CryptoComponent.java b/api/net/sf/briar/api/crypto/CryptoComponent.java index e73a7e78b..5cc912dc9 100644 --- a/api/net/sf/briar/api/crypto/CryptoComponent.java +++ b/api/net/sf/briar/api/crypto/CryptoComponent.java @@ -4,6 +4,7 @@ import java.security.KeyPair; import java.security.MessageDigest; import java.security.Signature; +import javax.crypto.Cipher; import javax.crypto.Mac; import javax.crypto.SecretKey; @@ -19,5 +20,9 @@ public interface CryptoComponent { MessageDigest getMessageDigest(); + Cipher getPacketCipher(); + Signature getSignature(); + + Cipher getTagCipher(); } diff --git a/api/net/sf/briar/api/transport/PacketWriter.java b/api/net/sf/briar/api/transport/PacketWriter.java index c6e3b7ea9..f88bd02eb 100644 --- a/api/net/sf/briar/api/transport/PacketWriter.java +++ b/api/net/sf/briar/api/transport/PacketWriter.java @@ -4,8 +4,8 @@ import java.io.IOException; import java.io.OutputStream; /** - * A filter that adds tags and MACs to outgoing packets. Encryption is handled - * by the underlying output stream. + * A filter that adds tags and MACs to outgoing packets, encrypts them and + * writes them to the underlying output stream. */ public interface PacketWriter { diff --git a/components/net/sf/briar/crypto/CryptoComponentImpl.java b/components/net/sf/briar/crypto/CryptoComponentImpl.java index caf2e24ad..7e4d87525 100644 --- a/components/net/sf/briar/crypto/CryptoComponentImpl.java +++ b/components/net/sf/briar/crypto/CryptoComponentImpl.java @@ -8,8 +8,10 @@ import java.security.NoSuchProviderException; import java.security.Security; import java.security.Signature; +import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.Mac; +import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import net.sf.briar.api.crypto.CryptoComponent; @@ -24,9 +26,11 @@ class CryptoComponentImpl implements CryptoComponent { private static final String KEY_PAIR_ALGO = "ECDSA"; private static final int KEY_PAIR_KEYSIZE = 256; // Bits private static final String MAC_ALGO = "HMacSHA256"; + private static final String PACKET_CIPHER_ALGO = "AES/CTR/NoPadding"; private static final String SECRET_KEY_ALGO = "AES"; private static final int SECRET_KEY_KEYSIZE = 256; // Bits private static final String SIGNATURE_ALGO = "ECDSA"; + private static final String TAG_CIPHER_ALGO = "AES/ECB/NoPadding"; private final KeyParser keyParser; private final KeyPairGenerator keyPairGenerator; @@ -81,6 +85,18 @@ class CryptoComponentImpl implements CryptoComponent { } } + public Cipher getPacketCipher() { + try { + return Cipher.getInstance(PACKET_CIPHER_ALGO, PROVIDER); + } catch(NoSuchAlgorithmException impossible) { + throw new RuntimeException(impossible); + } catch(NoSuchPaddingException impossible) { + throw new RuntimeException(impossible); + } catch(NoSuchProviderException impossible) { + throw new RuntimeException(impossible); + } + } + public Signature getSignature() { try { return Signature.getInstance(SIGNATURE_ALGO, PROVIDER); @@ -90,4 +106,16 @@ class CryptoComponentImpl implements CryptoComponent { throw new RuntimeException(impossible); } } + + public Cipher getTagCipher() { + try { + return Cipher.getInstance(TAG_CIPHER_ALGO, PROVIDER); + } catch(NoSuchAlgorithmException impossible) { + throw new RuntimeException(impossible); + } catch(NoSuchPaddingException impossible) { + throw new RuntimeException(impossible); + } catch(NoSuchProviderException impossible) { + throw new RuntimeException(impossible); + } + } } diff --git a/components/net/sf/briar/transport/PacketEncrypter.java b/components/net/sf/briar/transport/PacketEncrypter.java new file mode 100644 index 000000000..17543dace --- /dev/null +++ b/components/net/sf/briar/transport/PacketEncrypter.java @@ -0,0 +1,13 @@ +package net.sf.briar.transport; + +import java.io.IOException; +import java.io.OutputStream; + +interface PacketEncrypter { + + OutputStream getOutputStream(); + + void writeTag(byte[] tag) throws IOException; + + void finishPacket() throws IOException; +} diff --git a/components/net/sf/briar/transport/PacketEncrypterImpl.java b/components/net/sf/briar/transport/PacketEncrypterImpl.java new file mode 100644 index 000000000..ebde47713 --- /dev/null +++ b/components/net/sf/briar/transport/PacketEncrypterImpl.java @@ -0,0 +1,86 @@ +package net.sf.briar.transport; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; + +class PacketEncrypterImpl extends FilterOutputStream +implements PacketEncrypter { + + private final OutputStream out; + private final Cipher tagCipher, packetCipher; + private final SecretKey packetKey; + + PacketEncrypterImpl(OutputStream out, Cipher tagCipher, + Cipher packetCipher, SecretKey tagKey, SecretKey packetKey) { + super(out); + this.out = out; + this.tagCipher = tagCipher; + this.packetCipher = packetCipher; + this.packetKey = packetKey; + try { + tagCipher.init(Cipher.ENCRYPT_MODE, tagKey); + } catch(InvalidKeyException e) { + throw new IllegalArgumentException(e); + } + if(tagCipher.getOutputSize(16) != 16) + throw new IllegalArgumentException(); + } + + public OutputStream getOutputStream() { + return this; + } + + public void writeTag(byte[] tag) throws IOException { + if(tag.length != 16) throw new IllegalArgumentException(); + IvParameterSpec iv = new IvParameterSpec(tag); + try { + out.write(tagCipher.doFinal(tag)); + packetCipher.init(Cipher.ENCRYPT_MODE, packetKey, iv); + } catch(BadPaddingException badCipher) { + throw new IOException(badCipher); + } catch(IllegalBlockSizeException badCipher) { + throw new RuntimeException(badCipher); + } catch(InvalidAlgorithmParameterException badIv) { + throw new RuntimeException(badIv); + } catch(InvalidKeyException badKey) { + throw new RuntimeException(badKey); + } + } + + public void finishPacket() throws IOException { + try { + out.write(packetCipher.doFinal()); + } catch(BadPaddingException badCipher) { + throw new IOException(badCipher); + } catch(IllegalBlockSizeException badCipher) { + throw new RuntimeException(badCipher); + } + } + + @Override + public void write(int b) throws IOException { + byte[] ciphertext = packetCipher.update(new byte[] {(byte) b}); + if(ciphertext != null) out.write(ciphertext); + } + + @Override + public void write(byte[] b) throws IOException { + byte[] ciphertext = packetCipher.update(b); + if(ciphertext != null) out.write(ciphertext); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + byte[] ciphertext = packetCipher.update(b, off, len); + if(ciphertext != null) out.write(ciphertext); + } +} diff --git a/components/net/sf/briar/transport/PacketWriterImpl.java b/components/net/sf/briar/transport/PacketWriterImpl.java index 7642b7039..c0f66d40e 100644 --- a/components/net/sf/briar/transport/PacketWriterImpl.java +++ b/components/net/sf/briar/transport/PacketWriterImpl.java @@ -13,6 +13,7 @@ class PacketWriterImpl extends FilterOutputStream implements PacketWriter { private static final int MAX_16_BIT_UNSIGNED = 65535; // 2^16 - 1 private static final long MAX_32_BIT_UNSIGNED = 4294967295L; // 2^32 - 1 + private final PacketEncrypter encrypter; private final Mac mac; private final int transportIdentifier; private final long connectionNumber; @@ -20,9 +21,10 @@ class PacketWriterImpl extends FilterOutputStream implements PacketWriter { private long packetNumber = 0L; private boolean betweenPackets = true; - PacketWriterImpl(OutputStream out, Mac mac, int transportIdentifier, - long connectionNumber) { - super(out); + PacketWriterImpl(PacketEncrypter encrypter, Mac mac, + int transportIdentifier, long connectionNumber) { + super(encrypter.getOutputStream()); + this.encrypter = encrypter; this.mac = mac; if(transportIdentifier < 0) throw new IllegalArgumentException(); if(transportIdentifier > MAX_16_BIT_UNSIGNED) @@ -65,6 +67,7 @@ class PacketWriterImpl extends FilterOutputStream implements PacketWriter { private void writeMac() throws IOException { out.write(mac.doFinal()); + encrypter.finishPacket(); betweenPackets = true; } @@ -78,14 +81,15 @@ class PacketWriterImpl extends FilterOutputStream implements PacketWriter { writeUint32(connectionNumber, tag, 4); // Encode the packet number as an unsigned 32-bit integer writeUint32(packetNumber, tag, 8); - // Write the tag to the underlying output stream and the MAC - out.write(tag); + // Write the tag to the encrypter and start calculating the MAC + encrypter.writeTag(tag); mac.update(tag); packetNumber++; betweenPackets = false; } - private void writeUint16(int i, byte[] b, int offset) { + // Package access for testing + static void writeUint16(int i, byte[] b, int offset) { assert i >= 0; assert i <= MAX_16_BIT_UNSIGNED; assert b.length >= offset + 2; @@ -93,7 +97,8 @@ class PacketWriterImpl extends FilterOutputStream implements PacketWriter { b[offset + 1] = (byte) (i & 0xFF); } - private void writeUint32(long i, byte[] b, int offset) { + // Package access for testing + static void writeUint32(long i, byte[] b, int offset) { assert i >= 0L; assert i <= MAX_32_BIT_UNSIGNED; assert b.length >= offset + 4; diff --git a/test/build.xml b/test/build.xml index fdf1083a2..a7b466eef 100644 --- a/test/build.xml +++ b/test/build.xml @@ -32,6 +32,7 @@ + diff --git a/test/net/sf/briar/transport/PacketEncrypterImplTest.java b/test/net/sf/briar/transport/PacketEncrypterImplTest.java new file mode 100644 index 000000000..8cd64954e --- /dev/null +++ b/test/net/sf/briar/transport/PacketEncrypterImplTest.java @@ -0,0 +1,76 @@ +package net.sf.briar.transport; + +import java.io.ByteArrayOutputStream; +import java.util.Arrays; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; + +import junit.framework.TestCase; +import net.sf.briar.api.crypto.CryptoComponent; +import net.sf.briar.crypto.CryptoModule; + +import org.junit.Test; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +public class PacketEncrypterImplTest extends TestCase { + + private final Cipher tagCipher, packetCipher; + private final SecretKey tagKey, packetKey; + + public PacketEncrypterImplTest() { + super(); + Injector i = Guice.createInjector(new CryptoModule()); + CryptoComponent crypto = i.getInstance(CryptoComponent.class); + tagCipher = crypto.getTagCipher(); + packetCipher = crypto.getPacketCipher(); + tagKey = crypto.generateSecretKey(); + packetKey = crypto.generateSecretKey(); + } + + @Test + public void testSingleBytePacket() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + PacketEncrypter p = new PacketEncrypterImpl(out, tagCipher, + packetCipher, tagKey, packetKey); + p.writeTag(new byte[16]); + p.getOutputStream().write((byte) 0); + p.finishPacket(); + assertEquals(17, out.toByteArray().length); + } + + @Test + public void testEncryption() throws Exception { + byte[] tag = new byte[16]; + byte[] packet = new byte[123]; + // Calculate the expected encrypted tag + tagCipher.init(Cipher.ENCRYPT_MODE, tagKey); + byte[] expectedTag = tagCipher.doFinal(tag); + assertEquals(tag.length, expectedTag.length); + // Calculate the expected encrypted packet + IvParameterSpec iv = new IvParameterSpec(tag); + packetCipher.init(Cipher.ENCRYPT_MODE, packetKey, iv); + byte[] expectedPacket = packetCipher.doFinal(packet); + assertEquals(packet.length, expectedPacket.length); + // Check that the PacketEncrypter gets the same results + ByteArrayOutputStream out = new ByteArrayOutputStream(); + PacketEncrypter p = new PacketEncrypterImpl(out, tagCipher, + packetCipher, tagKey, packetKey); + p.writeTag(tag); + p.getOutputStream().write(packet); + p.finishPacket(); + byte[] ciphertext = out.toByteArray(); + assertEquals(16 + packet.length, ciphertext.length); + // Check the tag + byte[] actualTag = new byte[16]; + System.arraycopy(ciphertext, 0, actualTag, 0, 16); + assertTrue(Arrays.equals(expectedTag, actualTag)); + // Check the packet + byte[] actualPacket = new byte[packet.length]; + System.arraycopy(ciphertext, 16, actualPacket, 0, actualPacket.length); + assertTrue(Arrays.equals(expectedPacket, actualPacket)); + } +} diff --git a/test/net/sf/briar/transport/PacketWriterImplTest.java b/test/net/sf/briar/transport/PacketWriterImplTest.java index 8c22e8a4e..9bf4ee116 100644 --- a/test/net/sf/briar/transport/PacketWriterImplTest.java +++ b/test/net/sf/briar/transport/PacketWriterImplTest.java @@ -1,6 +1,8 @@ package net.sf.briar.transport; import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; import java.util.Arrays; import javax.crypto.Mac; @@ -31,7 +33,8 @@ public class PacketWriterImplTest extends TestCase { @Test public void testFirstWriteTriggersTag() throws Exception { ByteArrayOutputStream out = new ByteArrayOutputStream(); - PacketWriter p = new PacketWriterImpl(out, mac, 0, 0L); + PacketEncrypter e = new NullPacketEncrypter(out); + PacketWriter p = new PacketWriterImpl(e, mac, 0, 0L); p.getOutputStream().write(0); // There should be 16 zero bytes for the tag, 1 for the byte written assertTrue(Arrays.equals(new byte[17], out.toByteArray())); @@ -44,7 +47,8 @@ public class PacketWriterImplTest extends TestCase { byte[] expectedMac = mac.doFinal(); // Check that the PacketWriter calculates and writes the correct MAC ByteArrayOutputStream out = new ByteArrayOutputStream(); - PacketWriter p = new PacketWriterImpl(out, mac, 0, 0L); + PacketEncrypter e = new NullPacketEncrypter(out); + PacketWriter p = new PacketWriterImpl(e, mac, 0, 0L); p.getOutputStream().write(0); p.nextPacket(); byte[] written = out.toByteArray(); @@ -61,7 +65,8 @@ public class PacketWriterImplTest extends TestCase { byte[] expectedMac = mac.doFinal(); // Check that the PacketWriter calculates and writes the correct MAC ByteArrayOutputStream out = new ByteArrayOutputStream(); - PacketWriter p = new PacketWriterImpl(out, mac, 0, 0L); + PacketEncrypter e = new NullPacketEncrypter(out); + PacketWriter p = new PacketWriterImpl(e, mac, 0, 0L); // Initial calls to nextPacket() should have no effect p.nextPacket(); p.nextPacket(); @@ -105,7 +110,8 @@ public class PacketWriterImplTest extends TestCase { byte[] expectedMac1 = mac.doFinal(); // Check that the PacketWriter writes the correct tags and MACs ByteArrayOutputStream out = new ByteArrayOutputStream(); - PacketWriter p = new PacketWriterImpl(out, mac, 0xF00D, 0xDEADBEEFL); + PacketEncrypter e = new NullPacketEncrypter(out); + PacketWriter p = new PacketWriterImpl(e, mac, 0xF00D, 0xDEADBEEFL); // Packet one p.getOutputStream().write(0); p.nextPacket(); @@ -133,4 +139,45 @@ public class PacketWriterImplTest extends TestCase { actualMac1.length); assertTrue(Arrays.equals(expectedMac1, actualMac1)); } + + @Test + public void testWriteUint16() throws Exception { + byte[] b = new byte[3]; + PacketWriterImpl.writeUint16(0, b, 1); + assertEquals("000000", StringUtils.toHexString(b)); + PacketWriterImpl.writeUint16(1, b, 1); + assertEquals("000001", StringUtils.toHexString(b)); + PacketWriterImpl.writeUint16(65535, b, 1); + assertEquals("00FFFF", StringUtils.toHexString(b)); + } + + @Test + public void testWriteUint32() throws Exception { + byte[] b = new byte[5]; + PacketWriterImpl.writeUint32(0L, b, 1); + assertEquals("0000000000", StringUtils.toHexString(b)); + PacketWriterImpl.writeUint32(1L, b, 1); + assertEquals("0000000001", StringUtils.toHexString(b)); + PacketWriterImpl.writeUint32(4294967295L, b, 1); + assertEquals("00FFFFFFFF", StringUtils.toHexString(b)); + } + + private static class NullPacketEncrypter implements PacketEncrypter { + + private final OutputStream out; + + private NullPacketEncrypter(OutputStream out) { + this.out = out; + } + + public OutputStream getOutputStream() { + return out; + } + + public void writeTag(byte[] tag) throws IOException { + out.write(tag); + } + + public void finishPacket() {} + } }