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() {}
+ }
}