diff --git a/api/net/sf/briar/api/crypto/CryptoComponent.java b/api/net/sf/briar/api/crypto/CryptoComponent.java
index 3399127bc..e73a7e78b 100644
--- a/api/net/sf/briar/api/crypto/CryptoComponent.java
+++ b/api/net/sf/briar/api/crypto/CryptoComponent.java
@@ -4,13 +4,19 @@ import java.security.KeyPair;
import java.security.MessageDigest;
import java.security.Signature;
+import javax.crypto.Mac;
+import javax.crypto.SecretKey;
public interface CryptoComponent {
KeyPair generateKeyPair();
+ SecretKey generateSecretKey();
+
KeyParser getKeyParser();
+ Mac getMac();
+
MessageDigest getMessageDigest();
Signature getSignature();
diff --git a/api/net/sf/briar/api/transport/ConnectionRecogniser.java b/api/net/sf/briar/api/transport/ConnectionRecogniser.java
new file mode 100644
index 000000000..3ec801404
--- /dev/null
+++ b/api/net/sf/briar/api/transport/ConnectionRecogniser.java
@@ -0,0 +1,16 @@
+package net.sf.briar.api.transport;
+
+import net.sf.briar.api.ContactId;
+
+/**
+ * Maintains a transport plugin's connection reordering window and decides
+ * whether incoming connections should be accepted or rejected.
+ */
+public interface ConnectionRecogniser {
+
+ /**
+ * Returns the ID of the contact who created the tag if the connection
+ * should be accepted, or null if the connection should be rejected.
+ */
+ ContactId acceptConnection(byte[] tag);
+}
diff --git a/api/net/sf/briar/api/transport/PacketReader.java b/api/net/sf/briar/api/transport/PacketReader.java
new file mode 100644
index 000000000..7094bb025
--- /dev/null
+++ b/api/net/sf/briar/api/transport/PacketReader.java
@@ -0,0 +1,37 @@
+package net.sf.briar.api.transport;
+
+import java.io.IOException;
+
+import net.sf.briar.api.protocol.Ack;
+import net.sf.briar.api.protocol.Batch;
+import net.sf.briar.api.protocol.Offer;
+import net.sf.briar.api.protocol.Request;
+import net.sf.briar.api.protocol.SubscriptionUpdate;
+import net.sf.briar.api.protocol.TransportUpdate;
+
+/**
+ * Reads unencrypted packets from an underlying input stream and authenticates
+ * them.
+ */
+public interface PacketReader {
+
+ boolean eof() throws IOException;
+
+ boolean hasAck() throws IOException;
+ Ack readAck() throws IOException;
+
+ boolean hasBatch() throws IOException;
+ Batch readBatch() throws IOException;
+
+ boolean hasOffer() throws IOException;
+ Offer readOffer() throws IOException;
+
+ boolean hasRequest() throws IOException;
+ Request readRequest() throws IOException;
+
+ boolean hasSubscriptionUpdate() throws IOException;
+ SubscriptionUpdate readSubscriptionUpdate() throws IOException;
+
+ boolean hasTransportUpdate() throws IOException;
+ TransportUpdate readTransportUpdate() throws IOException;
+}
diff --git a/api/net/sf/briar/api/transport/PacketWriter.java b/api/net/sf/briar/api/transport/PacketWriter.java
new file mode 100644
index 000000000..c6e3b7ea9
--- /dev/null
+++ b/api/net/sf/briar/api/transport/PacketWriter.java
@@ -0,0 +1,24 @@
+package net.sf.briar.api.transport;
+
+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.
+ */
+public interface PacketWriter {
+
+ /**
+ * Returns the output stream to which packets should be written. (Note that
+ * this is not the underlying output stream.)
+ */
+ OutputStream getOutputStream();
+
+ /**
+ * Finishes writing the current packet (if any) and prepares to write the
+ * next packet. If this method is called twice in succession without any
+ * intervening writes, the underlying output stream will be unaffected.
+ */
+ void nextPacket() throws IOException;
+}
diff --git a/components/net/sf/briar/crypto/CryptoComponentImpl.java b/components/net/sf/briar/crypto/CryptoComponentImpl.java
index 37f25dad4..caf2e24ad 100644
--- a/components/net/sf/briar/crypto/CryptoComponentImpl.java
+++ b/components/net/sf/briar/crypto/CryptoComponentImpl.java
@@ -8,6 +8,10 @@ import java.security.NoSuchProviderException;
import java.security.Security;
import java.security.Signature;
+import javax.crypto.KeyGenerator;
+import javax.crypto.Mac;
+import javax.crypto.SecretKey;
+
import net.sf.briar.api.crypto.CryptoComponent;
import net.sf.briar.api.crypto.KeyParser;
@@ -18,11 +22,15 @@ class CryptoComponentImpl implements CryptoComponent {
private static final String PROVIDER = "BC";
private static final String DIGEST_ALGO = "SHA-256";
private static final String KEY_PAIR_ALGO = "ECDSA";
- private static final int KEY_PAIR_KEYSIZE = 256;
+ private static final int KEY_PAIR_KEYSIZE = 256; // Bits
+ private static final String MAC_ALGO = "HMacSHA256";
+ 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 final KeyParser keyParser;
private final KeyPairGenerator keyPairGenerator;
+ private final KeyGenerator keyGenerator;
CryptoComponentImpl() {
Security.addProvider(new BouncyCastleProvider());
@@ -31,6 +39,9 @@ class CryptoComponentImpl implements CryptoComponent {
keyPairGenerator = KeyPairGenerator.getInstance(KEY_PAIR_ALGO,
PROVIDER);
keyPairGenerator.initialize(KEY_PAIR_KEYSIZE);
+ keyGenerator = KeyGenerator.getInstance(SECRET_KEY_ALGO,
+ PROVIDER);
+ keyGenerator.init(SECRET_KEY_KEYSIZE);
} catch(NoSuchAlgorithmException impossible) {
throw new RuntimeException(impossible);
} catch(NoSuchProviderException impossible) {
@@ -42,10 +53,24 @@ class CryptoComponentImpl implements CryptoComponent {
return keyPairGenerator.generateKeyPair();
}
+ public SecretKey generateSecretKey() {
+ return keyGenerator.generateKey();
+ }
+
public KeyParser getKeyParser() {
return keyParser;
}
+ public Mac getMac() {
+ try {
+ return Mac.getInstance(MAC_ALGO, PROVIDER);
+ } catch(NoSuchAlgorithmException impossible) {
+ throw new RuntimeException(impossible);
+ } catch(NoSuchProviderException impossible) {
+ throw new RuntimeException(impossible);
+ }
+ }
+
public MessageDigest getMessageDigest() {
try {
return MessageDigest.getInstance(DIGEST_ALGO, PROVIDER);
diff --git a/components/net/sf/briar/transport/PacketWriterImpl.java b/components/net/sf/briar/transport/PacketWriterImpl.java
new file mode 100644
index 000000000..7642b7039
--- /dev/null
+++ b/components/net/sf/briar/transport/PacketWriterImpl.java
@@ -0,0 +1,105 @@
+package net.sf.briar.transport;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import javax.crypto.Mac;
+
+import net.sf.briar.api.transport.PacketWriter;
+
+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 Mac mac;
+ private final int transportIdentifier;
+ private final long connectionNumber;
+
+ private long packetNumber = 0L;
+ private boolean betweenPackets = true;
+
+ PacketWriterImpl(OutputStream out, Mac mac, int transportIdentifier,
+ long connectionNumber) {
+ super(out);
+ this.mac = mac;
+ if(transportIdentifier < 0) throw new IllegalArgumentException();
+ if(transportIdentifier > MAX_16_BIT_UNSIGNED)
+ throw new IllegalArgumentException();
+ this.transportIdentifier = transportIdentifier;
+ if(connectionNumber < 0L) throw new IllegalArgumentException();
+ if(connectionNumber > MAX_32_BIT_UNSIGNED)
+ throw new IllegalArgumentException();
+ this.connectionNumber = connectionNumber;
+ }
+
+ public OutputStream getOutputStream() {
+ return this;
+ }
+
+ public void nextPacket() throws IOException {
+ if(!betweenPackets) writeMac();
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ if(betweenPackets) writeTag();
+ out.write(b);
+ mac.update((byte) b);
+ }
+
+ @Override
+ public void write(byte[] b) throws IOException {
+ if(betweenPackets) writeTag();
+ out.write(b);
+ mac.update(b);
+ }
+
+ @Override
+ public void write(byte[] b, int len, int off) throws IOException {
+ if(betweenPackets) writeTag();
+ out.write(b, len, off);
+ mac.update(b, len, off);
+ }
+
+ private void writeMac() throws IOException {
+ out.write(mac.doFinal());
+ betweenPackets = true;
+ }
+
+ private void writeTag() throws IOException {
+ if(packetNumber > MAX_32_BIT_UNSIGNED)
+ throw new IllegalStateException();
+ byte[] tag = new byte[16];
+ // Encode the transport identifier as an unsigned 16-bit integer
+ writeUint16(transportIdentifier, tag, 2);
+ // Encode the connection number as an unsigned 32-bit integer
+ 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);
+ mac.update(tag);
+ packetNumber++;
+ betweenPackets = false;
+ }
+
+ private void writeUint16(int i, byte[] b, int offset) {
+ assert i >= 0;
+ assert i <= MAX_16_BIT_UNSIGNED;
+ assert b.length >= offset + 2;
+ b[offset] = (byte) (i >> 8);
+ b[offset + 1] = (byte) (i & 0xFF);
+ }
+
+ private void writeUint32(long i, byte[] b, int offset) {
+ assert i >= 0L;
+ assert i <= MAX_32_BIT_UNSIGNED;
+ assert b.length >= offset + 4;
+ b[offset] = (byte) (i >> 24);
+ b[offset + 1] = (byte) (i >> 16 & 0xFF);
+ b[offset + 2] = (byte) (i >> 8 & 0xFF);
+ b[offset + 3] = (byte) (i & 0xFF);
+ }
+}
diff --git a/test/build.xml b/test/build.xml
index c54833cf8..fdf1083a2 100644
--- a/test/build.xml
+++ b/test/build.xml
@@ -32,6 +32,7 @@
+
diff --git a/test/net/sf/briar/transport/PacketWriterImplTest.java b/test/net/sf/briar/transport/PacketWriterImplTest.java
new file mode 100644
index 000000000..8c22e8a4e
--- /dev/null
+++ b/test/net/sf/briar/transport/PacketWriterImplTest.java
@@ -0,0 +1,136 @@
+package net.sf.briar.transport;
+
+import java.io.ByteArrayOutputStream;
+import java.util.Arrays;
+
+import javax.crypto.Mac;
+
+import junit.framework.TestCase;
+import net.sf.briar.api.crypto.CryptoComponent;
+import net.sf.briar.api.transport.PacketWriter;
+import net.sf.briar.crypto.CryptoModule;
+import net.sf.briar.util.StringUtils;
+
+import org.junit.Test;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+public class PacketWriterImplTest extends TestCase {
+
+ private final Mac mac;
+
+ public PacketWriterImplTest() throws Exception {
+ super();
+ Injector i = Guice.createInjector(new CryptoModule());
+ CryptoComponent crypto = i.getInstance(CryptoComponent.class);
+ mac = crypto.getMac();
+ mac.init(crypto.generateSecretKey());
+ }
+
+ @Test
+ public void testFirstWriteTriggersTag() throws Exception {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ PacketWriter p = new PacketWriterImpl(out, 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()));
+ }
+
+ @Test
+ public void testNextPacketAfterWriteTriggersMac() throws Exception {
+ // Calculate what the MAC should be
+ mac.update(new byte[17]);
+ 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);
+ p.getOutputStream().write(0);
+ p.nextPacket();
+ byte[] written = out.toByteArray();
+ assertEquals(17 + expectedMac.length, written.length);
+ byte[] actualMac = new byte[expectedMac.length];
+ System.arraycopy(written, 17, actualMac, 0, actualMac.length);
+ assertTrue(Arrays.equals(expectedMac, actualMac));
+ }
+
+ @Test
+ public void testExtraCallsToNextPacketDoNothing() throws Exception {
+ // Calculate what the MAC should be
+ mac.update(new byte[17]);
+ 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);
+ // Initial calls to nextPacket() should have no effect
+ p.nextPacket();
+ p.nextPacket();
+ p.nextPacket();
+ p.getOutputStream().write(0);
+ p.nextPacket();
+ // Extra calls to nextPacket() should have no effect
+ p.nextPacket();
+ p.nextPacket();
+ p.nextPacket();
+ byte[] written = out.toByteArray();
+ assertEquals(17 + expectedMac.length, written.length);
+ byte[] actualMac = new byte[expectedMac.length];
+ System.arraycopy(written, 17, actualMac, 0, actualMac.length);
+ assertTrue(Arrays.equals(expectedMac, actualMac));
+ }
+
+ @Test
+ public void testPacketNumberIsIncremented() throws Exception {
+ byte[] expectedTag = StringUtils.fromHexString(
+ "0000" // 16 bits reserved
+ + "F00D" // 16 bits for the transport ID
+ + "DEADBEEF" // 32 bits for the connection number
+ + "00000000" // 32 bits for the packet number
+ + "00000000" // 32 bits for the block number
+ );
+ byte[] expectedTag1 = StringUtils.fromHexString(
+ "0000" // 16 bits reserved
+ + "F00D" // 16 bits for the transport ID
+ + "DEADBEEF" // 32 bits for the connection number
+ + "00000001" // 32 bits for the packet number
+ + "00000000" // 32 bits for the block number
+ );
+ // Calculate what the MAC on the first packet should be
+ mac.update(expectedTag);
+ mac.update((byte) 0);
+ byte[] expectedMac = mac.doFinal();
+ // Calculate what the MAC on the second packet should be
+ mac.update(expectedTag1);
+ mac.update((byte) 0);
+ 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);
+ // Packet one
+ p.getOutputStream().write(0);
+ p.nextPacket();
+ // Packet two
+ p.getOutputStream().write(0);
+ p.nextPacket();
+ byte[] written = out.toByteArray();
+ assertEquals(17 + expectedMac.length + 17 + expectedMac1.length,
+ written.length);
+ // Check the first packet's tag
+ byte[] actualTag = new byte[16];
+ System.arraycopy(written, 0, actualTag, 0, 16);
+ assertTrue(Arrays.equals(expectedTag, actualTag));
+ // Check the first packet's MAC
+ byte[] actualMac = new byte[expectedMac.length];
+ System.arraycopy(written, 17, actualMac, 0, actualMac.length);
+ assertTrue(Arrays.equals(expectedMac, actualMac));
+ // Check the second packet's tag
+ byte[] actualTag1 = new byte[16];
+ System.arraycopy(written, 17 + expectedMac.length, actualTag1, 0, 16);
+ assertTrue(Arrays.equals(expectedTag1, actualTag1));
+ // Check the second packet's MAC
+ byte[] actualMac1 = new byte[expectedMac1.length];
+ System.arraycopy(written, 17 + expectedMac.length + 17, actualMac1, 0,
+ actualMac1.length);
+ assertTrue(Arrays.equals(expectedMac1, actualMac1));
+ }
+}