Skip to content
Snippets Groups Projects
Forked from briar / briar
4755 commits behind the upstream repository.
PrivateGroupManagerTest.java 21.75 KiB
package org.briarproject;

import net.jodah.concurrentunit.Waiter;

import org.briarproject.api.clients.ClientHelper;
import org.briarproject.api.clients.ContactGroupFactory;
import org.briarproject.api.clients.MessageTracker.GroupCount;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.KeyPair;
import org.briarproject.api.crypto.SecretKey;
import org.briarproject.api.data.BdfList;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.Transaction;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.MessageStateChangedEvent;
import org.briarproject.api.identity.AuthorFactory;
import org.briarproject.api.identity.IdentityManager;
import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.privategroup.GroupMessage;
import org.briarproject.api.privategroup.GroupMessageFactory;
import org.briarproject.api.privategroup.GroupMessageHeader;
import org.briarproject.api.privategroup.JoinMessageHeader;
import org.briarproject.api.privategroup.PrivateGroup;
import org.briarproject.api.privategroup.PrivateGroupFactory;
import org.briarproject.api.privategroup.PrivateGroupManager;
import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import org.briarproject.api.sync.SyncSession;
import org.briarproject.api.sync.SyncSessionFactory;
import org.briarproject.api.system.Clock;
import org.briarproject.contact.ContactModule;
import org.briarproject.crypto.CryptoModule;
import org.briarproject.lifecycle.LifecycleModule;
import org.briarproject.privategroup.PrivateGroupModule;
import org.briarproject.properties.PropertiesModule;
import org.briarproject.sync.SyncModule;
import org.briarproject.transport.TransportModule;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.concurrent.TimeoutException;
import java.util.logging.Logger;

import javax.inject.Inject;

