diff --git a/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java b/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java index 7e27d74685bdf55eb9fb23d27981b029d97796ad..39b77cd2743f4b49d4f944365cd355e32867a831 100644 --- a/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java +++ b/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java @@ -180,7 +180,7 @@ class GroupMessageValidator extends BdfMessageValidator { // content (string) String content = body.getString(5); - checkLength(content, 0, MAX_GROUP_POST_BODY_LENGTH); + checkLength(content, 1, MAX_GROUP_POST_BODY_LENGTH); // signature (raw) // a signature with the member's private key over a list with 7 elements diff --git a/briar-tests/src/org/briarproject/ValidatorTestCase.java b/briar-tests/src/org/briarproject/ValidatorTestCase.java index 4e5a3b5e62392abe2cd2333bf73e164c5eb24e57..f7a7693fdae94e2d1e3adb038f4b6517de899f05 100644 --- a/briar-tests/src/org/briarproject/ValidatorTestCase.java +++ b/briar-tests/src/org/briarproject/ValidatorTestCase.java @@ -2,6 +2,7 @@ package org.briarproject; import org.briarproject.api.clients.ClientHelper; import org.briarproject.api.data.MetadataEncoder; +import org.briarproject.api.identity.AuthorFactory; import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.Group; import org.briarproject.api.sync.GroupId; @@ -16,6 +17,8 @@ public abstract class ValidatorTestCase extends BriarMockTestCase { protected final MetadataEncoder metadataEncoder = context.mock(MetadataEncoder.class); protected final Clock clock = context.mock(Clock.class); + protected final AuthorFactory authorFactory = + context.mock(AuthorFactory.class); protected final MessageId messageId = new MessageId(TestUtils.getRandomId()); diff --git a/briar-tests/src/org/briarproject/privategroup/GroupMessageValidatorTest.java b/briar-tests/src/org/briarproject/privategroup/GroupMessageValidatorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..d9da03860037c727b27cdc5d0d19cb21780a385d --- /dev/null +++ b/briar-tests/src/org/briarproject/privategroup/GroupMessageValidatorTest.java @@ -0,0 +1,534 @@ +package org.briarproject.privategroup; + +import org.briarproject.ValidatorTestCase; +import org.briarproject.api.FormatException; +import org.briarproject.api.clients.BdfMessageContext; +import org.briarproject.api.data.BdfDictionary; +import org.briarproject.api.data.BdfList; +import org.briarproject.api.identity.Author; +import org.briarproject.api.identity.AuthorId; +import org.briarproject.api.privategroup.PrivateGroup; +import org.briarproject.api.privategroup.PrivateGroupFactory; +import org.briarproject.api.privategroup.invitation.GroupInvitationFactory; +import org.briarproject.api.sync.InvalidMessageException; +import org.briarproject.api.sync.MessageId; +import org.jmock.Expectations; +import org.junit.Test; + +import java.security.GeneralSecurityException; + +import static org.briarproject.TestUtils.getRandomBytes; +import static org.briarproject.TestUtils.getRandomId; +import static org.briarproject.TestUtils.getRandomString; +import static org.briarproject.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; +import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; +import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH; +import static org.briarproject.api.privategroup.GroupMessageFactory.SIGNING_LABEL_JOIN; +import static org.briarproject.api.privategroup.GroupMessageFactory.SIGNING_LABEL_POST; +import static org.briarproject.api.privategroup.MessageType.JOIN; +import static org.briarproject.api.privategroup.MessageType.POST; +import static org.briarproject.api.privategroup.PrivateGroupConstants.GROUP_SALT_LENGTH; +import static org.briarproject.api.privategroup.PrivateGroupConstants.MAX_GROUP_POST_BODY_LENGTH; +import static org.briarproject.api.privategroup.invitation.GroupInvitationFactory.SIGNING_LABEL_INVITE; +import static org.briarproject.privategroup.GroupConstants.KEY_INITIAL_JOIN_MSG; +import static org.briarproject.privategroup.GroupConstants.KEY_MEMBER_ID; +import static org.briarproject.privategroup.GroupConstants.KEY_MEMBER_NAME; +import static org.briarproject.privategroup.GroupConstants.KEY_MEMBER_PUBLIC_KEY; +import static org.briarproject.privategroup.GroupConstants.KEY_PARENT_MSG_ID; +import static org.briarproject.privategroup.GroupConstants.KEY_PREVIOUS_MSG_ID; +import static org.briarproject.privategroup.GroupConstants.KEY_READ; +import static org.briarproject.privategroup.GroupConstants.KEY_TIMESTAMP; +import static org.briarproject.privategroup.GroupConstants.KEY_TYPE; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class GroupMessageValidatorTest extends ValidatorTestCase { + + private final PrivateGroupFactory privateGroupFactory = + context.mock(PrivateGroupFactory.class); + private final GroupInvitationFactory groupInvitationFactory = + context.mock(GroupInvitationFactory.class); + + + private final String creatorName = "Member Name"; + private final String memberName = "Member Name"; + private final byte[] creatorKey = getRandomBytes(MAX_PUBLIC_KEY_LENGTH); + private final byte[] memberKey = getRandomBytes(MAX_PUBLIC_KEY_LENGTH); + private final byte[] creatorSignature = + getRandomBytes(MAX_SIGNATURE_LENGTH); + private final byte[] signature = getRandomBytes(MAX_SIGNATURE_LENGTH); + private final Author member = + new Author(new AuthorId(getRandomId()), memberName, memberKey); + private final Author creator = + new Author(new AuthorId(getRandomId()), creatorName, creatorKey); + private final long inviteTimestamp = 42L; + private final PrivateGroup privateGroup = + new PrivateGroup(group, "Private Group Name", creator, + getRandomBytes(GROUP_SALT_LENGTH)); + private final BdfList token = BdfList.of("token"); + private MessageId parentId = new MessageId(getRandomId()); + private MessageId previousMsgId = new MessageId(getRandomId()); + private String postContent = "Post text"; + + private GroupMessageValidator validator = + new GroupMessageValidator(privateGroupFactory, clientHelper, + metadataEncoder, clock, authorFactory, + groupInvitationFactory); + + @Test(expected = FormatException.class) + public void testRejectTooShortMemberName() throws Exception { + BdfList list = BdfList.of(JOIN.getInt(), "", memberKey, null, + signature); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectTooLongMemberName() throws Exception { + BdfList list = BdfList.of(JOIN.getInt(), + getRandomString(MAX_AUTHOR_NAME_LENGTH + 1), memberKey, null, + signature); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectTooShortMemberKey() throws Exception { + BdfList list = BdfList.of(JOIN.getInt(), memberName, new byte[0], null, + signature); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectTooLongMemberKey() throws Exception { + BdfList list = BdfList.of(JOIN.getInt(), memberName, + getRandomBytes(MAX_PUBLIC_KEY_LENGTH + 1), null, + signature); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectNonRawMemberKey() throws Exception { + BdfList list = + BdfList.of(JOIN.getInt(), memberName, "non raw key", null, + signature); + validator.validateMessage(message, group, list); + } + + // JOIN message + + @Test(expected = FormatException.class) + public void testRejectsTooShortJoinMessage() throws Exception { + BdfList list = BdfList.of(JOIN.getInt(), creatorName, creatorKey, + null); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsTooLongJoinMessage() throws Exception { + expectCreateAuthor(creator); + BdfList list = BdfList.of(JOIN.getInt(), creatorName, creatorKey, + null, signature, ""); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsJoinMessageWithNonListInvitation() throws Exception { + expectCreateAuthor(creator); + expectParsePrivateGroup(); + BdfList list = BdfList.of(JOIN.getInt(), creatorName, creatorKey, + "not a list", signature); + validator.validateMessage(message, group, list); + } + + @Test + public void testAcceptCreatorJoinMessage() throws Exception { + final BdfList invite = null; + expectJoinMessage(creator, invite, true, true); + BdfList list = BdfList.of(JOIN.getInt(), creatorName, creatorKey, + invite, signature); + BdfMessageContext messageContext = + validator.validateMessage(message, group, list); + assertMessageContext(messageContext, creator); + assertTrue(messageContext.getDictionary() + .getBoolean(KEY_INITIAL_JOIN_MSG)); + } + + @Test(expected = InvalidMessageException.class) + public void testRejectsMemberJoinMessageWithoutInvitation() + throws Exception { + expectCreateAuthor(member); + expectParsePrivateGroup(); + BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, null, + signature); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsJoinMessageWithTooShortInvitation() throws Exception { + BdfList invite = BdfList.of(inviteTimestamp); + expectCreateAuthor(member); + expectParsePrivateGroup(); + BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite, + signature); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsJoinMessageWithTooLongInvitation() throws Exception { + BdfList invite = BdfList.of(inviteTimestamp, creatorSignature, ""); + expectCreateAuthor(member); + expectParsePrivateGroup(); + BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite, + signature); + validator.validateMessage(message, group, list); + } + + @Test(expected = InvalidMessageException.class) + public void testRejectsJoinMessageWithEqualInvitationTime() + throws Exception { + BdfList invite = BdfList.of(message.getTimestamp(), creatorSignature); + expectCreateAuthor(member); + expectParsePrivateGroup(); + BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite, + signature); + validator.validateMessage(message, group, list); + } + + @Test(expected = InvalidMessageException.class) + public void testRejectsJoinMessageWithLaterInvitationTime() + throws Exception { + BdfList invite = + BdfList.of(message.getTimestamp() + 1, creatorSignature); + expectCreateAuthor(member); + expectParsePrivateGroup(); + BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite, + signature); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsJoinMessageWithNonRawCreatorSignature() + throws Exception { + BdfList invite = BdfList.of(inviteTimestamp, "non-raw signature"); + expectCreateAuthor(member); + expectParsePrivateGroup(); + BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite, + signature); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsJoinMessageWithTooShortCreatorSignature() + throws Exception { + BdfList invite = BdfList.of(inviteTimestamp, new byte[0]); + expectCreateAuthor(member); + expectParsePrivateGroup(); + BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite, + signature); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsJoinMessageWithTooLongCreatorSignature() + throws Exception { + BdfList invite = BdfList.of(inviteTimestamp, + getRandomBytes(MAX_SIGNATURE_LENGTH + 1)); + expectCreateAuthor(member); + expectParsePrivateGroup(); + BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite, + signature); + validator.validateMessage(message, group, list); + } + + @Test(expected = InvalidMessageException.class) + public void testRejectsJoinMessageWithInvalidCreatorSignature() + throws Exception { + BdfList invite = BdfList.of(inviteTimestamp, creatorSignature); + expectJoinMessage(member, invite, false, true); + BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite, + signature); + validator.validateMessage(message, group, list); + } + + @Test(expected = InvalidMessageException.class) + public void testRejectsJoinMessageWithInvalidMemberSignature() + throws Exception { + BdfList invite = BdfList.of(inviteTimestamp, creatorSignature); + expectJoinMessage(member, invite, true, false); + BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite, + signature); + validator.validateMessage(message, group, list); + } + + @Test + public void testAcceptMemberJoinMessage() throws Exception { + BdfList invite = BdfList.of(inviteTimestamp, creatorSignature); + expectJoinMessage(member, invite, true, true); + BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite, + signature); + BdfMessageContext messageContext = + validator.validateMessage(message, group, list); + assertMessageContext(messageContext, member); + assertFalse(messageContext.getDictionary() + .getBoolean(KEY_INITIAL_JOIN_MSG)); + } + + private void expectCreateAuthor(final Author member) { + context.checking(new Expectations() {{ + oneOf(authorFactory) + .createAuthor(member.getName(), member.getPublicKey()); + will(returnValue(member)); + }}); + } + + private void expectParsePrivateGroup() throws Exception { + context.checking(new Expectations() {{ + oneOf(privateGroupFactory).parsePrivateGroup(group); + will(returnValue(privateGroup)); + }}); + } + + private void expectJoinMessage(final Author member, final BdfList invite, + final boolean creatorSigValid, final boolean memberSigValid) + throws Exception { + final BdfList signed = + BdfList.of(group.getId(), message.getTimestamp(), JOIN.getInt(), + member.getName(), member.getPublicKey(), invite); + expectCreateAuthor(member); + expectParsePrivateGroup(); + context.checking(new Expectations() {{ + if (invite != null) { + oneOf(groupInvitationFactory) + .createInviteToken(creator.getId(), member.getId(), + privateGroup.getId(), inviteTimestamp); + will(returnValue(token)); + oneOf(clientHelper) + .verifySignature(SIGNING_LABEL_INVITE, creatorSignature, + creatorKey, token); + if (!memberSigValid) + will(throwException(new GeneralSecurityException())); + } + if (memberSigValid) { + oneOf(clientHelper) + .verifySignature(SIGNING_LABEL_JOIN, signature, + member.getPublicKey(), signed); + if (!creatorSigValid) + will(throwException(new GeneralSecurityException())); + } + }}); + } + + // POST Message + + @Test(expected = FormatException.class) + public void testRejectsTooShortPost() throws Exception { + BdfList list = + BdfList.of(POST.getInt(), memberName, memberKey, parentId, + previousMsgId, postContent); + expectCreateAuthor(member); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsTooLongPost() throws Exception { + BdfList list = + BdfList.of(POST.getInt(), memberName, memberKey, parentId, + previousMsgId, postContent, signature, ""); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsPostWithNonRawParentId() throws Exception { + BdfList list = + BdfList.of(POST.getInt(), memberName, memberKey, "non-raw", + previousMsgId, postContent, signature); + expectCreateAuthor(member); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsPostWithTooShortParentId() throws Exception { + BdfList list = + BdfList.of(POST.getInt(), memberName, memberKey, + getRandomBytes(MessageId.LENGTH - 1), previousMsgId, + postContent, signature); + expectCreateAuthor(member); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsPostWithTooLongParentId() throws Exception { + BdfList list = + BdfList.of(POST.getInt(), memberName, memberKey, + getRandomBytes(MessageId.LENGTH + 1), previousMsgId, + postContent, signature); + expectCreateAuthor(member); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsPostWithNonRawPreviousMsgId() throws Exception { + BdfList list = + BdfList.of(POST.getInt(), memberName, memberKey, parentId, + "non-raw", postContent, signature); + expectCreateAuthor(member); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsPostWithTooShortPreviousMsgId() throws Exception { + BdfList list = + BdfList.of(POST.getInt(), memberName, memberKey, parentId, + getRandomBytes(MessageId.LENGTH - 1), + postContent, signature); + expectCreateAuthor(member); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsPostWithTooLongPreviousMsgId() throws Exception { + BdfList list = + BdfList.of(POST.getInt(), memberName, memberKey, parentId, + getRandomBytes(MessageId.LENGTH + 1), + postContent, signature); + expectCreateAuthor(member); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsPostWithEmptyContent() throws Exception { + postContent = ""; + BdfList list = + BdfList.of(POST.getInt(), memberName, memberKey, parentId, + previousMsgId, postContent, signature); + expectCreateAuthor(member); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsPostWithTooLongContent() throws Exception { + postContent = getRandomString(MAX_GROUP_POST_BODY_LENGTH + 1); + BdfList list = + BdfList.of(POST.getInt(), memberName, memberKey, parentId, + previousMsgId, postContent, signature); + expectCreateAuthor(member); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsPostWithNonStringContent() throws Exception { + BdfList list = + BdfList.of(POST.getInt(), memberName, memberKey, parentId, + previousMsgId, getRandomBytes(5), signature); + expectCreateAuthor(member); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsPostWithEmptySignature() throws Exception { + BdfList list = + BdfList.of(POST.getInt(), memberName, memberKey, parentId, + previousMsgId, postContent, new byte[0]); + expectCreateAuthor(member); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsPostWithTooLongSignature() throws Exception { + BdfList list = + BdfList.of(POST.getInt(), memberName, memberKey, parentId, + previousMsgId, postContent, + getRandomBytes(MAX_SIGNATURE_LENGTH + 1)); + expectCreateAuthor(member); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsPostWithNonRawSignature() throws Exception { + BdfList list = + BdfList.of(POST.getInt(), memberName, memberKey, parentId, + previousMsgId, postContent, "non-raw"); + expectCreateAuthor(member); + validator.validateMessage(message, group, list); + } + + @Test(expected = InvalidMessageException.class) + public void testRejectsPostWithInvalidSignature() throws Exception { + BdfList list = + BdfList.of(POST.getInt(), memberName, memberKey, parentId, + previousMsgId, postContent, signature); + expectPostMessage(member, false); + validator.validateMessage(message, group, list); + } + + @Test + public void testAcceptPost() throws Exception { + BdfList list = + BdfList.of(POST.getInt(), memberName, memberKey, parentId, + previousMsgId, postContent, signature); + expectPostMessage(member, true); + BdfMessageContext messageContext = + validator.validateMessage(message, group, list); + assertMessageContext(messageContext, member); + assertEquals(previousMsgId.getBytes(), + messageContext.getDictionary().getRaw(KEY_PREVIOUS_MSG_ID)); + assertEquals(parentId.getBytes(), + messageContext.getDictionary().getRaw(KEY_PARENT_MSG_ID)); + } + + @Test + public void testAcceptTopLevelPost() throws Exception { + parentId = null; + BdfList list = + BdfList.of(POST.getInt(), memberName, memberKey, parentId, + previousMsgId, postContent, signature); + expectPostMessage(member, true); + BdfMessageContext messageContext = + validator.validateMessage(message, group, list); + assertMessageContext(messageContext, member); + assertEquals(previousMsgId.getBytes(), + messageContext.getDictionary().getRaw(KEY_PREVIOUS_MSG_ID)); + assertFalse( + messageContext.getDictionary().containsKey(KEY_PARENT_MSG_ID)); + } + + private void expectPostMessage(final Author member, final boolean sigValid) + throws Exception { + final BdfList signed = + BdfList.of(group.getId(), message.getTimestamp(), POST.getInt(), + member.getName(), member.getPublicKey(), + parentId == null ? null : parentId.getBytes(), + previousMsgId == null ? null : previousMsgId.getBytes(), + postContent); + expectCreateAuthor(member); + context.checking(new Expectations() {{ + oneOf(clientHelper) + .verifySignature(SIGNING_LABEL_POST, signature, + member.getPublicKey(), signed); + if (!sigValid) will(throwException(new GeneralSecurityException())); + }}); + } + + private void assertMessageContext(BdfMessageContext c, Author member) + throws FormatException { + BdfDictionary d = c.getDictionary(); + assertTrue(message.getTimestamp() == d.getLong(KEY_TIMESTAMP)); + assertFalse(d.getBoolean(KEY_READ)); + assertEquals(member.getId().getBytes(), d.getRaw(KEY_MEMBER_ID)); + assertEquals(member.getName(), d.getString(KEY_MEMBER_NAME)); + assertEquals(member.getPublicKey(), d.getRaw(KEY_MEMBER_PUBLIC_KEY)); + + // assert message dependencies + if (d.getLong(KEY_TYPE) == POST.getInt()) { + assertTrue(c.getDependencies().contains(previousMsgId)); + if (parentId != null) { + assertTrue(c.getDependencies().contains(parentId)); + } else { + assertFalse(c.getDependencies().contains(parentId)); + } + } else { + assertEquals(JOIN.getInt(), d.getLong(KEY_TYPE).intValue()); + } + } + +} diff --git a/briar-tests/src/org/briarproject/privategroup/invitation/GroupInvitationValidatorTest.java b/briar-tests/src/org/briarproject/privategroup/invitation/GroupInvitationValidatorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..093de60f3300d223599b61255eb95a3d650c00e9 --- /dev/null +++ b/briar-tests/src/org/briarproject/privategroup/invitation/GroupInvitationValidatorTest.java @@ -0,0 +1,424 @@ +package org.briarproject.privategroup.invitation; + +import org.briarproject.ValidatorTestCase; +import org.briarproject.api.FormatException; +import org.briarproject.api.UniqueId; +import org.briarproject.api.clients.BdfMessageContext; +import org.briarproject.api.data.BdfDictionary; +import org.briarproject.api.data.BdfEntry; +import org.briarproject.api.data.BdfList; +import org.briarproject.api.identity.Author; +import org.briarproject.api.identity.AuthorId; +import org.briarproject.api.privategroup.PrivateGroup; +import org.briarproject.api.privategroup.PrivateGroupFactory; +import org.briarproject.api.sync.GroupId; +import org.briarproject.api.sync.MessageId; +import org.jmock.Expectations; +import org.junit.Test; + +import java.security.GeneralSecurityException; + +import static org.briarproject.TestUtils.getRandomBytes; +import static org.briarproject.TestUtils.getRandomId; +import static org.briarproject.TestUtils.getRandomString; +import static org.briarproject.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; +import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; +import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH; +import static org.briarproject.api.privategroup.PrivateGroupConstants.GROUP_SALT_LENGTH; +import static org.briarproject.api.privategroup.PrivateGroupConstants.MAX_GROUP_INVITATION_MSG_LENGTH; +import static org.briarproject.api.privategroup.PrivateGroupConstants.MAX_GROUP_NAME_LENGTH; +import static org.briarproject.api.privategroup.invitation.GroupInvitationFactory.SIGNING_LABEL_INVITE; +import static org.briarproject.privategroup.invitation.MessageType.ABORT; +import static org.briarproject.privategroup.invitation.MessageType.INVITE; +import static org.briarproject.privategroup.invitation.MessageType.JOIN; +import static org.briarproject.privategroup.invitation.MessageType.LEAVE; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class GroupInvitationValidatorTest extends ValidatorTestCase { + + private final PrivateGroupFactory privateGroupFactory = + context.mock(PrivateGroupFactory.class); + private final MessageEncoder messageEncoder = + context.mock(MessageEncoder.class); + + private final String groupName = "Group Name"; + private final String creatorName = "Creator Name"; + private final byte[] creatorKey = getRandomBytes(MAX_PUBLIC_KEY_LENGTH); + private final Author creator = + new Author(new AuthorId(getRandomId()), creatorName, creatorKey); + private final byte[] salt = getRandomBytes(GROUP_SALT_LENGTH); + private final PrivateGroup privateGroup = + new PrivateGroup(group, groupName, creator, salt); + private final String inviteText = "Invitation Text"; + private final byte[] signature = getRandomBytes(MAX_SIGNATURE_LENGTH); + private final BdfDictionary meta = + BdfDictionary.of(new BdfEntry("meta", "data")); + private final MessageId previousMessageId = new MessageId(getRandomId()); + + private GroupInvitationValidator validator = + new GroupInvitationValidator(clientHelper, metadataEncoder, + clock, authorFactory, privateGroupFactory, messageEncoder); + + @Test(expected = FormatException.class) + public void testRejectsTooShortInviteMessage() throws Exception { + BdfList list = BdfList.of(INVITE.getValue(), groupName, creatorName, + creatorKey, salt, inviteText); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsTooLongInviteMessage() throws Exception { + BdfList list = BdfList.of(INVITE.getValue(), groupName, creatorName, + creatorKey, salt, inviteText, signature, ""); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsInviteMessageWithTooLongGroupName() + throws Exception { + BdfList list = BdfList.of(INVITE.getValue(), + getRandomString(MAX_GROUP_NAME_LENGTH + 1), creatorName, + creatorKey, salt, inviteText, signature); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsInviteMessageWithEmptyGroupName() + throws Exception { + BdfList list = BdfList.of(INVITE.getValue(), "", creatorName, + creatorKey, salt, inviteText, signature); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsInviteMessageWithTooLongCreatorName() + throws Exception { + BdfList list = BdfList.of(INVITE.getValue(), groupName, + getRandomString(MAX_AUTHOR_NAME_LENGTH + 1), creatorKey, salt, + inviteText, signature); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsInviteMessageWithEmptyCreatorName() + throws Exception { + BdfList list = + BdfList.of(INVITE.getValue(), groupName, "", creatorKey, salt, + inviteText, signature); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsInviteMessageWithTooLongCreatorKey() + throws Exception { + BdfList list = BdfList.of(INVITE.getValue(), groupName, creatorName, + getRandomBytes(MAX_PUBLIC_KEY_LENGTH + 1), salt, inviteText, + signature); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsInviteMessageWithEmptyCreatorKey() + throws Exception { + BdfList list = BdfList.of(INVITE.getValue(), groupName, creatorName, + new byte[0], salt, inviteText, signature); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsInviteMessageWithTooLongGroupSalt() + throws Exception { + BdfList list = BdfList.of(INVITE.getValue(), groupName, creatorName, + creatorKey, getRandomBytes(GROUP_SALT_LENGTH + 1), inviteText, + signature); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsInviteMessageWithTooShortGroupSalt() + throws Exception { + BdfList list = BdfList.of(INVITE.getValue(), groupName, creatorName, + creatorKey, getRandomBytes(GROUP_SALT_LENGTH - 1), inviteText, + signature); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsInviteMessageWithTooLongMessage() + throws Exception { + BdfList list = BdfList.of(INVITE.getValue(), groupName, creatorName, + creatorKey, salt, + getRandomString(MAX_GROUP_INVITATION_MSG_LENGTH + 1), + signature); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsInviteMessageWithTooLongSignature() + throws Exception { + BdfList list = BdfList.of(INVITE.getValue(), groupName, creatorName, + creatorKey, salt, inviteText, + getRandomBytes(MAX_SIGNATURE_LENGTH + 1)); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsInviteMessageWithEmptySignature() + throws Exception { + BdfList list = BdfList.of(INVITE.getValue(), groupName, creatorName, + creatorKey, salt, inviteText, new byte[0]); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsInviteMessageWithNullSignature() + throws Exception { + BdfList list = BdfList.of(INVITE.getValue(), groupName, creatorName, + creatorKey, salt, inviteText, null); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsInviteMessageWithNonRawSignature() + throws Exception { + BdfList list = BdfList.of(INVITE.getValue(), groupName, creatorName, + creatorKey, salt, inviteText, "non raw signature"); + validator.validateMessage(message, group, list); + } + + @Test + public void testAcceptsInviteMessageWithNullMessage() + throws Exception { + expectInviteMessage(false); + BdfList list = BdfList.of(INVITE.getValue(), groupName, creatorName, + creatorKey, salt, null, signature); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsInviteMessageWithInvalidSignature() + throws Exception { + expectInviteMessage(true); + BdfList list = BdfList.of(INVITE.getValue(), groupName, creatorName, + creatorKey, salt, null, signature); + validator.validateMessage(message, group, list); + } + + @Test + public void testAcceptsProperInviteMessage() + throws Exception { + expectInviteMessage(false); + BdfList list = BdfList.of(INVITE.getValue(), groupName, creatorName, + creatorKey, salt, inviteText, signature); + BdfMessageContext messageContext = + validator.validateMessage(message, group, list); + assertTrue(messageContext.getDependencies().isEmpty()); + assertEquals(meta ,messageContext.getDictionary()); + } + + private void expectInviteMessage(final boolean exception) throws Exception { + final BdfList toSign = + BdfList.of(message.getTimestamp(), message.getGroupId(), + privateGroup.getId()); + context.checking(new Expectations() {{ + oneOf(authorFactory).createAuthor(creatorName, creatorKey); + will(returnValue(creator)); + oneOf(privateGroupFactory) + .createPrivateGroup(groupName, creator, salt); + will(returnValue(privateGroup)); + oneOf(clientHelper).verifySignature(SIGNING_LABEL_INVITE, signature, + creatorKey, toSign); + if (exception) will(throwException(new GeneralSecurityException())); + else { + oneOf(messageEncoder) + .encodeMetadata(INVITE, message.getGroupId(), + message.getTimestamp(), false, false, false, + false); + will(returnValue(meta)); + } + }}); + } + + // JOIN Message + + @Test(expected = FormatException.class) + public void testRejectsTooShortJoinMessage() throws Exception { + BdfList list = BdfList.of(JOIN.getValue(), privateGroup.getId()); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsTooLongJoinMessage() throws Exception { + BdfList list = BdfList.of(JOIN.getValue(), privateGroup.getId(), + previousMessageId, ""); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsJoinMessageWithTooLongGroupId() throws Exception { + BdfList list = + BdfList.of(JOIN.getValue(), getRandomBytes(GroupId.LENGTH + 1), + previousMessageId); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsJoinMessageWithTooShortGroupId() throws Exception { + BdfList list = + BdfList.of(JOIN.getValue(), getRandomBytes(GroupId.LENGTH - 1), + previousMessageId); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsJoinMessageWithTooLongPreviousMessageId() + throws Exception { + BdfList list = BdfList.of(JOIN.getValue(), privateGroup.getId(), + getRandomBytes(UniqueId.LENGTH + 1)); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsJoinMessageWithTooShortPreviousMessageId() + throws Exception { + BdfList list = BdfList.of(JOIN.getValue(), privateGroup.getId(), + getRandomBytes(UniqueId.LENGTH - 1)); + validator.validateMessage(message, group, list); + } + + @Test + public void testAcceptsProperJoinMessage() + throws Exception { + context.checking(new Expectations() {{ + oneOf(messageEncoder) + .encodeMetadata(JOIN, message.getGroupId(), + message.getTimestamp(), false, false, false, + false); + will(returnValue(meta)); + }}); + BdfList list = BdfList.of(JOIN.getValue(), privateGroup.getId(), + previousMessageId); + BdfMessageContext messageContext = + validator.validateMessage(message, group, list); + assertEquals(1, messageContext.getDependencies().size()); + assertEquals(previousMessageId, + messageContext.getDependencies().iterator().next()); + assertEquals(meta ,messageContext.getDictionary()); + } + + // LEAVE message + + @Test(expected = FormatException.class) + public void testRejectsTooShortLeaveMessage() throws Exception { + BdfList list = BdfList.of(LEAVE.getValue(), privateGroup.getId()); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsTooLongLeaveMessage() throws Exception { + BdfList list = BdfList.of(LEAVE.getValue(), privateGroup.getId(), + previousMessageId, ""); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsLeaveMessageWithTooLongGroupId() throws Exception { + BdfList list = + BdfList.of(LEAVE.getValue(), getRandomBytes(GroupId.LENGTH + 1), + previousMessageId); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsLeaveMessageWithTooShortGroupId() throws Exception { + BdfList list = + BdfList.of(LEAVE.getValue(), getRandomBytes(GroupId.LENGTH - 1), + previousMessageId); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsLeaveMessageWithTooLongPreviousMessageId() + throws Exception { + BdfList list = BdfList.of(LEAVE.getValue(), privateGroup.getId(), + getRandomBytes(UniqueId.LENGTH + 1)); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsLeaveMessageWithTooShortPreviousMessageId() + throws Exception { + BdfList list = BdfList.of(LEAVE.getValue(), privateGroup.getId(), + getRandomBytes(UniqueId.LENGTH - 1)); + validator.validateMessage(message, group, list); + } + + @Test + public void testAcceptsProperLeaveMessage() + throws Exception { + context.checking(new Expectations() {{ + oneOf(messageEncoder).encodeMetadata(LEAVE, message.getGroupId(), + message.getTimestamp(), false, false, false, false); + will(returnValue(meta)); + }}); + BdfList list = BdfList.of(LEAVE.getValue(), privateGroup.getId(), + previousMessageId); + BdfMessageContext messageContext = + validator.validateMessage(message, group, list); + assertEquals(1, messageContext.getDependencies().size()); + assertEquals(previousMessageId, + messageContext.getDependencies().iterator().next()); + assertEquals(meta ,messageContext.getDictionary()); + } + + // ABORT message + + @Test(expected = FormatException.class) + public void testRejectsTooShortAbortMessage() throws Exception { + BdfList list = BdfList.of(ABORT.getValue()); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsTooLongAbortMessage() throws Exception { + BdfList list = BdfList.of(ABORT.getValue(), privateGroup.getId(), ""); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsAbortMessageWithTooLongGroupId() throws Exception { + BdfList list = BdfList.of(ABORT.getValue(), + getRandomBytes(GroupId.LENGTH + 1)); + validator.validateMessage(message, group, list); + } + + @Test(expected = FormatException.class) + public void testRejectsAbortMessageWithTooShortGroupId() throws Exception { + BdfList list = BdfList.of(ABORT.getValue(), + getRandomBytes(GroupId.LENGTH - 1)); + validator.validateMessage(message, group, list); + } + + @Test + public void testAcceptsProperAbortMessage() + throws Exception { + context.checking(new Expectations() {{ + oneOf(messageEncoder).encodeMetadata(ABORT, message.getGroupId(), + message.getTimestamp(), false, false, false, false); + will(returnValue(meta)); + }}); + BdfList list = BdfList.of(ABORT.getValue(), privateGroup.getId()); + BdfMessageContext messageContext = + validator.validateMessage(message, group, list); + assertEquals(0, messageContext.getDependencies().size()); + assertEquals(meta ,messageContext.getDictionary()); + } + + @Test(expected = FormatException.class) + public void testRejectsMessageWithUnknownType() throws Exception { + BdfList list = BdfList.of(ABORT.getValue() + 1); + validator.validateMessage(message, group, list); + } + +}