Added optional padding to the frame format, so transports that are

vulnerable to traffic analysis can frame their data independently of
packet boundaries.
This commit is contained in:
akwizgran
2011-08-19 14:47:16 +02:00
parent 7a53ea7814
commit 3084a6b058
6 changed files with 123 additions and 63 deletions

View File

@@ -31,8 +31,8 @@ implements ConnectionReader {
super(decrypter.getInputStream()); super(decrypter.getInputStream());
this.decrypter = decrypter; this.decrypter = decrypter;
this.mac = mac; this.mac = mac;
maxPayloadLength = MAX_FRAME_LENGTH - 6 - mac.getMacLength(); maxPayloadLength = MAX_FRAME_LENGTH - 8 - mac.getMacLength();
header = new byte[6]; header = new byte[8];
payload = new byte[maxPayloadLength]; payload = new byte[maxPayloadLength];
footer = new byte[mac.getMacLength()]; footer = new byte[mac.getMacLength()];
} }
@@ -83,9 +83,11 @@ implements ConnectionReader {
// Check that the frame has the expected frame number // Check that the frame has the expected frame number
if(ByteUtils.readUint32(header, 0) != frame) if(ByteUtils.readUint32(header, 0) != frame)
throw new FormatException(); throw new FormatException();
// Check that the payload length is legal // Check that the payload and padding lengths are legal
payloadLen = ByteUtils.readUint16(header, 4); payloadLen = ByteUtils.readUint16(header, 4);
if(payloadLen == 0 || payloadLen > maxPayloadLength) int paddingLen = ByteUtils.readUint16(header, 6);
if(payloadLen + paddingLen == 0) throw new FormatException();
if(payloadLen + paddingLen > maxPayloadLength)
throw new FormatException(); throw new FormatException();
frame++; frame++;
// Read the payload // Read the payload
@@ -97,6 +99,18 @@ implements ConnectionReader {
offset += read; offset += read;
} }
payloadOff = 0; payloadOff = 0;
// Read the padding
while(offset < payloadLen + paddingLen) {
int read = in.read(payload, offset,
payloadLen + paddingLen - offset);
if(read == -1) throw new EOFException(); // Unexpected EOF
mac.update(payload, offset, read);
offset += read;
}
// Check that the padding is all zeroes
for(int i = payloadLen; i < payloadLen + paddingLen; i++) {
if(payload[i] != 0) throw new FormatException();
}
// Read the MAC // Read the MAC
byte[] expectedMac = mac.doFinal(); byte[] expectedMac = mac.doFinal();
decrypter.readMac(footer); decrypter.readMac(footer);

View File

