mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-24 00:29:52 +01:00
Updated java.library.path.
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
package org.briarproject.bramble.transport;
|
||||
|
||||
import org.briarproject.BriarTestCase;
|
||||
import org.briarproject.bramble.api.contact.Contact;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
|
||||
import org.briarproject.bramble.api.contact.event.ContactStatusChangedEvent;
|
||||
import org.briarproject.bramble.api.crypto.SecretKey;
|
||||
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||
import org.briarproject.bramble.api.db.Transaction;
|
||||
import org.briarproject.bramble.api.identity.Author;
|
||||
import org.briarproject.bramble.api.identity.AuthorId;
|
||||
import org.briarproject.bramble.api.plugin.PluginConfig;
|
||||
import org.briarproject.bramble.api.plugin.TransportId;
|
||||
import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory;
|
||||
import org.briarproject.bramble.api.transport.StreamContext;
|
||||
import org.jmock.Expectations;
|
||||
import org.jmock.Mockery;
|
||||
import org.jmock.lib.concurrent.DeterministicExecutor;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.briarproject.TestUtils.getRandomBytes;
|
||||
import static org.briarproject.TestUtils.getRandomId;
|
||||
import static org.briarproject.TestUtils.getSecretKey;
|
||||
import static org.briarproject.bramble.api.transport.TransportConstants.TAG_LENGTH;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class KeyManagerImplTest extends BriarTestCase {
|
||||
|
||||
private final Mockery context = new Mockery();
|
||||
private final KeyManagerImpl keyManager;
|
||||
private final DatabaseComponent db = context.mock(DatabaseComponent.class);
|
||||
private final PluginConfig pluginConfig = context.mock(PluginConfig.class);
|
||||
private final TransportKeyManagerFactory transportKeyManagerFactory =
|
||||
context.mock(TransportKeyManagerFactory.class);
|
||||
private final TransportKeyManager transportKeyManager =
|
||||
context.mock(TransportKeyManager.class);
|
||||
private final DeterministicExecutor executor = new DeterministicExecutor();
|
||||
private final Transaction txn = new Transaction(null, false);
|
||||
private final ContactId contactId = new ContactId(42);
|
||||
private final ContactId inactiveContactId = new ContactId(43);
|
||||
private final TransportId transportId = new TransportId("tId");
|
||||
private final TransportId unknownTransportId = new TransportId("id");
|
||||
private final StreamContext streamContext =
|
||||
new StreamContext(contactId, transportId, getSecretKey(),
|
||||
getSecretKey(), 1);
|
||||
private final byte[] tag = getRandomBytes(TAG_LENGTH);
|
||||
|
||||
public KeyManagerImplTest() {
|
||||
keyManager = new KeyManagerImpl(db, executor, pluginConfig,
|
||||
transportKeyManagerFactory);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void testStartService() throws Exception {
|
||||
final Transaction txn = new Transaction(null, false);
|
||||
AuthorId remoteAuthorId = new AuthorId(getRandomId());
|
||||
Author remoteAuthor = new Author(remoteAuthorId, "author",
|
||||
getRandomBytes(42));
|
||||
AuthorId localAuthorId = new AuthorId(getRandomId());
|
||||
final Collection<Contact> contacts = new ArrayList<>();
|
||||
contacts.add(new Contact(contactId, remoteAuthor, localAuthorId, true,
|
||||
true));
|
||||
contacts.add(new Contact(inactiveContactId, remoteAuthor, localAuthorId,
|
||||
true, false));
|
||||
final SimplexPluginFactory pluginFactory =
|
||||
context.mock(SimplexPluginFactory.class);
|
||||
final Collection<SimplexPluginFactory> factories = Collections
|
||||
.singletonList(pluginFactory);
|
||||
final int maxLatency = 1337;
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(pluginConfig).getSimplexFactories();
|
||||
will(returnValue(factories));
|
||||
oneOf(pluginFactory).getId();
|
||||
will(returnValue(transportId));
|
||||
oneOf(pluginFactory).getMaxLatency();
|
||||
will(returnValue(maxLatency));
|
||||
oneOf(db).addTransport(txn, transportId, maxLatency);
|
||||
oneOf(transportKeyManagerFactory)
|
||||
.createTransportKeyManager(transportId, maxLatency);
|
||||
will(returnValue(transportKeyManager));
|
||||
oneOf(pluginConfig).getDuplexFactories();
|
||||
oneOf(db).startTransaction(false);
|
||||
will(returnValue(txn));
|
||||
oneOf(db).getContacts(txn);
|
||||
will(returnValue(contacts));
|
||||
oneOf(transportKeyManager).start(txn);
|
||||
oneOf(db).commitTransaction(txn);
|
||||
oneOf(db).endTransaction(txn);
|
||||
}});
|
||||
|
||||
keyManager.startService();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddContact() throws Exception {
|
||||
final SecretKey secretKey = getSecretKey();
|
||||
final long timestamp = 42L;
|
||||
final boolean alice = true;
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(transportKeyManager)
|
||||
.addContact(txn, contactId, secretKey, timestamp, alice);
|
||||
}});
|
||||
|
||||
keyManager.addContact(txn, contactId, secretKey, timestamp, alice);
|
||||
context.assertIsSatisfied();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetStreamContextForInactiveContact() throws Exception {
|
||||
assertEquals(null,
|
||||
keyManager.getStreamContext(inactiveContactId, transportId));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetStreamContextForUnknownTransport() throws Exception {
|
||||
assertEquals(null, keyManager
|
||||
.getStreamContext(contactId, unknownTransportId));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetStreamContextForContact() throws Exception {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(db).startTransaction(false);
|
||||
will(returnValue(txn));
|
||||
oneOf(transportKeyManager).getStreamContext(txn, contactId);
|
||||
will(returnValue(streamContext));
|
||||
oneOf(db).commitTransaction(txn);
|
||||
oneOf(db).endTransaction(txn);
|
||||
}});
|
||||
|
||||
assertEquals(streamContext,
|
||||
keyManager.getStreamContext(contactId, transportId));
|
||||
context.assertIsSatisfied();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetStreamContextForTagAndUnknownTransport()
|
||||
throws Exception {
|
||||
assertEquals(null,
|
||||
keyManager.getStreamContext(unknownTransportId, tag));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetStreamContextForTag() throws Exception {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(db).startTransaction(false);
|
||||
will(returnValue(txn));
|
||||
oneOf(transportKeyManager).getStreamContext(txn, tag);
|
||||
will(returnValue(streamContext));
|
||||
oneOf(db).commitTransaction(txn);
|
||||
oneOf(db).endTransaction(txn);
|
||||
}});
|
||||
|
||||
assertEquals(streamContext,
|
||||
keyManager.getStreamContext(transportId, tag));
|
||||
context.assertIsSatisfied();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContactRemovedEvent() throws Exception {
|
||||
ContactRemovedEvent event = new ContactRemovedEvent(contactId);
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(transportKeyManager).removeContact(contactId);
|
||||
}});
|
||||
|
||||
keyManager.eventOccurred(event);
|
||||
executor.runUntilIdle();
|
||||
assertEquals(null, keyManager.getStreamContext(contactId, transportId));
|
||||
|
||||
context.assertIsSatisfied();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContactStatusChangedEvent() throws Exception {
|
||||
ContactStatusChangedEvent event =
|
||||
new ContactStatusChangedEvent(inactiveContactId, true);
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(db).startTransaction(false);
|
||||
will(returnValue(txn));
|
||||
oneOf(transportKeyManager).getStreamContext(txn, inactiveContactId);
|
||||
will(returnValue(streamContext));
|
||||
oneOf(db).commitTransaction(txn);
|
||||
oneOf(db).endTransaction(txn);
|
||||
}});
|
||||
|
||||
keyManager.eventOccurred(event);
|
||||
assertEquals(streamContext,
|
||||
keyManager.getStreamContext(inactiveContactId, transportId));
|
||||
|
||||
context.assertIsSatisfied();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package org.briarproject.bramble.transport;
|
||||
|
||||
import org.briarproject.BriarTestCase;
|
||||
import org.briarproject.TestUtils;
|
||||
import org.briarproject.bramble.transport.ReorderingWindow.Change;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.briarproject.bramble.api.transport.TransportConstants.REORDERING_WINDOW_SIZE;
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class ReorderingWindowTest extends BriarTestCase {
|
||||
|
||||
private static final int BITMAP_BYTES = REORDERING_WINDOW_SIZE / 8;
|
||||
|
||||
@Test
|
||||
public void testBitmapConversion() {
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
byte[] bitmap = TestUtils.getRandomBytes(BITMAP_BYTES);
|
||||
ReorderingWindow window = new ReorderingWindow(0L, bitmap);
|
||||
assertArrayEquals(bitmap, window.getBitmap());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWindowSlidesWhenFirstElementIsSeen() {
|
||||
byte[] bitmap = new byte[BITMAP_BYTES];
|
||||
ReorderingWindow window = new ReorderingWindow(0L, bitmap);
|
||||
// Set the first element seen
|
||||
Change change = window.setSeen(0L);
|
||||
// The window should slide by one element
|
||||
assertEquals(1L, window.getBase());
|
||||
assertEquals(Collections.singletonList((long) REORDERING_WINDOW_SIZE),
|
||||
change.getAdded());
|
||||
assertEquals(Collections.singletonList(0L), change.getRemoved());
|
||||
// All elements in the window should be unseen
|
||||
assertArrayEquals(bitmap, window.getBitmap());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWindowDoesNotSlideWhenElementBelowMidpointIsSeen() {
|
||||
byte[] bitmap = new byte[BITMAP_BYTES];
|
||||
ReorderingWindow window = new ReorderingWindow(0L, bitmap);
|
||||
// Set an element below the midpoint seen
|
||||
Change change = window.setSeen(1L);
|
||||
// The window should not slide
|
||||
assertEquals(0L, window.getBase());
|
||||
assertEquals(Collections.emptyList(), change.getAdded());
|
||||
assertEquals(Collections.singletonList(1L), change.getRemoved());
|
||||
// The second element in the window should be seen
|
||||
bitmap[0] = 0x40; // 0100 0000
|
||||
assertArrayEquals(bitmap, window.getBitmap());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWindowSlidesWhenElementAboveMidpointIsSeen() {
|
||||
byte[] bitmap = new byte[BITMAP_BYTES];
|
||||
ReorderingWindow window = new ReorderingWindow(0, bitmap);
|
||||
long aboveMidpoint = REORDERING_WINDOW_SIZE / 2;
|
||||
// Set an element above the midpoint seen
|
||||
Change change = window.setSeen(aboveMidpoint);
|
||||
// The window should slide by one element
|
||||
assertEquals(1L, window.getBase());
|
||||
assertEquals(Collections.singletonList((long) REORDERING_WINDOW_SIZE),
|
||||
change.getAdded());
|
||||
assertEquals(Arrays.asList(0L, aboveMidpoint), change.getRemoved());
|
||||
// The highest element below the midpoint should be seen
|
||||
bitmap[bitmap.length / 2 - 1] = (byte) 0x01; // 0000 0001
|
||||
assertArrayEquals(bitmap, window.getBitmap());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWindowSlidesUntilLowestElementIsUnseenWhenFirstElementIsSeen() {
|
||||
byte[] bitmap = new byte[BITMAP_BYTES];
|
||||
ReorderingWindow window = new ReorderingWindow(0L, bitmap);
|
||||
window.setSeen(1L);
|
||||
// Set the first element seen
|
||||
Change change = window.setSeen(0L);
|
||||
// The window should slide by two elements
|
||||
assertEquals(2L, window.getBase());
|
||||
assertEquals(Arrays.asList((long) REORDERING_WINDOW_SIZE,
|
||||
(long) (REORDERING_WINDOW_SIZE + 1)), change.getAdded());
|
||||
assertEquals(Collections.singletonList(0L), change.getRemoved());
|
||||
// All elements in the window should be unseen
|
||||
assertArrayEquals(bitmap, window.getBitmap());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWindowSlidesUntilLowestElementIsUnseenWhenElementAboveMidpointIsSeen() {
|
||||
byte[] bitmap = new byte[BITMAP_BYTES];
|
||||
ReorderingWindow window = new ReorderingWindow(0L, bitmap);
|
||||
window.setSeen(1L);
|
||||
long aboveMidpoint = REORDERING_WINDOW_SIZE / 2;
|
||||
// Set an element above the midpoint seen
|
||||
Change change = window.setSeen(aboveMidpoint);
|
||||
// The window should slide by two elements
|
||||
assertEquals(2L, window.getBase());
|
||||
assertEquals(Arrays.asList((long) REORDERING_WINDOW_SIZE,
|
||||
(long) (REORDERING_WINDOW_SIZE + 1)), change.getAdded());
|
||||
assertEquals(Arrays.asList(0L, aboveMidpoint), change.getRemoved());
|
||||
// The second-highest element below the midpoint should be seen
|
||||
bitmap[bitmap.length / 2 - 1] = (byte) 0x02; // 0000 0010
|
||||
assertArrayEquals(bitmap, window.getBitmap());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package org.briarproject.bramble.transport;
|
||||
|
||||
import org.briarproject.BriarTestCase;
|
||||
import org.briarproject.bramble.api.crypto.StreamDecrypter;
|
||||
import org.jmock.Expectations;
|
||||
import org.jmock.Mockery;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.briarproject.bramble.api.transport.TransportConstants.MAX_PAYLOAD_LENGTH;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class StreamReaderImplTest extends BriarTestCase {
|
||||
|
||||
@Test
|
||||
public void testEmptyFramesAreSkipped() throws Exception {
|
||||
Mockery context = new Mockery();
|
||||
final StreamDecrypter decrypter = context.mock(StreamDecrypter.class);
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(decrypter).readFrame(with(any(byte[].class)));
|
||||
will(returnValue(0)); // Empty frame
|
||||
oneOf(decrypter).readFrame(with(any(byte[].class)));
|
||||
will(returnValue(2)); // Non-empty frame with two payload bytes
|
||||
oneOf(decrypter).readFrame(with(any(byte[].class)));
|
||||
will(returnValue(0)); // Empty frame
|
||||
oneOf(decrypter).readFrame(with(any(byte[].class)));
|
||||
will(returnValue(-1)); // No more frames
|
||||
}});
|
||||
StreamReaderImpl r = new StreamReaderImpl(decrypter);
|
||||
assertEquals(0, r.read()); // Skip the first empty frame, read a byte
|
||||
assertEquals(0, r.read()); // Read another byte
|
||||
assertEquals(-1, r.read()); // Skip the second empty frame, reach EOF
|
||||
assertEquals(-1, r.read()); // Still at EOF
|
||||
context.assertIsSatisfied();
|
||||
r.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyFramesAreSkippedWithBuffer() throws Exception {
|
||||
Mockery context = new Mockery();
|
||||
final StreamDecrypter decrypter = context.mock(StreamDecrypter.class);
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(decrypter).readFrame(with(any(byte[].class)));
|
||||
will(returnValue(0)); // Empty frame
|
||||
oneOf(decrypter).readFrame(with(any(byte[].class)));
|
||||
will(returnValue(2)); // Non-empty frame with two payload bytes
|
||||
oneOf(decrypter).readFrame(with(any(byte[].class)));
|
||||
will(returnValue(0)); // Empty frame
|
||||
oneOf(decrypter).readFrame(with(any(byte[].class)));
|
||||
will(returnValue(-1)); // No more frames
|
||||
}});
|
||||
StreamReaderImpl r = new StreamReaderImpl(decrypter);
|
||||
byte[] buf = new byte[MAX_PAYLOAD_LENGTH];
|
||||
// Skip the first empty frame, read the two payload bytes
|
||||
assertEquals(2, r.read(buf));
|
||||
// Skip the second empty frame, reach EOF
|
||||
assertEquals(-1, r.read(buf));
|
||||
// Still at EOF
|
||||
assertEquals(-1, r.read(buf));
|
||||
context.assertIsSatisfied();
|
||||
r.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleReadsPerFrame() throws Exception {
|
||||
Mockery context = new Mockery();
|
||||
final StreamDecrypter decrypter = context.mock(StreamDecrypter.class);
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(decrypter).readFrame(with(any(byte[].class)));
|
||||
will(returnValue(MAX_PAYLOAD_LENGTH)); // Nice long frame
|
||||
oneOf(decrypter).readFrame(with(any(byte[].class)));
|
||||
will(returnValue(-1)); // No more frames
|
||||
}});
|
||||
StreamReaderImpl r = new StreamReaderImpl(decrypter);
|
||||
byte[] buf = new byte[MAX_PAYLOAD_LENGTH / 2];
|
||||
// Read the first half of the payload
|
||||
assertEquals(MAX_PAYLOAD_LENGTH / 2, r.read(buf));
|
||||
// Read the second half of the payload
|
||||
assertEquals(MAX_PAYLOAD_LENGTH / 2, r.read(buf));
|
||||
// Reach EOF
|
||||
assertEquals(-1, r.read(buf, 0, buf.length));
|
||||
context.assertIsSatisfied();
|
||||
r.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleReadsPerFrameWithOffsets() throws Exception {
|
||||
Mockery context = new Mockery();
|
||||
final StreamDecrypter decrypter = context.mock(StreamDecrypter.class);
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(decrypter).readFrame(with(any(byte[].class)));
|
||||
will(returnValue(MAX_PAYLOAD_LENGTH)); // Nice long frame
|
||||
oneOf(decrypter).readFrame(with(any(byte[].class)));
|
||||
will(returnValue(-1)); // No more frames
|
||||
}});
|
||||
StreamReaderImpl r = new StreamReaderImpl(decrypter);
|
||||
byte[] buf = new byte[MAX_PAYLOAD_LENGTH];
|
||||
// Read the first half of the payload
|
||||
assertEquals(MAX_PAYLOAD_LENGTH / 2, r.read(buf, MAX_PAYLOAD_LENGTH / 2,
|
||||
MAX_PAYLOAD_LENGTH / 2));
|
||||
// Read the second half of the payload
|
||||
assertEquals(MAX_PAYLOAD_LENGTH / 2, r.read(buf, 123,
|
||||
MAX_PAYLOAD_LENGTH / 2));
|
||||
// Reach EOF
|
||||
assertEquals(-1, r.read(buf, 0, buf.length));
|
||||
context.assertIsSatisfied();
|
||||
r.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package org.briarproject.bramble.transport;
|
||||
|
||||
import org.briarproject.BriarTestCase;
|
||||
import org.briarproject.TestUtils;
|
||||
import org.briarproject.bramble.api.crypto.StreamDecrypter;
|
||||
import org.briarproject.bramble.api.crypto.StreamEncrypter;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import static org.briarproject.bramble.api.transport.TransportConstants.FRAME_HEADER_LENGTH;
|
||||
import static org.briarproject.bramble.api.transport.TransportConstants.MAC_LENGTH;
|
||||
import static org.briarproject.bramble.api.transport.TransportConstants.STREAM_HEADER_LENGTH;
|
||||
import static org.briarproject.bramble.api.transport.TransportConstants.TAG_LENGTH;
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class StreamReaderWriterIntegrationTest extends BriarTestCase {
|
||||
|
||||
@Test
|
||||
public void testWriteAndRead() throws Exception {
|
||||
// Generate a random tag
|
||||
byte[] tag = TestUtils.getRandomBytes(TAG_LENGTH);
|
||||
// Generate two frames with random payloads
|
||||
byte[] payload1 = TestUtils.getRandomBytes(123);
|
||||
byte[] payload2 = TestUtils.getRandomBytes(321);
|
||||
// Write the tag and the frames
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
StreamEncrypter encrypter = new TestStreamEncrypter(out, tag);
|
||||
OutputStream streamWriter = new StreamWriterImpl(encrypter);
|
||||
streamWriter.write(payload1);
|
||||
streamWriter.flush();
|
||||
streamWriter.write(payload2);
|
||||
streamWriter.flush();
|
||||
byte[] output = out.toByteArray();
|
||||
assertEquals(TAG_LENGTH + STREAM_HEADER_LENGTH
|
||||
+ FRAME_HEADER_LENGTH + payload1.length + MAC_LENGTH
|
||||
+ FRAME_HEADER_LENGTH + payload2.length + MAC_LENGTH,
|
||||
output.length);
|
||||
// Read the tag back
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(output);
|
||||
byte[] recoveredTag = new byte[tag.length];
|
||||
read(in, recoveredTag);
|
||||
assertArrayEquals(tag, recoveredTag);
|
||||
// Read the frames back
|
||||
StreamDecrypter decrypter = new TestStreamDecrypter(in);
|
||||
InputStream streamReader = new StreamReaderImpl(decrypter);
|
||||
byte[] recoveredPayload1 = new byte[payload1.length];
|
||||
read(streamReader, recoveredPayload1);
|
||||
assertArrayEquals(payload1, recoveredPayload1);
|
||||
byte[] recoveredPayload2 = new byte[payload2.length];
|
||||
read(streamReader, recoveredPayload2);
|
||||
assertArrayEquals(payload2, recoveredPayload2);
|
||||
streamWriter.close();
|
||||
streamReader.close();
|
||||
}
|
||||
|
||||
private void read(InputStream in, byte[] dest) throws IOException {
|
||||
int offset = 0;
|
||||
while (offset < dest.length) {
|
||||
int read = in.read(dest, offset, dest.length - offset);
|
||||
if (read == -1) break;
|
||||
offset += read;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package org.briarproject.bramble.transport;
|
||||
|
||||
import org.briarproject.BriarTestCase;
|
||||
import org.briarproject.bramble.api.crypto.StreamEncrypter;
|
||||
import org.jmock.Expectations;
|
||||
import org.jmock.Mockery;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.briarproject.bramble.api.transport.TransportConstants.MAX_PAYLOAD_LENGTH;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class StreamWriterImplTest extends BriarTestCase {
|
||||
|
||||
@Test
|
||||
public void testCloseWithoutWritingWritesFinalFrame() throws Exception {
|
||||
Mockery context = new Mockery();
|
||||
final StreamEncrypter encrypter = context.mock(StreamEncrypter.class);
|
||||
context.checking(new Expectations() {{
|
||||
// Write an empty final frame
|
||||
oneOf(encrypter).writeFrame(with(any(byte[].class)), with(0),
|
||||
with(0), with(true));
|
||||
// Flush the stream
|
||||
oneOf(encrypter).flush();
|
||||
}});
|
||||
StreamWriterImpl w = new StreamWriterImpl(encrypter);
|
||||
w.close();
|
||||
context.assertIsSatisfied();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFlushWithoutBufferedDataWritesFrameAndFlushes()
|
||||
throws Exception {
|
||||
Mockery context = new Mockery();
|
||||
final StreamEncrypter encrypter = context.mock(StreamEncrypter.class);
|
||||
StreamWriterImpl w = new StreamWriterImpl(encrypter);
|
||||
context.checking(new Expectations() {{
|
||||
// Write a non-final frame with an empty payload
|
||||
oneOf(encrypter).writeFrame(with(any(byte[].class)), with(0),
|
||||
with(0), with(false));
|
||||
// Flush the stream
|
||||
oneOf(encrypter).flush();
|
||||
}});
|
||||
w.flush();
|
||||
context.assertIsSatisfied();
|
||||
|
||||
// Clean up
|
||||
context.checking(new Expectations() {{
|
||||
// Closing the writer writes a final frame and flushes again
|
||||
oneOf(encrypter).writeFrame(with(any(byte[].class)), with(0),
|
||||
with(0), with(true));
|
||||
oneOf(encrypter).flush();
|
||||
}});
|
||||
w.close();
|
||||
context.assertIsSatisfied();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFlushWithBufferedDataWritesFrameAndFlushes()
|
||||
throws Exception {
|
||||
Mockery context = new Mockery();
|
||||
final StreamEncrypter encrypter = context.mock(StreamEncrypter.class);
|
||||
StreamWriterImpl w = new StreamWriterImpl(encrypter);
|
||||
context.checking(new Expectations() {{
|
||||
// Write a non-final frame with one payload byte
|
||||
oneOf(encrypter).writeFrame(with(any(byte[].class)), with(1),
|
||||
with(0), with(false));
|
||||
// Flush the stream
|
||||
oneOf(encrypter).flush();
|
||||
}});
|
||||
w.write(0);
|
||||
w.flush();
|
||||
context.assertIsSatisfied();
|
||||
|
||||
// Clean up
|
||||
context.checking(new Expectations() {{
|
||||
// Closing the writer writes a final frame and flushes again
|
||||
oneOf(encrypter).writeFrame(with(any(byte[].class)), with(0),
|
||||
with(0), with(true));
|
||||
oneOf(encrypter).flush();
|
||||
}});
|
||||
w.close();
|
||||
context.assertIsSatisfied();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSingleByteWritesWriteFullFrame() throws Exception {
|
||||
Mockery context = new Mockery();
|
||||
final StreamEncrypter encrypter = context.mock(StreamEncrypter.class);
|
||||
StreamWriterImpl w = new StreamWriterImpl(encrypter);
|
||||
context.checking(new Expectations() {{
|
||||
// Write a full non-final frame
|
||||
oneOf(encrypter).writeFrame(with(any(byte[].class)),
|
||||
with(MAX_PAYLOAD_LENGTH), with(0), with(false));
|
||||
}});
|
||||
for (int i = 0; i < MAX_PAYLOAD_LENGTH; i++) w.write(0);
|
||||
context.assertIsSatisfied();
|
||||
|
||||
// Clean up
|
||||
context.checking(new Expectations() {{
|
||||
// Closing the writer writes a final frame and flushes again
|
||||
oneOf(encrypter).writeFrame(with(any(byte[].class)), with(0),
|
||||
with(0), with(true));
|
||||
oneOf(encrypter).flush();
|
||||
}});
|
||||
w.close();
|
||||
context.assertIsSatisfied();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultiByteWritesWriteFullFrames() throws Exception {
|
||||
Mockery context = new Mockery();
|
||||
final StreamEncrypter encrypter = context.mock(StreamEncrypter.class);
|
||||
StreamWriterImpl w = new StreamWriterImpl(encrypter);
|
||||
context.checking(new Expectations() {{
|
||||
// Write two full non-final frames
|
||||
exactly(2).of(encrypter).writeFrame(with(any(byte[].class)),
|
||||
with(MAX_PAYLOAD_LENGTH), with(0), with(false));
|
||||
}});
|
||||
// Sanity check
|
||||
assertEquals(0, MAX_PAYLOAD_LENGTH % 2);
|
||||
// Write two full payloads using four multi-byte writes
|
||||
byte[] b = new byte[MAX_PAYLOAD_LENGTH / 2];
|
||||
w.write(b);
|
||||
w.write(b);
|
||||
w.write(b);
|
||||
w.write(b);
|
||||
context.assertIsSatisfied();
|
||||
|
||||
// Clean up
|
||||
context.checking(new Expectations() {{
|
||||
// Closing the writer writes a final frame and flushes again
|
||||
oneOf(encrypter).writeFrame(with(any(byte[].class)), with(0),
|
||||
with(0), with(true));
|
||||
oneOf(encrypter).flush();
|
||||
}});
|
||||
w.close();
|
||||
context.assertIsSatisfied();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLargeMultiByteWriteWritesFullFrames() throws Exception {
|
||||
Mockery context = new Mockery();
|
||||
final StreamEncrypter encrypter = context.mock(StreamEncrypter.class);
|
||||
StreamWriterImpl w = new StreamWriterImpl(encrypter);
|
||||
context.checking(new Expectations() {{
|
||||
// Write two full non-final frames
|
||||
exactly(2).of(encrypter).writeFrame(with(any(byte[].class)),
|
||||
with(MAX_PAYLOAD_LENGTH), with(0), with(false));
|
||||
// Write a final frame with a one-byte payload
|
||||
oneOf(encrypter).writeFrame(with(any(byte[].class)), with(1),
|
||||
with(0), with(true));
|
||||
// Flush the stream
|
||||
oneOf(encrypter).flush();
|
||||
}});
|
||||
// Write two full payloads using one large multi-byte write
|
||||
byte[] b = new byte[MAX_PAYLOAD_LENGTH * 2 + 1];
|
||||
w.write(b);
|
||||
// There should be one byte left in the buffer
|
||||
w.close();
|
||||
context.assertIsSatisfied();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.briarproject.bramble.transport;
|
||||
|
||||
import org.briarproject.bramble.api.crypto.StreamDecrypter;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.util.ByteUtils;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import static org.briarproject.bramble.api.transport.TransportConstants.FRAME_HEADER_LENGTH;
|
||||
import static org.briarproject.bramble.api.transport.TransportConstants.MAC_LENGTH;
|
||||
import static org.briarproject.bramble.api.transport.TransportConstants.MAX_FRAME_LENGTH;
|
||||
import static org.briarproject.bramble.api.transport.TransportConstants.STREAM_HEADER_LENGTH;
|
||||
import static org.briarproject.bramble.util.ByteUtils.INT_16_BYTES;
|
||||
|
||||
@NotNullByDefault
|
||||
class TestStreamDecrypter implements StreamDecrypter {
|
||||
|
||||
private final InputStream in;
|
||||
private final byte[] frame;
|
||||
|
||||
private boolean readStreamHeader = true, finalFrame = false;
|
||||
|
||||
TestStreamDecrypter(InputStream in) {
|
||||
this.in = in;
|
||||
frame = new byte[MAX_FRAME_LENGTH];
|
||||
}
|
||||
|
||||
@Override
|
||||
public int readFrame(byte[] payload) throws IOException {
|
||||
if (finalFrame) return -1;
|
||||
if (readStreamHeader) readStreamHeader();
|
||||
int offset = 0;
|
||||
while (offset < FRAME_HEADER_LENGTH) {
|
||||
int read = in.read(frame, offset, FRAME_HEADER_LENGTH - offset);
|
||||
if (read == -1) throw new EOFException();
|
||||
offset += read;
|
||||
}
|
||||
finalFrame = (frame[0] & 0x80) == 0x80;
|
||||
int payloadLength = ByteUtils.readUint16(frame, 0) & 0x7FFF;
|
||||
int paddingLength = ByteUtils.readUint16(frame, INT_16_BYTES);
|
||||
int frameLength = FRAME_HEADER_LENGTH + payloadLength + paddingLength
|
||||
+ MAC_LENGTH;
|
||||
while (offset < frameLength) {
|
||||
int read = in.read(frame, offset, frameLength - offset);
|
||||
if (read == -1) throw new EOFException();
|
||||
offset += read;
|
||||
}
|
||||
System.arraycopy(frame, FRAME_HEADER_LENGTH, payload, 0, payloadLength);
|
||||
return payloadLength;
|
||||
}
|
||||
|
||||
private void readStreamHeader() throws IOException {
|
||||
byte[] streamHeader = new byte[STREAM_HEADER_LENGTH];
|
||||
int offset = 0;
|
||||
while (offset < STREAM_HEADER_LENGTH) {
|
||||
int read = in.read(streamHeader, offset,
|
||||
STREAM_HEADER_LENGTH - offset);
|
||||
if (read == -1) throw new EOFException();
|
||||
offset += read;
|
||||
}
|
||||
readStreamHeader = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.briarproject.bramble.transport;
|
||||
|
||||
import org.briarproject.bramble.api.crypto.StreamEncrypter;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.util.ByteUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import static org.briarproject.bramble.api.transport.TransportConstants.FRAME_HEADER_LENGTH;
|
||||
import static org.briarproject.bramble.api.transport.TransportConstants.MAC_LENGTH;
|
||||
import static org.briarproject.bramble.api.transport.TransportConstants.STREAM_HEADER_LENGTH;
|
||||
import static org.briarproject.bramble.util.ByteUtils.INT_16_BYTES;
|
||||
|
||||
@NotNullByDefault
|
||||
class TestStreamEncrypter implements StreamEncrypter {
|
||||
|
||||
private final OutputStream out;
|
||||
private final byte[] tag;
|
||||
|
||||
private boolean writeTagAndHeader = true;
|
||||
|
||||
TestStreamEncrypter(OutputStream out, byte[] tag) {
|
||||
this.out = out;
|
||||
this.tag = tag;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeFrame(byte[] payload, int payloadLength,
|
||||
int paddingLength, boolean finalFrame) throws IOException {
|
||||
if (writeTagAndHeader) writeTagAndHeader();
|
||||
byte[] frameHeader = new byte[FRAME_HEADER_LENGTH];
|
||||
ByteUtils.writeUint16(payloadLength, frameHeader, 0);
|
||||
ByteUtils.writeUint16(paddingLength, frameHeader, INT_16_BYTES);
|
||||
if (finalFrame) frameHeader[0] |= 0x80;
|
||||
out.write(frameHeader);
|
||||
out.write(payload, 0, payloadLength);
|
||||
out.write(new byte[paddingLength]);
|
||||
out.write(new byte[MAC_LENGTH]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
if (writeTagAndHeader) writeTagAndHeader();
|
||||
out.flush();
|
||||
}
|
||||
|
||||
private void writeTagAndHeader() throws IOException {
|
||||
out.write(tag);
|
||||
out.write(new byte[STREAM_HEADER_LENGTH]);
|
||||
writeTagAndHeader = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,512 @@
|
||||
package org.briarproject.bramble.transport;
|
||||
|
||||
import org.briarproject.BriarTestCase;
|
||||
import org.briarproject.RunAction;
|
||||
import org.briarproject.TestUtils;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||
import org.briarproject.bramble.api.crypto.SecretKey;
|
||||
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||
import org.briarproject.bramble.api.db.Transaction;
|
||||
import org.briarproject.bramble.api.plugin.TransportId;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.api.transport.IncomingKeys;
|
||||
import org.briarproject.bramble.api.transport.OutgoingKeys;
|
||||
import org.briarproject.bramble.api.transport.StreamContext;
|
||||
import org.briarproject.bramble.api.transport.TransportKeys;
|
||||
import org.hamcrest.Description;
|
||||
import org.jmock.Expectations;
|
||||
import org.jmock.Mockery;
|
||||
import org.jmock.api.Action;
|
||||
import org.jmock.api.Invocation;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static org.briarproject.bramble.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE;
|
||||
import static org.briarproject.bramble.api.transport.TransportConstants.REORDERING_WINDOW_SIZE;
|
||||
import static org.briarproject.bramble.api.transport.TransportConstants.TAG_LENGTH;
|
||||
import static org.briarproject.bramble.util.ByteUtils.MAX_32_BIT_UNSIGNED;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
public class TransportKeyManagerImplTest extends BriarTestCase {
|
||||
|
||||
private final TransportId transportId = new TransportId("id");
|
||||
private final long maxLatency = 30 * 1000; // 30 seconds
|
||||
private final long rotationPeriodLength = maxLatency + MAX_CLOCK_DIFFERENCE;
|
||||
private final ContactId contactId = new ContactId(123);
|
||||
private final ContactId contactId1 = new ContactId(234);
|
||||
private final SecretKey tagKey = TestUtils.getSecretKey();
|
||||
private final SecretKey headerKey = TestUtils.getSecretKey();
|
||||
private final SecretKey masterKey = TestUtils.getSecretKey();
|
||||
private final Random random = new Random();
|
||||
|
||||
@Test
|
||||
public void testKeysAreRotatedAtStartup() throws Exception {
|
||||
Mockery context = new Mockery();
|
||||
final DatabaseComponent db = context.mock(DatabaseComponent.class);
|
||||
final CryptoComponent crypto = context.mock(CryptoComponent.class);
|
||||
final Executor dbExecutor = context.mock(Executor.class);
|
||||
final ScheduledExecutorService scheduler =
|
||||
context.mock(ScheduledExecutorService.class);
|
||||
final Clock clock = context.mock(Clock.class);
|
||||
|
||||
final Map<ContactId, TransportKeys> loaded = new LinkedHashMap<>();
|
||||
final TransportKeys shouldRotate = createTransportKeys(900, 0);
|
||||
final TransportKeys shouldNotRotate = createTransportKeys(1000, 0);
|
||||
loaded.put(contactId, shouldRotate);
|
||||
loaded.put(contactId1, shouldNotRotate);
|
||||
final TransportKeys rotated = createTransportKeys(1000, 0);
|
||||
final Transaction txn = new Transaction(null, false);
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
// Get the current time (1 ms after start of rotation period 1000)
|
||||
oneOf(clock).currentTimeMillis();
|
||||
will(returnValue(rotationPeriodLength * 1000 + 1));
|
||||
// Load the transport keys
|
||||
oneOf(db).getTransportKeys(txn, transportId);
|
||||
will(returnValue(loaded));
|
||||
// Rotate the transport keys
|
||||
oneOf(crypto).rotateTransportKeys(shouldRotate, 1000);
|
||||
will(returnValue(rotated));
|
||||
oneOf(crypto).rotateTransportKeys(shouldNotRotate, 1000);
|
||||
will(returnValue(shouldNotRotate));
|
||||
// Encode the tags (3 sets per contact)
|
||||
for (long i = 0; i < REORDERING_WINDOW_SIZE; i++) {
|
||||
exactly(6).of(crypto).encodeTag(with(any(byte[].class)),
|
||||
with(tagKey), with(i));
|
||||
will(new EncodeTagAction());
|
||||
}
|
||||
// Save the keys that were rotated
|
||||
oneOf(db).updateTransportKeys(txn,
|
||||
Collections.singletonMap(contactId, rotated));
|
||||
// Schedule key rotation at the start of the next rotation period
|
||||
oneOf(scheduler).schedule(with(any(Runnable.class)),
|
||||
with(rotationPeriodLength - 1), with(MILLISECONDS));
|
||||
}});
|
||||
|
||||
TransportKeyManager
|
||||
transportKeyManager = new TransportKeyManagerImpl(db,
|
||||
crypto, dbExecutor, scheduler, clock, transportId, maxLatency);
|
||||
transportKeyManager.start(txn);
|
||||
|
||||
context.assertIsSatisfied();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeysAreRotatedWhenAddingContact() throws Exception {
|
||||
Mockery context = new Mockery();
|
||||
final DatabaseComponent db = context.mock(DatabaseComponent.class);
|
||||
final CryptoComponent crypto = context.mock(CryptoComponent.class);
|
||||
final Executor dbExecutor = context.mock(Executor.class);
|
||||
final ScheduledExecutorService scheduler =
|
||||
context.mock(ScheduledExecutorService.class);
|
||||
final Clock clock = context.mock(Clock.class);
|
||||
|
||||
final boolean alice = true;
|
||||
final TransportKeys transportKeys = createTransportKeys(999, 0);
|
||||
final TransportKeys rotated = createTransportKeys(1000, 0);
|
||||
final Transaction txn = new Transaction(null, false);
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(crypto).deriveTransportKeys(transportId, masterKey, 999,
|
||||
alice);
|
||||
will(returnValue(transportKeys));
|
||||
// Get the current time (1 ms after start of rotation period 1000)
|
||||
oneOf(clock).currentTimeMillis();
|
||||
will(returnValue(rotationPeriodLength * 1000 + 1));
|
||||
// Rotate the transport keys
|
||||
oneOf(crypto).rotateTransportKeys(transportKeys, 1000);
|
||||
will(returnValue(rotated));
|
||||
// Encode the tags (3 sets)
|
||||
for (long i = 0; i < REORDERING_WINDOW_SIZE; i++) {
|
||||
exactly(3).of(crypto).encodeTag(with(any(byte[].class)),
|
||||
with(tagKey), with(i));
|
||||
will(new EncodeTagAction());
|
||||
}
|
||||
// Save the keys
|
||||
oneOf(db).addTransportKeys(txn, contactId, rotated);
|
||||
}});
|
||||
|
||||
TransportKeyManager
|
||||
transportKeyManager = new TransportKeyManagerImpl(db,
|
||||
crypto, dbExecutor, scheduler, clock, transportId, maxLatency);
|
||||
// The timestamp is 1 ms before the start of rotation period 1000
|
||||
long timestamp = rotationPeriodLength * 1000 - 1;
|
||||
transportKeyManager.addContact(txn, contactId, masterKey, timestamp,
|
||||
alice);
|
||||
|
||||
context.assertIsSatisfied();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOutgoingStreamContextIsNullIfContactIsNotFound()
|
||||
throws Exception {
|
||||
Mockery context = new Mockery();
|
||||
final DatabaseComponent db = context.mock(DatabaseComponent.class);
|
||||
final CryptoComponent crypto = context.mock(CryptoComponent.class);
|
||||
final Executor dbExecutor = context.mock(Executor.class);
|
||||
final ScheduledExecutorService scheduler =
|
||||
context.mock(ScheduledExecutorService.class);
|
||||
final Clock clock = context.mock(Clock.class);
|
||||
|
||||
final Transaction txn = new Transaction(null, false);
|
||||
|
||||
TransportKeyManager
|
||||
transportKeyManager = new TransportKeyManagerImpl(db,
|
||||
crypto, dbExecutor, scheduler, clock, transportId, maxLatency);
|
||||
assertNull(transportKeyManager.getStreamContext(txn, contactId));
|
||||
|
||||
context.assertIsSatisfied();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOutgoingStreamContextIsNullIfStreamCounterIsExhausted()
|
||||
throws Exception {
|
||||
Mockery context = new Mockery();
|
||||
final DatabaseComponent db = context.mock(DatabaseComponent.class);
|
||||
final CryptoComponent crypto = context.mock(CryptoComponent.class);
|
||||
final Executor dbExecutor = context.mock(Executor.class);
|
||||
final ScheduledExecutorService scheduler =
|
||||
context.mock(ScheduledExecutorService.class);
|
||||
final Clock clock = context.mock(Clock.class);
|
||||
|
||||
final boolean alice = true;
|
||||
// The stream counter has been exhausted
|
||||
final TransportKeys transportKeys = createTransportKeys(1000,
|
||||
MAX_32_BIT_UNSIGNED + 1);
|
||||
final Transaction txn = new Transaction(null, false);
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(crypto).deriveTransportKeys(transportId, masterKey, 1000,
|
||||
alice);
|
||||
will(returnValue(transportKeys));
|
||||
// Get the current time (the start of rotation period 1000)
|
||||
oneOf(clock).currentTimeMillis();
|
||||
will(returnValue(rotationPeriodLength * 1000));
|
||||
// Encode the tags (3 sets)
|
||||
for (long i = 0; i < REORDERING_WINDOW_SIZE; i++) {
|
||||
exactly(3).of(crypto).encodeTag(with(any(byte[].class)),
|
||||
with(tagKey), with(i));
|
||||
will(new EncodeTagAction());
|
||||
}
|
||||
// Rotate the transport keys (the keys are unaffected)
|
||||
oneOf(crypto).rotateTransportKeys(transportKeys, 1000);
|
||||
will(returnValue(transportKeys));
|
||||
// Save the keys
|
||||
oneOf(db).addTransportKeys(txn, contactId, transportKeys);
|
||||
}});
|
||||
|
||||
TransportKeyManager
|
||||
transportKeyManager = new TransportKeyManagerImpl(db,
|
||||
crypto, dbExecutor, scheduler, clock, transportId, maxLatency);
|
||||
// The timestamp is at the start of rotation period 1000
|
||||
long timestamp = rotationPeriodLength * 1000;
|
||||
transportKeyManager.addContact(txn, contactId, masterKey, timestamp,
|
||||
alice);
|
||||
assertNull(transportKeyManager.getStreamContext(txn, contactId));
|
||||
|
||||
context.assertIsSatisfied();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOutgoingStreamCounterIsIncremented() throws Exception {
|
||||
Mockery context = new Mockery();
|
||||
final DatabaseComponent db = context.mock(DatabaseComponent.class);
|
||||
final CryptoComponent crypto = context.mock(CryptoComponent.class);
|
||||
final Executor dbExecutor = context.mock(Executor.class);
|
||||
final ScheduledExecutorService scheduler =
|
||||
context.mock(ScheduledExecutorService.class);
|
||||
final Clock clock = context.mock(Clock.class);
|
||||
|
||||
final boolean alice = true;
|
||||
// The stream counter can be used one more time before being exhausted
|
||||
final TransportKeys transportKeys = createTransportKeys(1000,
|
||||
MAX_32_BIT_UNSIGNED);
|
||||
final Transaction txn = new Transaction(null, false);
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(crypto).deriveTransportKeys(transportId, masterKey, 1000,
|
||||
alice);
|
||||
will(returnValue(transportKeys));
|
||||
// Get the current time (the start of rotation period 1000)
|
||||
oneOf(clock).currentTimeMillis();
|
||||
will(returnValue(rotationPeriodLength * 1000));
|
||||
// Encode the tags (3 sets)
|
||||
for (long i = 0; i < REORDERING_WINDOW_SIZE; i++) {
|
||||
exactly(3).of(crypto).encodeTag(with(any(byte[].class)),
|
||||
with(tagKey), with(i));
|
||||
will(new EncodeTagAction());
|
||||
}
|
||||
// Rotate the transport keys (the keys are unaffected)
|
||||
oneOf(crypto).rotateTransportKeys(transportKeys, 1000);
|
||||
will(returnValue(transportKeys));
|
||||
// Save the keys
|
||||
oneOf(db).addTransportKeys(txn, contactId, transportKeys);
|
||||
// Increment the stream counter
|
||||
oneOf(db).incrementStreamCounter(txn, contactId, transportId, 1000);
|
||||
}});
|
||||
|
||||
TransportKeyManager
|
||||
transportKeyManager = new TransportKeyManagerImpl(db,
|
||||
crypto, dbExecutor, scheduler, clock, transportId, maxLatency);
|
||||
// The timestamp is at the start of rotation period 1000
|
||||
long timestamp = rotationPeriodLength * 1000;
|
||||
transportKeyManager.addContact(txn, contactId, masterKey, timestamp,
|
||||
alice);
|
||||
// The first request should return a stream context
|
||||
StreamContext ctx = transportKeyManager.getStreamContext(txn,
|
||||
contactId);
|
||||
assertNotNull(ctx);
|
||||
assertEquals(contactId, ctx.getContactId());
|
||||
assertEquals(transportId, ctx.getTransportId());
|
||||
assertEquals(tagKey, ctx.getTagKey());
|
||||
assertEquals(headerKey, ctx.getHeaderKey());
|
||||
assertEquals(MAX_32_BIT_UNSIGNED, ctx.getStreamNumber());
|
||||
// The second request should return null, the counter is exhausted
|
||||
assertNull(transportKeyManager.getStreamContext(txn, contactId));
|
||||
|
||||
context.assertIsSatisfied();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIncomingStreamContextIsNullIfTagIsNotFound()
|
||||
throws Exception {
|
||||
Mockery context = new Mockery();
|
||||
final DatabaseComponent db = context.mock(DatabaseComponent.class);
|
||||
final CryptoComponent crypto = context.mock(CryptoComponent.class);
|
||||
final Executor dbExecutor = context.mock(Executor.class);
|
||||
final ScheduledExecutorService scheduler =
|
||||
context.mock(ScheduledExecutorService.class);
|
||||
final Clock clock = context.mock(Clock.class);
|
||||
|
||||
final boolean alice = true;
|
||||
final TransportKeys transportKeys = createTransportKeys(1000, 0);
|
||||
final Transaction txn = new Transaction(null, false);
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(crypto).deriveTransportKeys(transportId, masterKey, 1000,
|
||||
alice);
|
||||
will(returnValue(transportKeys));
|
||||
// Get the current time (the start of rotation period 1000)
|
||||
oneOf(clock).currentTimeMillis();
|
||||
will(returnValue(rotationPeriodLength * 1000));
|
||||
// Encode the tags (3 sets)
|
||||
for (long i = 0; i < REORDERING_WINDOW_SIZE; i++) {
|
||||
exactly(3).of(crypto).encodeTag(with(any(byte[].class)),
|
||||
with(tagKey), with(i));
|
||||
will(new EncodeTagAction());
|
||||
}
|
||||
// Rotate the transport keys (the keys are unaffected)
|
||||
oneOf(crypto).rotateTransportKeys(transportKeys, 1000);
|
||||
will(returnValue(transportKeys));
|
||||
// Save the keys
|
||||
oneOf(db).addTransportKeys(txn, contactId, transportKeys);
|
||||
}});
|
||||
|
||||
TransportKeyManager
|
||||
transportKeyManager = new TransportKeyManagerImpl(db,
|
||||
crypto, dbExecutor, scheduler, clock, transportId, maxLatency);
|
||||
// The timestamp is at the start of rotation period 1000
|
||||
long timestamp = rotationPeriodLength * 1000;
|
||||
transportKeyManager.addContact(txn, contactId, masterKey, timestamp,
|
||||
alice);
|
||||
assertNull(transportKeyManager.getStreamContext(txn,
|
||||
new byte[TAG_LENGTH]));
|
||||
|
||||
context.assertIsSatisfied();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTagIsNotRecognisedTwice() throws Exception {
|
||||
Mockery context = new Mockery();
|
||||
final DatabaseComponent db = context.mock(DatabaseComponent.class);
|
||||
final CryptoComponent crypto = context.mock(CryptoComponent.class);
|
||||
final Executor dbExecutor = context.mock(Executor.class);
|
||||
final ScheduledExecutorService scheduler =
|
||||
context.mock(ScheduledExecutorService.class);
|
||||
final Clock clock = context.mock(Clock.class);
|
||||
|
||||
final boolean alice = true;
|
||||
final TransportKeys transportKeys = createTransportKeys(1000, 0);
|
||||
// Keep a copy of the tags
|
||||
final List<byte[]> tags = new ArrayList<>();
|
||||
final Transaction txn = new Transaction(null, false);
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(crypto).deriveTransportKeys(transportId, masterKey, 1000,
|
||||
alice);
|
||||
will(returnValue(transportKeys));
|
||||
// Get the current time (the start of rotation period 1000)
|
||||
oneOf(clock).currentTimeMillis();
|
||||
will(returnValue(rotationPeriodLength * 1000));
|
||||
// Encode the tags (3 sets)
|
||||
for (long i = 0; i < REORDERING_WINDOW_SIZE; i++) {
|
||||
exactly(3).of(crypto).encodeTag(with(any(byte[].class)),
|
||||
with(tagKey), with(i));
|
||||
will(new EncodeTagAction(tags));
|
||||
}
|
||||
// Rotate the transport keys (the keys are unaffected)
|
||||
oneOf(crypto).rotateTransportKeys(transportKeys, 1000);
|
||||
will(returnValue(transportKeys));
|
||||
// Save the keys
|
||||
oneOf(db).addTransportKeys(txn, contactId, transportKeys);
|
||||
// Encode a new tag after sliding the window
|
||||
oneOf(crypto).encodeTag(with(any(byte[].class)),
|
||||
with(tagKey), with((long) REORDERING_WINDOW_SIZE));
|
||||
will(new EncodeTagAction(tags));
|
||||
// Save the reordering window (previous rotation period, base 1)
|
||||
oneOf(db).setReorderingWindow(txn, contactId, transportId, 999,
|
||||
1, new byte[REORDERING_WINDOW_SIZE / 8]);
|
||||
}});
|
||||
|
||||
TransportKeyManager
|
||||
transportKeyManager = new TransportKeyManagerImpl(db,
|
||||
crypto, dbExecutor, scheduler, clock, transportId, maxLatency);
|
||||
// The timestamp is at the start of rotation period 1000
|
||||
long timestamp = rotationPeriodLength * 1000;
|
||||
transportKeyManager.addContact(txn, contactId, masterKey, timestamp,
|
||||
alice);
|
||||
// Use the first tag (previous rotation period, stream number 0)
|
||||
assertEquals(REORDERING_WINDOW_SIZE * 3, tags.size());
|
||||
byte[] tag = tags.get(0);
|
||||
// The first request should return a stream context
|
||||
StreamContext ctx = transportKeyManager.getStreamContext(txn, tag);
|
||||
assertNotNull(ctx);
|
||||
assertEquals(contactId, ctx.getContactId());
|
||||
assertEquals(transportId, ctx.getTransportId());
|
||||
assertEquals(tagKey, ctx.getTagKey());
|
||||
assertEquals(headerKey, ctx.getHeaderKey());
|
||||
assertEquals(0L, ctx.getStreamNumber());
|
||||
// Another tag should have been encoded
|
||||
assertEquals(REORDERING_WINDOW_SIZE * 3 + 1, tags.size());
|
||||
// The second request should return null, the tag has already been used
|
||||
assertNull(transportKeyManager.getStreamContext(txn, tag));
|
||||
|
||||
context.assertIsSatisfied();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeysAreRotatedToCurrentPeriod() throws Exception {
|
||||
Mockery context = new Mockery();
|
||||
final DatabaseComponent db = context.mock(DatabaseComponent.class);
|
||||
final CryptoComponent crypto = context.mock(CryptoComponent.class);
|
||||
final Executor dbExecutor = context.mock(Executor.class);
|
||||
final ScheduledExecutorService scheduler =
|
||||
context.mock(ScheduledExecutorService.class);
|
||||
final Clock clock = context.mock(Clock.class);
|
||||
|
||||
final TransportKeys transportKeys = createTransportKeys(1000, 0);
|
||||
final Map<ContactId, TransportKeys> loaded =
|
||||
Collections.singletonMap(contactId, transportKeys);
|
||||
final TransportKeys rotated = createTransportKeys(1001, 0);
|
||||
final Transaction txn = new Transaction(null, false);
|
||||
final Transaction txn1 = new Transaction(null, false);
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
// Get the current time (the start of rotation period 1000)
|
||||
oneOf(clock).currentTimeMillis();
|
||||
will(returnValue(rotationPeriodLength * 1000));
|
||||
// Load the transport keys
|
||||
oneOf(db).getTransportKeys(txn, transportId);
|
||||
will(returnValue(loaded));
|
||||
// Rotate the transport keys (the keys are unaffected)
|
||||
oneOf(crypto).rotateTransportKeys(transportKeys, 1000);
|
||||
will(returnValue(transportKeys));
|
||||
// Encode the tags (3 sets)
|
||||
for (long i = 0; i < REORDERING_WINDOW_SIZE; i++) {
|
||||
exactly(3).of(crypto).encodeTag(with(any(byte[].class)),
|
||||
with(tagKey), with(i));
|
||||
will(new EncodeTagAction());
|
||||
}
|
||||
// Schedule key rotation at the start of the next rotation period
|
||||
oneOf(scheduler).schedule(with(any(Runnable.class)),
|
||||
with(rotationPeriodLength), with(MILLISECONDS));
|
||||
will(new RunAction());
|
||||
oneOf(dbExecutor).execute(with(any(Runnable.class)));
|
||||
will(new RunAction());
|
||||
// Start a transaction for key rotation
|
||||
oneOf(db).startTransaction(false);
|
||||
will(returnValue(txn1));
|
||||
// Get the current time (the start of rotation period 1001)
|
||||
oneOf(clock).currentTimeMillis();
|
||||
will(returnValue(rotationPeriodLength * 1001));
|
||||
// Rotate the transport keys
|
||||
oneOf(crypto).rotateTransportKeys(with(any(TransportKeys.class)),
|
||||
with(1001L));
|
||||
will(returnValue(rotated));
|
||||
// Encode the tags (3 sets)
|
||||
for (long i = 0; i < REORDERING_WINDOW_SIZE; i++) {
|
||||
exactly(3).of(crypto).encodeTag(with(any(byte[].class)),
|
||||
with(tagKey), with(i));
|
||||
will(new EncodeTagAction());
|
||||
}
|
||||
// Save the keys that were rotated
|
||||
oneOf(db).updateTransportKeys(txn1,
|
||||
Collections.singletonMap(contactId, rotated));
|
||||
// Schedule key rotation at the start of the next rotation period
|
||||
oneOf(scheduler).schedule(with(any(Runnable.class)),
|
||||
with(rotationPeriodLength), with(MILLISECONDS));
|
||||
// Commit the key rotation transaction
|
||||
oneOf(db).commitTransaction(txn1);
|
||||
oneOf(db).endTransaction(txn1);
|
||||
}});
|
||||
|
||||
TransportKeyManager
|
||||
transportKeyManager = new TransportKeyManagerImpl(db,
|
||||
crypto, dbExecutor, scheduler, clock, transportId, maxLatency);
|
||||
transportKeyManager.start(txn);
|
||||
|
||||
context.assertIsSatisfied();
|
||||
}
|
||||
|
||||
private TransportKeys createTransportKeys(long rotationPeriod,
|
||||
long streamCounter) {
|
||||
IncomingKeys inPrev = new IncomingKeys(tagKey, headerKey,
|
||||
rotationPeriod - 1);
|
||||
IncomingKeys inCurr = new IncomingKeys(tagKey, headerKey,
|
||||
rotationPeriod);
|
||||
IncomingKeys inNext = new IncomingKeys(tagKey, headerKey,
|
||||
rotationPeriod + 1);
|
||||
OutgoingKeys outCurr = new OutgoingKeys(tagKey, headerKey,
|
||||
rotationPeriod, streamCounter);
|
||||
return new TransportKeys(transportId, inPrev, inCurr, inNext, outCurr);
|
||||
}
|
||||
|
||||
private class EncodeTagAction implements Action {
|
||||
|
||||
private final Collection<byte[]> tags;
|
||||
|
||||
private EncodeTagAction() {
|
||||
tags = null;
|
||||
}
|
||||
|
||||
private EncodeTagAction(Collection<byte[]> tags) {
|
||||
this.tags = tags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(Invocation invocation) throws Throwable {
|
||||
byte[] tag = (byte[]) invocation.getParameter(0);
|
||||
random.nextBytes(tag);
|
||||
if (tags != null) tags.add(tag);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText("encodes a tag");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user