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)); + } +}