import static org.briarproject.TestPluginsModule.MAX_LATENCY;
import static org.briarproject.TestUtils.getRandomBytes;
import static org.briarproject.api.identity.Author.Status.VERIFIED;
import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
import static org.briarproject.api.sync.ValidationManager.State.INVALID;
import static org.briarproject.api.sync.ValidationManager.State.PENDING;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public class PrivateGroupManagerTest extends BriarIntegrationTest {

	private LifecycleManager lifecycleManager0, lifecycleManager1;
	private SyncSessionFactory sync0, sync1;
	private PrivateGroupManager groupManager0, groupManager1;
	private ContactManager contactManager0, contactManager1;
	private ContactId contactId0, contactId1;
	private IdentityManager identityManager0, identityManager1;
	private LocalAuthor author0, author1;
	private PrivateGroup privateGroup0;
	private GroupId groupId0;

	@Inject
	Clock clock;
	@Inject
	AuthorFactory authorFactory;
	@Inject
	ClientHelper clientHelper;
	@Inject
	CryptoComponent crypto;
	@Inject
	ContactGroupFactory contactGroupFactory;
	@Inject
	PrivateGroupFactory privateGroupFactory;
	@Inject
	GroupMessageFactory groupMessageFactory;
	@Inject
	GroupInvitationManager groupInvitationManager;

	// objects accessed from background threads need to be volatile
	private volatile Waiter validationWaiter;
	private volatile Waiter deliveryWaiter;

	private final File testDir = TestUtils.getTestDirectory();
	private final SecretKey master = TestUtils.getSecretKey();
	private final int TIMEOUT = 15000;
	private final String AUTHOR1 = "Author 1";
	private final String AUTHOR2 = "Author 2";

	private static final Logger LOG =
			Logger.getLogger(PrivateGroupManagerTest.class.getName());

	private PrivateGroupManagerTestComponent t0, t1;

	@Rule
	public ExpectedException thrown = ExpectedException.none();

	@Before
	public void setUp() throws Exception {
		PrivateGroupManagerTestComponent component =
				DaggerPrivateGroupManagerTestComponent.builder().build();
		component.inject(this);
		injectEagerSingletons(component);

		assertTrue(testDir.mkdirs());
		File t0Dir = new File(testDir, AUTHOR1);
		t0 = DaggerPrivateGroupManagerTestComponent.builder()
				.testDatabaseModule(new TestDatabaseModule(t0Dir)).build();
		injectEagerSingletons(t0);
		File t1Dir = new File(testDir, AUTHOR2);
		t1 = DaggerPrivateGroupManagerTestComponent.builder()
				.testDatabaseModule(new TestDatabaseModule(t1Dir)).build();
		injectEagerSingletons(t1);

		identityManager0 = t0.getIdentityManager();
		identityManager1 = t1.getIdentityManager();
		contactManager0 = t0.getContactManager();
		contactManager1 = t1.getContactManager();
		groupManager0 = t0.getPrivateGroupManager();
		groupManager1 = t1.getPrivateGroupManager();
		sync0 = t0.getSyncSessionFactory();
		sync1 = t1.getSyncSessionFactory();

		// initialize waiters fresh for each test
		validationWaiter = new Waiter();
		deliveryWaiter = new Waiter();

		startLifecycles();
	}

	@Test
	public void testSendingMessage() throws Exception {
		defaultInit();

		// create and add test message
		long time = clock.currentTimeMillis();
		String body = "This is a test message!";
		MessageId previousMsgId =
				groupManager0.getPreviousMsgId(groupId0);
		GroupMessage msg = groupMessageFactory
				.createGroupMessage(groupId0, time, null, author0, body,
						previousMsgId);
		groupManager0.addLocalMessage(msg);
		assertEquals(msg.getMessage().getId(),
				groupManager0.getPreviousMsgId(groupId0));

		// sync test message
		sync0To1();
		deliveryWaiter.await(TIMEOUT, 1);

		// assert that message arrived as expected
		Collection<GroupMessageHeader> headers =
				groupManager1.getHeaders(groupId0);
		assertEquals(3, headers.size());
		GroupMessageHeader header = null;
		for (GroupMessageHeader h : headers) {
			if (!(h instanceof JoinMessageHeader)) {
				header = h;
			}
		}
		assertTrue(header != null);
		assertFalse(header.isRead());
		assertEquals(author0, header.getAuthor());
		assertEquals(time, header.getTimestamp());
		assertEquals(VERIFIED, header.getAuthorStatus());
		assertEquals(body, groupManager1.getMessageBody(header.getId()));
		GroupCount count = groupManager1.getGroupCount(groupId0);
		assertEquals(2, count.getUnreadCount());
		assertEquals(time, count.getLatestMsgTime());
		assertEquals(3, count.getMsgCount());
	}

	@Test
	public void testMessageWithWrongPreviousMsgId() throws Exception {
		defaultInit();

		// create and add test message with no previousMsgId
		GroupMessage msg = groupMessageFactory
				.createGroupMessage(groupId0, clock.currentTimeMillis(), null,
						author0, "test", null);
		groupManager0.addLocalMessage(msg);

		// sync test message
		sync0To1();
		validationWaiter.await(TIMEOUT, 1);

		// assert that message did not arrive
		assertEquals(2, groupManager1.getHeaders(groupId0).size());

		// create and add test message with random previousMsgId
		MessageId previousMsgId = new MessageId(TestUtils.getRandomId());
		msg = groupMessageFactory
				.createGroupMessage(groupId0, clock.currentTimeMillis(), null,
						author0, "test", previousMsgId);
		groupManager0.addLocalMessage(msg);

		// sync test message
		sync0To1();
		validationWaiter.await(TIMEOUT, 1);

		// assert that message did not arrive
		assertEquals(2, groupManager1.getHeaders(groupId0).size());

		// create and add test message with wrong previousMsgId
		previousMsgId = groupManager1.getPreviousMsgId(groupId0);
		msg = groupMessageFactory
				.createGroupMessage(groupId0, clock.currentTimeMillis(), null,
						author0, "test", previousMsgId);
		groupManager0.addLocalMessage(msg);

		// sync test message
		sync0To1();
		validationWaiter.await(TIMEOUT, 1);

		// assert that message did not arrive
		assertEquals(2, groupManager1.getHeaders(groupId0).size());
	}

	@Test
	public void testMessageWithWrongParentMsgId() throws Exception {
		defaultInit();

		// create and add test message with random parentMsgId
		MessageId parentMsgId = new MessageId(TestUtils.getRandomId());
		MessageId previousMsgId = groupManager0.getPreviousMsgId(groupId0);
		GroupMessage msg = groupMessageFactory
				.createGroupMessage(groupId0, clock.currentTimeMillis(),
						parentMsgId, author0, "test", previousMsgId);
		groupManager0.addLocalMessage(msg);

		// sync test message
		sync0To1();
		validationWaiter.await(TIMEOUT, 1);

		// assert that message did not arrive
		assertEquals(2, groupManager1.getHeaders(groupId0).size());

		// create and add test message with wrong parentMsgId
		parentMsgId = previousMsgId;
		msg = groupMessageFactory
				.createGroupMessage(groupId0, clock.currentTimeMillis(),
						parentMsgId, author0, "test", previousMsgId);
		groupManager0.addLocalMessage(msg);

		// sync test message
		sync0To1();
		validationWaiter.await(TIMEOUT, 1);

		// assert that message did not arrive
		assertEquals(2, groupManager1.getHeaders(groupId0).size());
	}

	@Test
	public void testMessageWithWrongTimestamp() throws Exception {
		defaultInit();

		// create and add test message with wrong timestamp
		MessageId previousMsgId = groupManager0.getPreviousMsgId(groupId0);
		GroupMessage msg = groupMessageFactory
				.createGroupMessage(groupId0, 42, null, author0, "test",
						previousMsgId);
		groupManager0.addLocalMessage(msg);

		// sync test message
		sync0To1();
		validationWaiter.await(TIMEOUT, 1);

		// assert that message did not arrive
		assertEquals(2, groupManager1.getHeaders(groupId0).size());

		// create and add test message with good timestamp
		long time = clock.currentTimeMillis();
		msg = groupMessageFactory
				.createGroupMessage(groupId0, time, null, author0, "test",
						previousMsgId);
		groupManager0.addLocalMessage(msg);

		// sync test message
		sync0To1();
		deliveryWaiter.await(TIMEOUT, 1);
		assertEquals(3, groupManager1.getHeaders(groupId0).size());

		// create and add test message with same timestamp as previous message
		previousMsgId = msg.getMessage().getId();
		msg = groupMessageFactory
				.createGroupMessage(groupId0, time, previousMsgId, author0,
						"test2", previousMsgId);
		groupManager0.addLocalMessage(msg);

		// sync test message
		sync0To1();
		validationWaiter.await(TIMEOUT, 1);

		// assert that message did not arrive
		assertEquals(3, groupManager1.getHeaders(groupId0).size());
	}

	@Test
	public void testWrongJoinMessages1() throws Exception {
		addDefaultIdentities();
		addDefaultContacts();
		listenToEvents();

		// author0 joins privateGroup0 with wrong join message
		long joinTime = clock.currentTimeMillis();
		GroupMessage joinMsg0 = groupMessageFactory
				.createJoinMessage(privateGroup0.getId(), joinTime, author0,
						joinTime, getRandomBytes(12));
		groupManager0.addPrivateGroup(privateGroup0, joinMsg0);
		assertEquals(joinMsg0.getMessage().getId(),
				groupManager0.getPreviousMsgId(groupId0));

		// make group visible to 1
		Transaction txn0 = t0.getDatabaseComponent().startTransaction(false);
		t0.getDatabaseComponent()
				.setVisibleToContact(txn0, contactId1, privateGroup0.getId(),
						true);
		t0.getDatabaseComponent().commitTransaction(txn0);
		t0.getDatabaseComponent().endTransaction(txn0);

		// author1 joins privateGroup0 with wrong timestamp
		joinTime = clock.currentTimeMillis();
		long inviteTime = joinTime;
		Group invitationGroup = contactGroupFactory
				.createContactGroup(groupInvitationManager.getClientId(),
						author0.getId(), author1.getId());
		BdfList toSign = BdfList.of(0, inviteTime, invitationGroup.getId(),
				privateGroup0.getId());
		byte[] creatorSignature =
				clientHelper.sign(toSign, author0.getPrivateKey());
		GroupMessage joinMsg1 = groupMessageFactory
				.createJoinMessage(privateGroup0.getId(), joinTime, author1,
						inviteTime, creatorSignature);
		groupManager1.addPrivateGroup(privateGroup0, joinMsg1);
		assertEquals(joinMsg1.getMessage().getId(),
				groupManager1.getPreviousMsgId(groupId0));

		// make group visible to 0
		Transaction txn1 = t1.getDatabaseComponent().startTransaction(false);
		t1.getDatabaseComponent()
				.setVisibleToContact(txn1, contactId0, privateGroup0.getId(),
						true);
		t1.getDatabaseComponent().commitTransaction(txn1);
		t1.getDatabaseComponent().endTransaction(txn1);

		// sync join messages
		sync0To1();
		validationWaiter.await(TIMEOUT, 1);

		// assert that 0 never joined the group from 1's perspective
		assertEquals(1, groupManager1.getHeaders(groupId0).size());

		sync1To0();
		validationWaiter.await(TIMEOUT, 1);

		// assert that 1 never joined the group from 0's perspective
		assertEquals(1, groupManager0.getHeaders(groupId0).size());
	}

	@Test
	public void testWrongJoinMessages2() throws Exception {
		addDefaultIdentities();
		addDefaultContacts();
		listenToEvents();

		// author0 joins privateGroup0 with wrong member's join message
		long joinTime = clock.currentTimeMillis();
		long inviteTime = joinTime - 1;
		Group invitationGroup = contactGroupFactory
				.createContactGroup(groupInvitationManager.getClientId(),
						author0.getId(), author0.getId());
		BdfList toSign = BdfList.of(0, inviteTime, invitationGroup.getId(),
				privateGroup0.getId());
		byte[] creatorSignature =
				clientHelper.sign(toSign, author0.getPrivateKey());
		// join message should not include invite time and creator's signature
		GroupMessage joinMsg0 = groupMessageFactory
				.createJoinMessage(privateGroup0.getId(), joinTime, author0,
						inviteTime, creatorSignature);
		groupManager0.addPrivateGroup(privateGroup0, joinMsg0);
		assertEquals(joinMsg0.getMessage().getId(),
				groupManager0.getPreviousMsgId(groupId0));

		// make group visible to 1
		Transaction txn0 = t0.getDatabaseComponent().startTransaction(false);
		t0.getDatabaseComponent()
				.setVisibleToContact(txn0, contactId1, privateGroup0.getId(),
						true);
		t0.getDatabaseComponent().commitTransaction(txn0);
		t0.getDatabaseComponent().endTransaction(txn0);

		// author1 joins privateGroup0 with wrong signature in join message
		joinTime = clock.currentTimeMillis();
		inviteTime = joinTime - 1;
		invitationGroup = contactGroupFactory
				.createContactGroup(groupInvitationManager.getClientId(),
						author0.getId(), author1.getId());
		toSign = BdfList.of(0, inviteTime, invitationGroup.getId(),
				privateGroup0.getId());
		// signature uses joiner's key, not creator's key
		creatorSignature = clientHelper.sign(toSign, author1.getPrivateKey());
		GroupMessage joinMsg1 = groupMessageFactory
				.createJoinMessage(privateGroup0.getId(), joinTime, author1,
						inviteTime, creatorSignature);
		groupManager1.addPrivateGroup(privateGroup0, joinMsg1);
		assertEquals(joinMsg1.getMessage().getId(),
				groupManager1.getPreviousMsgId(groupId0));

		// make group visible to 0
		Transaction txn1 = t1.getDatabaseComponent().startTransaction(false);
		t1.getDatabaseComponent()
				.setVisibleToContact(txn1, contactId0, privateGroup0.getId(),
						true);
		t1.getDatabaseComponent().commitTransaction(txn1);
		t1.getDatabaseComponent().endTransaction(txn1);

		// sync join messages
		sync0To1();
		validationWaiter.await(TIMEOUT, 1);

		// assert that 0 never joined the group from 1's perspective
		assertEquals(1, groupManager1.getHeaders(groupId0).size());

		sync1To0();
		validationWaiter.await(TIMEOUT, 1);

		// assert that 1 never joined the group from 0's perspective
		assertEquals(1, groupManager0.getHeaders(groupId0).size());
	}

	@After
	public void tearDown() throws Exception {
		stopLifecycles();
		TestUtils.deleteTestDirectory(testDir);
	}

	private class Listener implements EventListener {
		@Override
		public void eventOccurred(Event e) {
			if (e instanceof MessageStateChangedEvent) {
				MessageStateChangedEvent event = (MessageStateChangedEvent) e;
				if (!event.isLocal()) {
					if (event.getState() == DELIVERED) {
						LOG.info("Delivered new message");
						deliveryWaiter.resume();
					} else if (event.getState() == INVALID ||
							event.getState() == PENDING) {
						LOG.info("Validated new " + event.getState().name() +
								" message");
						validationWaiter.resume();
					}
				}
			}
		}
	}

	private void defaultInit() throws Exception {
		addDefaultIdentities();
		addDefaultContacts();
		listenToEvents();
		addGroup();
	}

	private void addDefaultIdentities() throws DbException {
		KeyPair keyPair0 = crypto.generateSignatureKeyPair();
		byte[] publicKey0 = keyPair0.getPublic().getEncoded();
		byte[] privateKey0 = keyPair0.getPrivate().getEncoded();
		author0 = authorFactory
				.createLocalAuthor(AUTHOR1, publicKey0, privateKey0);
		identityManager0.registerLocalAuthor(author0);
		privateGroup0 =
				privateGroupFactory.createPrivateGroup("Testgroup", author0);
		groupId0 = privateGroup0.getId();

		KeyPair keyPair1 = crypto.generateSignatureKeyPair();
		byte[] publicKey1 = keyPair1.getPublic().getEncoded();
		byte[] privateKey1 = keyPair1.getPrivate().getEncoded();
		author1 = authorFactory
				.createLocalAuthor(AUTHOR2, publicKey1, privateKey1);
		identityManager1.registerLocalAuthor(author1);
	}

	private void addDefaultContacts() throws DbException {
		// sharer adds invitee as contact
		contactId1 = contactManager0.addContact(author1,
				author0.getId(), master, clock.currentTimeMillis(), true,
				true, true
		);
		// invitee adds sharer back
		contactId0 = contactManager1.addContact(author0,
				author1.getId(), master, clock.currentTimeMillis(), true,
				true, true
		);
	}

	private void listenToEvents() {
		Listener listener0 = new Listener();
		t0.getEventBus().addListener(listener0);
		Listener listener1 = new Listener();
		t1.getEventBus().addListener(listener1);
	}

	private void addGroup() throws Exception {
		// author0 joins privateGroup0
		long joinTime = clock.currentTimeMillis();
		GroupMessage joinMsg0 = groupMessageFactory
				.createJoinMessage(privateGroup0.getId(), joinTime, author0);
		groupManager0.addPrivateGroup(privateGroup0, joinMsg0);
		assertEquals(joinMsg0.getMessage().getId(),
				groupManager0.getPreviousMsgId(groupId0));

		// make group visible to 1
		Transaction txn0 = t0.getDatabaseComponent().startTransaction(false);
		t0.getDatabaseComponent()
				.setVisibleToContact(txn0, contactId1, privateGroup0.getId(),
						true);
		t0.getDatabaseComponent().commitTransaction(txn0);
		t0.getDatabaseComponent().endTransaction(txn0);

		// author1 joins privateGroup0
		joinTime = clock.currentTimeMillis();
		long inviteTime = joinTime - 1;
		Group invitationGroup = contactGroupFactory
				.createContactGroup(groupInvitationManager.getClientId(),
						author0.getId(), author1.getId());
		BdfList toSign = BdfList.of(0, inviteTime, invitationGroup.getId(),
				privateGroup0.getId());
		byte[] creatorSignature =
				clientHelper.sign(toSign, author0.getPrivateKey());
		GroupMessage joinMsg1 = groupMessageFactory
				.createJoinMessage(privateGroup0.getId(), joinTime, author1,
						inviteTime, creatorSignature);
		groupManager1.addPrivateGroup(privateGroup0, joinMsg1);
		assertEquals(joinMsg1.getMessage().getId(),
				groupManager1.getPreviousMsgId(groupId0));

		// make group visible to 0
		Transaction txn1 = t1.getDatabaseComponent().startTransaction(false);
		t1.getDatabaseComponent()
				.setVisibleToContact(txn1, contactId0, privateGroup0.getId(),
						true);
		t1.getDatabaseComponent().commitTransaction(txn1);
		t1.getDatabaseComponent().endTransaction(txn1);

		// sync join messages
		sync0To1();
		deliveryWaiter.await(TIMEOUT, 1);
		sync1To0();
		deliveryWaiter.await(TIMEOUT, 1);
	}

	private void sync0To1() throws IOException, TimeoutException {
		deliverMessage(sync0, contactId0, sync1, contactId1, "0 to 1");
	}

	private void sync1To0() throws IOException, TimeoutException {
		deliverMessage(sync1, contactId1, sync0, contactId0, "1 to 0");
	}

	private void deliverMessage(SyncSessionFactory fromSync, ContactId fromId,
			SyncSessionFactory toSync, ContactId toId, String debug)
			throws IOException, TimeoutException {

		if (debug != null) LOG.info("TEST: Sending message from " + debug);

		ByteArrayOutputStream out = new ByteArrayOutputStream();
		// Create an outgoing sync session
		SyncSession sessionFrom =
				fromSync.createSimplexOutgoingSession(toId, MAX_LATENCY, out);
		// Write whatever needs to be written
		sessionFrom.run();
		out.close();

		ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
		// Create an incoming sync session
		SyncSession sessionTo = toSync.createIncomingSession(fromId, in);
		// Read whatever needs to be read
		sessionTo.run();
		in.close();
	}

	private void startLifecycles() throws InterruptedException {
		// Start the lifecycle manager and wait for it to finish
		lifecycleManager0 = t0.getLifecycleManager();
		lifecycleManager1 = t1.getLifecycleManager();
		lifecycleManager0.startServices(AUTHOR1);
		lifecycleManager1.startServices(AUTHOR2);
		lifecycleManager0.waitForStartup();
		lifecycleManager1.waitForStartup();
	}

	private void stopLifecycles() throws InterruptedException {
		// Clean up
		lifecycleManager0.stopServices();
		lifecycleManager1.stopServices();
		lifecycleManager0.waitForShutdown();
		lifecycleManager1.waitForShutdown();
	}

	private void injectEagerSingletons(
			PrivateGroupManagerTestComponent component) {
		component.inject(new LifecycleModule.EagerSingletons());
		component.inject(new PrivateGroupModule.EagerSingletons());
		component.inject(new CryptoModule.EagerSingletons());
		component.inject(new ContactModule.EagerSingletons());
		component.inject(new TransportModule.EagerSingletons());
		component.inject(new SyncModule.EagerSingletons());
		component.inject(new PropertiesModule.EagerSingletons());
	}

}