Forward secrecy.

Each connection's keys are derived from a secret that is erased after
deriving the keys and the secret for the next connection.
This commit is contained in:
akwizgran
2011-11-16 15:35:16 +00:00
parent d02a68edfc
commit f6ae4734ce
45 changed files with 506 additions and 430 deletions

View File

@@ -52,7 +52,8 @@ public class ConnectionDecrypterImplTest extends TestCase {
private void testDecryption(boolean initiator) throws Exception {
// Calculate the plaintext and ciphertext for the IV
byte[] iv = IvEncoder.encodeIv(initiator, transportIndex, connection);
byte[] iv = IvEncoder.encodeIv(initiator, transportIndex.getInt(),
connection);
ivCipher.init(Cipher.ENCRYPT_MODE, ivKey);
byte[] encryptedIv = ivCipher.doFinal(iv);
assertEquals(IV_LENGTH, encryptedIv.length);
@@ -85,8 +86,8 @@ public class ConnectionDecrypterImplTest extends TestCase {
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
// Use a ConnectionDecrypter to decrypt the ciphertext
ConnectionDecrypter d = new ConnectionDecrypterImpl(in,
IvEncoder.encodeIv(initiator, transportIndex, connection),
frameCipher, frameKey);
IvEncoder.encodeIv(initiator, transportIndex.getInt(),
connection), frameCipher, frameKey);
// First frame
byte[] decrypted = new byte[ciphertext.length];
TestUtils.readFully(d.getInputStream(), decrypted);

View File

@@ -50,7 +50,8 @@ public class ConnectionEncrypterImplTest extends TestCase {
private void testEncryption(boolean initiator) throws Exception {
// Calculate the expected ciphertext for the IV
byte[] iv = IvEncoder.encodeIv(initiator, transportIndex, connection);
byte[] iv = IvEncoder.encodeIv(initiator, transportIndex.getInt(),
connection);
ivCipher.init(Cipher.ENCRYPT_MODE, ivKey);
byte[] encryptedIv = ivCipher.doFinal(iv);
assertEquals(IV_LENGTH, encryptedIv.length);
@@ -82,7 +83,7 @@ public class ConnectionEncrypterImplTest extends TestCase {
byte[] expected = out.toByteArray();
// Use a ConnectionEncrypter to encrypt the plaintext
out.reset();
iv = IvEncoder.encodeIv(initiator, transportIndex, connection);
iv = IvEncoder.encodeIv(initiator, transportIndex.getInt(), connection);
ConnectionEncrypter e = new ConnectionEncrypterImpl(out, Long.MAX_VALUE,
iv, ivCipher, frameCipher, ivKey, frameKey);
e.getOutputStream().write(plaintext);

View File

@@ -4,6 +4,7 @@ import static net.sf.briar.api.transport.TransportConstants.IV_LENGTH;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Random;
import javax.crypto.Cipher;
@@ -51,7 +52,8 @@ public class ConnectionRecogniserImplTest extends TestCase {
Transport transport = new Transport(transportId, localIndex,
Collections.singletonMap("foo", "bar"));
transports = Collections.singletonList(transport);
connectionWindow = new ConnectionWindowImpl();
connectionWindow = new ConnectionWindowImpl(crypto, remoteIndex,
inSecret);
}
@Test
@@ -65,8 +67,6 @@ public class ConnectionRecogniserImplTest extends TestCase {
will(returnValue(transports));
oneOf(db).getContacts();
will(returnValue(Collections.singletonList(contactId)));
oneOf(db).getSharedSecret(contactId, true);
will(returnValue(inSecret));
oneOf(db).getRemoteIndex(contactId, transportId);
will(returnValue(remoteIndex));
oneOf(db).getConnectionWindow(contactId, remoteIndex);
@@ -80,11 +80,16 @@ public class ConnectionRecogniserImplTest extends TestCase {
@Test
public void testExpectedIv() throws Exception {
// Calculate the shared secret for connection number 3
byte[] secret = inSecret;
for(int i = 0; i < 4; i++) {
secret = crypto.deriveNextSecret(secret, remoteIndex.getInt(), i);
}
// Calculate the expected IV for connection number 3
ErasableKey ivKey = crypto.deriveIvKey(inSecret, true);
ErasableKey ivKey = crypto.deriveIvKey(secret, true);
Cipher ivCipher = crypto.getIvCipher();
ivCipher.init(Cipher.ENCRYPT_MODE, ivKey);
byte[] iv = IvEncoder.encodeIv(true, remoteIndex, 3L);
byte[] iv = IvEncoder.encodeIv(true, remoteIndex.getInt(), 3);
byte[] encryptedIv = ivCipher.doFinal(iv);
Mockery context = new Mockery();
@@ -96,8 +101,6 @@ public class ConnectionRecogniserImplTest extends TestCase {
will(returnValue(transports));
oneOf(db).getContacts();
will(returnValue(Collections.singletonList(contactId)));
oneOf(db).getSharedSecret(contactId, true);
will(returnValue(inSecret));
oneOf(db).getRemoteIndex(contactId, transportId);
will(returnValue(remoteIndex));
oneOf(db).getConnectionWindow(contactId, remoteIndex);
@@ -107,8 +110,6 @@ public class ConnectionRecogniserImplTest extends TestCase {
will(returnValue(connectionWindow));
oneOf(db).setConnectionWindow(contactId, remoteIndex,
connectionWindow);
oneOf(db).getSharedSecret(contactId, true);
will(returnValue(inSecret));
}});
final ConnectionRecogniserImpl c =
new ConnectionRecogniserImpl(crypto, db);
@@ -121,11 +122,11 @@ public class ConnectionRecogniserImplTest extends TestCase {
// Second time - the IV should no longer be expected
assertNull(c.acceptConnection(encryptedIv));
// The window should have advanced
Collection<Long> unseen = connectionWindow.getUnseen();
Map<Long, byte[]> unseen = connectionWindow.getUnseen();
assertEquals(19, unseen.size());
for(int i = 0; i < 19; i++) {
if(i == 3) continue;
assertTrue(unseen.contains(Long.valueOf(i)));
assertTrue(unseen.containsKey(Long.valueOf(i)));
}
context.assertIsSatisfied();
}

View File

@@ -1,18 +1,39 @@
package net.sf.briar.transport;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import junit.framework.TestCase;
import net.sf.briar.api.crypto.CryptoComponent;
import net.sf.briar.api.protocol.TransportIndex;
import net.sf.briar.api.transport.ConnectionWindow;
import net.sf.briar.crypto.CryptoModule;
import net.sf.briar.util.ByteUtils;
import org.junit.Test;
import com.google.inject.Guice;
import com.google.inject.Injector;
public class ConnectionWindowImplTest extends TestCase {
private final CryptoComponent crypto;
private final byte[] secret;
private final TransportIndex transportIndex = new TransportIndex(13);
public ConnectionWindowImplTest(String name) {
super(name);
Injector i = Guice.createInjector(new CryptoModule());
crypto = i.getInstance(CryptoComponent.class);
secret = new byte[32];
new Random().nextBytes(secret);
}
@Test
public void testWindowSliding() {
ConnectionWindowImpl w = new ConnectionWindowImpl();
ConnectionWindow w = new ConnectionWindowImpl(crypto,
transportIndex, secret);
for(int i = 0; i < 100; i++) {
assertFalse(w.isSeen(i));
w.setSeen(i);
@@ -22,7 +43,8 @@ public class ConnectionWindowImplTest extends TestCase {
@Test
public void testWindowJumping() {
ConnectionWindowImpl w = new ConnectionWindowImpl();
ConnectionWindow w = new ConnectionWindowImpl(crypto, transportIndex,
secret);
for(int i = 0; i < 100; i += 13) {
assertFalse(w.isSeen(i));
w.setSeen(i);
@@ -32,7 +54,8 @@ public class ConnectionWindowImplTest extends TestCase {
@Test
public void testWindowUpperLimit() {
ConnectionWindowImpl w = new ConnectionWindowImpl();
ConnectionWindow w = new ConnectionWindowImpl(crypto, transportIndex,
secret);
// Centre is 0, highest value in window is 15
w.setSeen(15);
// Centre is 16, highest value in window is 31
@@ -43,11 +66,11 @@ public class ConnectionWindowImplTest extends TestCase {
fail();
} catch(IllegalArgumentException expected) {}
// Values greater than 2^32 - 1 should never be allowed
Collection<Long> unseen = new ArrayList<Long>();
Map<Long, byte[]> unseen = new HashMap<Long, byte[]>();
for(int i = 0; i < 32; i++) {
unseen.add(ByteUtils.MAX_32_BIT_UNSIGNED - i);
unseen.put(ByteUtils.MAX_32_BIT_UNSIGNED - i, secret);
}
w = new ConnectionWindowImpl(unseen);
w = new ConnectionWindowImpl(crypto, transportIndex, unseen);
w.setSeen(ByteUtils.MAX_32_BIT_UNSIGNED);
try {
w.setSeen(ByteUtils.MAX_32_BIT_UNSIGNED + 1);
@@ -57,7 +80,8 @@ public class ConnectionWindowImplTest extends TestCase {
@Test
public void testWindowLowerLimit() {
ConnectionWindowImpl w = new ConnectionWindowImpl();
ConnectionWindow w = new ConnectionWindowImpl(crypto, transportIndex,
secret);
// Centre is 0, negative values should never be allowed
try {
w.setSeen(-1);
@@ -87,7 +111,8 @@ public class ConnectionWindowImplTest extends TestCase {
@Test
public void testCannotSetSeenTwice() {
ConnectionWindowImpl w = new ConnectionWindowImpl();
ConnectionWindow w = new ConnectionWindowImpl(crypto, transportIndex,
secret);
w.setSeen(15);
try {
w.setSeen(15);
@@ -97,12 +122,13 @@ public class ConnectionWindowImplTest extends TestCase {
@Test
public void testGetUnseenConnectionNumbers() {
ConnectionWindowImpl w = new ConnectionWindowImpl();
ConnectionWindow w = new ConnectionWindowImpl(crypto, transportIndex,
secret);
// Centre is 0; window should cover 0 to 15, inclusive, with none seen
Collection<Long> unseen = w.getUnseen();
Map<Long, byte[]> unseen = w.getUnseen();
assertEquals(16, unseen.size());
for(int i = 0; i < 16; i++) {
assertTrue(unseen.contains(Long.valueOf(i)));
assertTrue(unseen.containsKey(Long.valueOf(i)));
assertFalse(w.isSeen(i));
}
w.setSeen(3);
@@ -112,10 +138,10 @@ public class ConnectionWindowImplTest extends TestCase {
assertEquals(19, unseen.size());
for(int i = 0; i < 21; i++) {
if(i == 3 || i == 4) {
assertFalse(unseen.contains(Long.valueOf(i)));
assertFalse(unseen.containsKey(Long.valueOf(i)));
assertTrue(w.isSeen(i));
} else {
assertTrue(unseen.contains(Long.valueOf(i)));
assertTrue(unseen.containsKey(Long.valueOf(i)));
assertFalse(w.isSeen(i));
}
}
@@ -125,10 +151,10 @@ public class ConnectionWindowImplTest extends TestCase {
assertEquals(30, unseen.size());
for(int i = 4; i < 36; i++) {
if(i == 4 || i == 19) {
assertFalse(unseen.contains(Long.valueOf(i)));
assertFalse(unseen.containsKey(Long.valueOf(i)));
assertTrue(w.isSeen(i));
} else {
assertTrue(unseen.contains(Long.valueOf(i)));
assertTrue(unseen.containsKey(Long.valueOf(i)));
assertFalse(w.isSeen(i));
}
}

View File

@@ -8,7 +8,10 @@ import java.util.Random;
import junit.framework.TestCase;
import net.sf.briar.TestDatabaseModule;
import net.sf.briar.api.ContactId;
import net.sf.briar.api.protocol.TransportIndex;
import net.sf.briar.api.transport.ConnectionContext;
import net.sf.briar.api.transport.ConnectionContextFactory;
import net.sf.briar.api.transport.ConnectionWriter;
import net.sf.briar.api.transport.ConnectionWriterFactory;
import net.sf.briar.crypto.CryptoModule;
@@ -26,8 +29,10 @@ import com.google.inject.Injector;
public class ConnectionWriterTest extends TestCase {
private final ConnectionContextFactory connectionContextFactory;
private final ConnectionWriterFactory connectionWriterFactory;
private final byte[] outSecret;
private final byte[] secret;
private final ContactId contactId = new ContactId(13);
private final TransportIndex transportIndex = new TransportIndex(13);
private final long connection = 12345L;
@@ -38,17 +43,22 @@ public class ConnectionWriterTest extends TestCase {
new ProtocolWritersModule(), new SerialModule(),
new TestDatabaseModule(), new TransportBatchModule(),
new TransportModule(), new TransportStreamModule());
connectionContextFactory =
i.getInstance(ConnectionContextFactory.class);
connectionWriterFactory = i.getInstance(ConnectionWriterFactory.class);
outSecret = new byte[32];
new Random().nextBytes(outSecret);
secret = new byte[32];
new Random().nextBytes(secret);
}
@Test
public void testOverhead() throws Exception {
ByteArrayOutputStream out =
new ByteArrayOutputStream(MIN_CONNECTION_LENGTH);
ConnectionContext ctx =
connectionContextFactory.createConnectionContext(contactId,
transportIndex, connection, secret);
ConnectionWriter w = connectionWriterFactory.createConnectionWriter(out,
MIN_CONNECTION_LENGTH, transportIndex, connection, outSecret);
MIN_CONNECTION_LENGTH, ctx);
// Check that the connection writer thinks there's room for a packet
long capacity = w.getRemainingCapacity();
assertTrue(capacity >= MAX_PACKET_LENGTH);

View File

@@ -64,7 +64,8 @@ public class FrameReadWriteTest extends TestCase {
private void testWriteAndRead(boolean initiator) throws Exception {
// Create and encrypt the IV
byte[] iv = IvEncoder.encodeIv(initiator, transportIndex, connection);
byte[] iv = IvEncoder.encodeIv(initiator, transportIndex.getInt(),
connection);
ivCipher.init(Cipher.ENCRYPT_MODE, ivKey);
byte[] encryptedIv = ivCipher.doFinal(iv);
assertEquals(IV_LENGTH, encryptedIv.length);
@@ -92,7 +93,7 @@ public class FrameReadWriteTest extends TestCase {
// Decrypt the IV
ivCipher.init(Cipher.DECRYPT_MODE, ivKey);
byte[] recoveredIv = ivCipher.doFinal(recoveredEncryptedIv);
iv = IvEncoder.encodeIv(initiator, transportIndex, connection);
iv = IvEncoder.encodeIv(initiator, transportIndex.getInt(), connection);
assertArrayEquals(iv, recoveredIv);
// Read the frames back
ConnectionDecrypter decrypter = new ConnectionDecrypterImpl(in, iv,

View File

@@ -119,7 +119,7 @@ public class BatchConnectionReadWriteTest extends TestCase {
alice.getInstance(ProtocolWriterFactory.class);
BatchTransportWriter writer = new TestBatchTransportWriter(out);
OutgoingBatchConnection batchOut = new OutgoingBatchConnection(
connFactory, db, protoFactory, transportIndex, contactId,
connFactory, db, protoFactory, contactId, transportIndex,
writer);
// Write whatever needs to be written
batchOut.write();
@@ -170,8 +170,7 @@ public class BatchConnectionReadWriteTest extends TestCase {
bob.getInstance(ProtocolReaderFactory.class);
BatchTransportReader reader = new TestBatchTransportReader(in);
IncomingBatchConnection batchIn = new IncomingBatchConnection(
connFactory, db, protoFactory, transportIndex, contactId,
reader, encryptedIv);
connFactory, db, protoFactory, ctx, reader, encryptedIv);
// No messages should have been added yet
assertFalse(listener.messagesAdded);
// Read whatever needs to be read