@@ -28,9 +28,9 @@ implements ConnectionWriter {
super(encrypter.getOutputStream()); super(encrypter.getOutputStream());
this.encrypter = encrypter; this.encrypter = encrypter;
this.mac = mac; this.mac = mac;
maxPayloadLength = MAX_FRAME_LENGTH - 6 - mac.getMacLength(); maxPayloadLength = MAX_FRAME_LENGTH - 8 - mac.getMacLength();
buf = new ByteArrayOutputStream(maxPayloadLength); buf = new ByteArrayOutputStream(maxPayloadLength);
header = new byte[6]; header = new byte[8];
} }
public OutputStream getOutputStream() { public OutputStream getOutputStream() {

View File

@@ -31,7 +31,6 @@
<test name='net.sf.briar.protocol.ConsumersTest'/> <test name='net.sf.briar.protocol.ConsumersTest'/>
<test name='net.sf.briar.protocol.ProtocolReadWriteTest'/> <test name='net.sf.briar.protocol.ProtocolReadWriteTest'/>
<test name='net.sf.briar.protocol.RequestReaderTest'/> <test name='net.sf.briar.protocol.RequestReaderTest'/>
<test name='net.sf.briar.protocol.SigningDigestingOutputStreamTest'/>
<test name='net.sf.briar.protocol.writers.RequestWriterImplTest'/> <test name='net.sf.briar.protocol.writers.RequestWriterImplTest'/>
<test name='net.sf.briar.serial.ReaderImplTest'/> <test name='net.sf.briar.serial.ReaderImplTest'/>
<test name='net.sf.briar.serial.WriterImplTest'/> <test name='net.sf.briar.serial.WriterImplTest'/>

View File

@@ -173,8 +173,7 @@ public class FileReadWriteTest extends TestCase {
TransportWriter t = protocolWriterFactory.createTransportWriter(out); TransportWriter t = protocolWriterFactory.createTransportWriter(out);
t.writeTransports(transports, timestamp); t.writeTransports(transports, timestamp);
w.getOutputStream().flush(); out.close();
w.getOutputStream().close();
assertTrue(file.exists()); assertTrue(file.exists());
assertTrue(file.length() > message.getSize()); assertTrue(file.length() > message.getSize());
} }

View File

@@ -27,6 +27,7 @@ import com.google.inject.Injector;
public class ConnectionReaderImplTest extends TestCase { public class ConnectionReaderImplTest extends TestCase {
private final Mac mac; private final Mac mac;
private final int headerLength = 8, macLength;
public ConnectionReaderImplTest() throws Exception { public ConnectionReaderImplTest() throws Exception {
super(); super();
@@ -34,15 +35,17 @@ public class ConnectionReaderImplTest extends TestCase {
CryptoComponent crypto = i.getInstance(CryptoComponent.class); CryptoComponent crypto = i.getInstance(CryptoComponent.class);
mac = crypto.getMac(); mac = crypto.getMac();
mac.init(crypto.generateSecretKey()); mac.init(crypto.generateSecretKey());
macLength = mac.getMacLength();
} }
@Test @Test
public void testLengthZero() throws Exception { public void testLengthZero() throws Exception {
// Six bytes for the header, none for the payload int payloadLength = 0;
byte[] frame = new byte[6 + mac.getMacLength()]; byte[] frame = new byte[headerLength + payloadLength + macLength];
writeHeader(frame, 0L, payloadLength, 0);
// Calculate the MAC // Calculate the MAC
mac.update(frame, 0, 6); mac.update(frame, 0, headerLength + payloadLength);
mac.doFinal(frame, 6); mac.doFinal(frame, headerLength + payloadLength);
// Read the frame // Read the frame
ByteArrayInputStream in = new ByteArrayInputStream(frame); ByteArrayInputStream in = new ByteArrayInputStream(frame);
ConnectionDecrypter d = new NullConnectionDecrypter(in); ConnectionDecrypter d = new NullConnectionDecrypter(in);
@@ -55,12 +58,12 @@ public class ConnectionReaderImplTest extends TestCase {
@Test @Test
public void testLengthOne() throws Exception { public void testLengthOne() throws Exception {
// Six bytes for the header, one for the payload int payloadLength = 1;
byte[] frame = new byte[6 + 1 + mac.getMacLength()]; byte[] frame = new byte[headerLength + payloadLength + macLength];
ByteUtils.writeUint16(1, frame, 4); // Frame number 0, length 1 writeHeader(frame, 0L, payloadLength, 0);
// Calculate the MAC // Calculate the MAC
mac.update(frame, 0, 6 + 1); mac.update(frame, 0, headerLength + payloadLength);
mac.doFinal(frame, 6 + 1); mac.doFinal(frame, headerLength + payloadLength);
// Read the frame // Read the frame
ByteArrayInputStream in = new ByteArrayInputStream(frame); ByteArrayInputStream in = new ByteArrayInputStream(frame);
ConnectionDecrypter d = new NullConnectionDecrypter(in); ConnectionDecrypter d = new NullConnectionDecrypter(in);
@@ -72,18 +75,17 @@ public class ConnectionReaderImplTest extends TestCase {
@Test @Test
public void testMaxLength() throws Exception { public void testMaxLength() throws Exception {
int maxPayloadLength = MAX_FRAME_LENGTH - 6 - mac.getMacLength(); int maxPayloadLength = MAX_FRAME_LENGTH - headerLength - macLength;
// First frame: max payload length // First frame: max payload length
byte[] frame = new byte[6 + maxPayloadLength + mac.getMacLength()]; byte[] frame = new byte[MAX_FRAME_LENGTH];
ByteUtils.writeUint16(maxPayloadLength, frame, 4); writeHeader(frame, 0L, maxPayloadLength, 0);
mac.update(frame, 0, 6 + maxPayloadLength); mac.update(frame, 0, headerLength + maxPayloadLength);
mac.doFinal(frame, 6 + maxPayloadLength); mac.doFinal(frame, headerLength + maxPayloadLength);
// Second frame: max payload length plus one // Second frame: max payload length plus one
byte[] frame1 = new byte[6 + maxPayloadLength + 1 + mac.getMacLength()]; byte[] frame1 = new byte[MAX_FRAME_LENGTH + 1];
ByteUtils.writeUint32(1, frame1, 0); writeHeader(frame1, 1L, maxPayloadLength + 1, 0);
ByteUtils.writeUint16(maxPayloadLength + 1, frame1, 4); mac.update(frame1, 0, headerLength + maxPayloadLength + 1);
mac.update(frame1, 0, 6 + maxPayloadLength + 1); mac.doFinal(frame1, headerLength + maxPayloadLength + 1);
mac.doFinal(frame1, 6 + maxPayloadLength + 1);
// Concatenate the frames // Concatenate the frames
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(frame); out.write(frame);
@@ -102,19 +104,51 @@ public class ConnectionReaderImplTest extends TestCase {
} catch(FormatException expected) {} } catch(FormatException expected) {}
} }
@Test
public void testMaxLengthWithPadding() throws Exception {
int maxPayloadLength = MAX_FRAME_LENGTH - headerLength - macLength;
int paddingLength = 10;
// First frame: max payload length, including padding
byte[] frame = new byte[MAX_FRAME_LENGTH];
writeHeader(frame, 0L, maxPayloadLength - paddingLength, paddingLength);
mac.update(frame, 0, headerLength + maxPayloadLength);
mac.doFinal(frame, headerLength + maxPayloadLength);
// Second frame: max payload length plus one, including padding
byte[] frame1 = new byte[MAX_FRAME_LENGTH + 1];
writeHeader(frame1, 1L, maxPayloadLength + 1 - paddingLength,
paddingLength);
mac.update(frame1, 0, headerLength + maxPayloadLength + 1);
mac.doFinal(frame1, headerLength + maxPayloadLength + 1);
// Concatenate the frames
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(frame);
out.write(frame1);
// Read the first frame
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
ConnectionDecrypter d = new NullConnectionDecrypter(in);
ConnectionReader r = new ConnectionReaderImpl(d, mac);
byte[] read = new byte[maxPayloadLength - paddingLength];
TestUtils.readFully(r.getInputStream(), read);
// Try to read the second frame
byte[] read1 = new byte[maxPayloadLength + 1 - paddingLength];
try {
TestUtils.readFully(r.getInputStream(), read1);
fail();
} catch(FormatException expected) {}
}
@Test @Test
public void testMultipleFrames() throws Exception { public void testMultipleFrames() throws Exception {
// First frame: 123-byte payload // First frame: 123-byte payload
byte[] frame = new byte[6 + 123 + mac.getMacLength()]; byte[] frame = new byte[8 + 123 + mac.getMacLength()];
ByteUtils.writeUint16(123, frame, 4); // Frame number 0, length 123 writeHeader(frame, 0L, 123, 0);
mac.update(frame, 0, 6 + 123); mac.update(frame, 0, 8 + 123);
mac.doFinal(frame, 6 + 123); mac.doFinal(frame, 8 + 123);
// Second frame: 1234-byte payload // Second frame: 1234-byte payload
byte[] frame1 = new byte[6 + 1234 + mac.getMacLength()]; byte[] frame1 = new byte[8 + 1234 + mac.getMacLength()];
ByteUtils.writeUint32(1, frame1, 0); // Frame number 1 writeHeader(frame1, 1L, 1234, 0);
ByteUtils.writeUint16(1234, frame1, 4); // Length 1234 mac.update(frame1, 0, 8 + 1234);
mac.update(frame1, 0, 6 + 1234); mac.doFinal(frame1, 8 + 1234);
mac.doFinal(frame1, 6 + 1234);
// Concatenate the frames // Concatenate the frames
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(frame); out.write(frame);
@@ -133,12 +167,12 @@ public class ConnectionReaderImplTest extends TestCase {
@Test @Test
public void testCorruptPayload() throws Exception { public void testCorruptPayload() throws Exception {
// Six bytes for the header, eight for the payload int payloadLength = 8;
byte[] frame = new byte[6 + 8 + mac.getMacLength()]; byte[] frame = new byte[headerLength + payloadLength + macLength];
ByteUtils.writeUint16(8, frame, 4); // Frame number 0, length 8 writeHeader(frame, 0L, payloadLength, 0);
// Calculate the MAC // Calculate the MAC
mac.update(frame, 0, 6 + 8); mac.update(frame, 0, headerLength + payloadLength);
mac.doFinal(frame, 6 + 8); mac.doFinal(frame, headerLength + payloadLength);
// Modify the payload // Modify the payload
frame[12] ^= 1; frame[12] ^= 1;
// Try to read the frame - not a single byte should be read // Try to read the frame - not a single byte should be read
@@ -153,12 +187,12 @@ public class ConnectionReaderImplTest extends TestCase {
@Test @Test
public void testCorruptMac() throws Exception { public void testCorruptMac() throws Exception {
// Six bytes for the header, eight for the payload int payloadLength = 8;
byte[] frame = new byte[6 + 8 + mac.getMacLength()]; byte[] frame = new byte[headerLength + payloadLength + macLength];
ByteUtils.writeUint16(8, frame, 4); // Frame number 0, length 8 writeHeader(frame, 0L, payloadLength, 0);
// Calculate the MAC // Calculate the MAC
mac.update(frame, 0, 6 + 8); mac.update(frame, 0, headerLength + payloadLength);
mac.doFinal(frame, 6 + 8); mac.doFinal(frame, headerLength + payloadLength);
// Modify the MAC // Modify the MAC
frame[17] ^= 1; frame[17] ^= 1;
// Try to read the frame - not a single byte should be read // Try to read the frame - not a single byte should be read
@@ -171,6 +205,12 @@ public class ConnectionReaderImplTest extends TestCase {
} catch(FormatException expected) {} } catch(FormatException expected) {}
} }
private void writeHeader(byte[] b, long frame, int payload, int padding) {
ByteUtils.writeUint32(frame, b, 0);
ByteUtils.writeUint16(payload, b, 4);
ByteUtils.writeUint16(padding, b, 6);
}
/** A ConnectionDecrypter that performs no decryption. */ /** A ConnectionDecrypter that performs no decryption. */
private static class NullConnectionDecrypter private static class NullConnectionDecrypter
implements ConnectionDecrypter { implements ConnectionDecrypter {

View File

@@ -23,6 +23,7 @@ import com.google.inject.Injector;
public class ConnectionWriterImplTest extends TestCase { public class ConnectionWriterImplTest extends TestCase {
private final Mac mac; private final Mac mac;
private final int headerLength = 8, macLength;
public ConnectionWriterImplTest() throws Exception { public ConnectionWriterImplTest() throws Exception {
super(); super();
@@ -30,6 +31,7 @@ public class ConnectionWriterImplTest extends TestCase {
CryptoComponent crypto = i.getInstance(CryptoComponent.class); CryptoComponent crypto = i.getInstance(CryptoComponent.class);
mac = crypto.getMac(); mac = crypto.getMac();
mac.init(crypto.generateSecretKey()); mac.init(crypto.generateSecretKey());
macLength = mac.getMacLength();
} }
@Test @Test
@@ -45,12 +47,12 @@ public class ConnectionWriterImplTest extends TestCase {
@Test @Test
public void testSingleByteFrame() throws Exception { public void testSingleByteFrame() throws Exception {
// Six bytes for the header, one for the payload int payloadLength = 1;
byte[] frame = new byte[6 + 1 + mac.getMacLength()]; byte[] frame = new byte[headerLength + payloadLength + macLength];
ByteUtils.writeUint16(1, frame, 4); // Payload length = 1 writeHeader(frame, 0L, payloadLength, 0);
// Calculate the MAC // Calculate the MAC
mac.update(frame, 0, 6 + 1); mac.update(frame, 0, headerLength + payloadLength);
mac.doFinal(frame, 6 + 1); mac.doFinal(frame, headerLength + payloadLength);
// Check that the ConnectionWriter gets the same results // Check that the ConnectionWriter gets the same results
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
ConnectionEncrypter e = new NullConnectionEncrypter(out); ConnectionEncrypter e = new NullConnectionEncrypter(out);
@@ -62,7 +64,7 @@ public class ConnectionWriterImplTest extends TestCase {
@Test @Test
public void testFrameIsWrittenAtMaxLength() throws Exception { public void testFrameIsWrittenAtMaxLength() throws Exception {
int maxPayloadLength = MAX_FRAME_LENGTH - 6 - mac.getMacLength(); int maxPayloadLength = MAX_FRAME_LENGTH - headerLength - macLength;
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
ConnectionEncrypter e = new NullConnectionEncrypter(out); ConnectionEncrypter e = new NullConnectionEncrypter(out);
ConnectionWriter w = new ConnectionWriterImpl(e, mac); ConnectionWriter w = new ConnectionWriterImpl(e, mac);
@@ -75,22 +77,22 @@ public class ConnectionWriterImplTest extends TestCase {
assertEquals(MAX_FRAME_LENGTH, out.size()); assertEquals(MAX_FRAME_LENGTH, out.size());
// Flushing the stream should write a single-byte frame // Flushing the stream should write a single-byte frame
out1.flush(); out1.flush();
assertEquals(MAX_FRAME_LENGTH + 6 + 1 + mac.getMacLength(), out.size()); assertEquals(MAX_FRAME_LENGTH + headerLength + 1 + macLength,
out.size());
} }
@Test @Test
public void testMultipleFrames() throws Exception { public void testMultipleFrames() throws Exception {
// First frame: 123-byte payload // First frame: 123-byte payload
byte[] frame = new byte[6 + 123 + mac.getMacLength()]; byte[] frame = new byte[headerLength + 123 + macLength];
ByteUtils.writeUint16(123, frame, 4); writeHeader(frame, 0L, 123, 0);
mac.update(frame, 0, 6 + 123); mac.update(frame, 0, headerLength + 123);
mac.doFinal(frame, 6 + 123); mac.doFinal(frame, headerLength + 123);
// Second frame: 1234-byte payload // Second frame: 1234-byte payload
byte[] frame1 = new byte[6 + 1234 + mac.getMacLength()]; byte[] frame1 = new byte[headerLength + 1234 + macLength];
ByteUtils.writeUint32(1, frame1, 0); writeHeader(frame1, 1L, 1234, 0);
ByteUtils.writeUint16(1234, frame1, 4); mac.update(frame1, 0, headerLength + 1234);
mac.update(frame1, 0, 6 + 1234); mac.doFinal(frame1, headerLength + 1234);
mac.doFinal(frame1, 6 + 1234);
// Concatenate the frames // Concatenate the frames
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(frame); out.write(frame);
@@ -108,6 +110,12 @@ public class ConnectionWriterImplTest extends TestCase {
assertTrue(Arrays.equals(expected, actual)); assertTrue(Arrays.equals(expected, actual));
} }
private void writeHeader(byte[] b, long frame, int payload, int padding) {
ByteUtils.writeUint32(frame, b, 0);
ByteUtils.writeUint16(payload, b, 4);
ByteUtils.writeUint16(padding, b, 6);
}
/** A ConnectionEncrypter that performs no encryption. */ /** A ConnectionEncrypter that performs no encryption. */
private static class NullConnectionEncrypter private static class NullConnectionEncrypter
implements ConnectionEncrypter { implements ConnectionEncrypter {