From 66c7f8bcb299f10035722c34c1b82170a768ccf4 Mon Sep 17 00:00:00 2001 From: Alan Evans Date: Fri, 27 Mar 2020 11:28:48 -0300 Subject: [PATCH] GroupId for GV2. --- .../securesms/database/GroupDatabase.java | 3 +- .../securesms/database/RecipientDatabase.java | 6 +- .../securesms/groups/GroupId.java | 158 +++++++++++++-- .../securesms/groups/GroupManager.java | 2 +- .../securesms/groups/V1GroupManager.java | 10 +- .../securesms/jobs/LeaveGroupJob.java | 6 +- .../securesms/recipients/LiveRecipient.java | 2 +- .../securesms/recipients/Recipient.java | 6 +- .../securesms/groups/GroupIdTest.java | 188 +++++++++++++++--- .../securesms/groups/ZkGroupLibraryUtil.java | 39 ++++ 10 files changed, 358 insertions(+), 62 deletions(-) create mode 100644 app/src/test/java/org/thoughtcrime/securesms/groups/ZkGroupLibraryUtil.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index 0f4dbdb54..b997ccfbd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -26,7 +26,6 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPoin import java.io.Closeable; import java.security.SecureRandom; -import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -233,7 +232,7 @@ public class GroupDatabase extends Database { contentValues.put(AVATAR_RELAY, relay); contentValues.put(TIMESTAMP, System.currentTimeMillis()); contentValues.put(ACTIVE, 1); - contentValues.put(MMS, groupId.isMmsGroup()); + contentValues.put(MMS, groupId.isMms()); databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index ce8031b56..2848c72e6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -356,7 +356,7 @@ public class RecipientDatabase extends Database { if (result.neededInsert) { ContentValues values = new ContentValues(); - if (groupId.isMmsGroup()) { + if (groupId.isMms()) { values.put(GROUP_TYPE, GroupType.MMS.getId()); } else { values.put(GROUP_TYPE, GroupType.SIGNAL_V1.getId()); @@ -1406,9 +1406,9 @@ public class RecipientDatabase extends Database { db.update(TABLE_NAME, setBlocked, UUID + " = ?", new String[] { uuid }); } - List groupIdStrings = Stream.of(groupIds).map(GroupId::v1).toList(); + List groupIdStrings = Stream.of(groupIds).map(GroupId::v1).toList(); - for (GroupId groupId : groupIdStrings) { + for (GroupId.V1 groupId : groupIdStrings) { db.update(TABLE_NAME, setBlocked, GROUP_ID + " = ?", new String[] { groupId.toString() }); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupId.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupId.java index 769b4bf38..bba076950 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupId.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupId.java @@ -3,27 +3,52 @@ package org.thoughtcrime.securesms.groups; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.signal.zkgroup.groups.GroupIdentifier; +import org.signal.zkgroup.groups.GroupMasterKey; +import org.signal.zkgroup.groups.GroupSecretParams; import org.thoughtcrime.securesms.util.Hex; import java.io.IOException; -public final class GroupId { +public abstract class GroupId { private static final String ENCODED_SIGNAL_GROUP_PREFIX = "__textsecure_group__!"; private static final String ENCODED_MMS_GROUP_PREFIX = "__signal_mms_group__!"; + private static final int V2_BYTE_LENGTH = GroupIdentifier.SIZE; + private static final int V2_ENCODED_LENGTH = ENCODED_SIGNAL_GROUP_PREFIX.length() + V2_BYTE_LENGTH * 2; private final String encodedId; - private GroupId(@NonNull String encodedId) { - this.encodedId = encodedId; + private GroupId(@NonNull String prefix, @NonNull byte[] bytes) { + this.encodedId = prefix + Hex.toStringCondensed(bytes); } - public static @NonNull GroupId v1(byte[] gv1GroupIdBytes) { - return new GroupId(ENCODED_SIGNAL_GROUP_PREFIX + Hex.toStringCondensed(gv1GroupIdBytes)); + public static @NonNull GroupId.Mms mms(byte[] mmsGroupIdBytes) { + return new GroupId.Mms(mmsGroupIdBytes); } - public static @NonNull GroupId mms(byte[] mmsGroupIdBytes) { - return new GroupId(ENCODED_MMS_GROUP_PREFIX + Hex.toStringCondensed(mmsGroupIdBytes)); + public static @NonNull GroupId.V1 v1(byte[] gv1GroupIdBytes) { + if (gv1GroupIdBytes.length == V2_BYTE_LENGTH) { + throw new AssertionError(); + } + return new GroupId.V1(gv1GroupIdBytes); + } + + public static GroupId.V2 v2(@NonNull byte[] bytes) { + if (bytes.length != V2_BYTE_LENGTH) { + throw new AssertionError(); + } + return new GroupId.V2(bytes); + } + + public static GroupId.V2 v2(@NonNull GroupIdentifier groupIdentifier) { + return v2(groupIdentifier.serialize()); + } + + public static GroupId.V2 v2(@NonNull GroupMasterKey masterKey) { + return v2(GroupSecretParams.deriveFromMasterKey(masterKey) + .getPublicParams() + .getGroupIdentifier()); } public static @NonNull GroupId parse(@NonNull String encodedGroupId) { @@ -33,7 +58,11 @@ public final class GroupId { } byte[] bytes = extractDecodedId(encodedGroupId); - return isMmsGroup(encodedGroupId) ? mms(bytes) : v1(bytes); + + if (encodedGroupId.startsWith(ENCODED_MMS_GROUP_PREFIX)) return mms(bytes); + else if (encodedGroupId.length() == V2_ENCODED_LENGTH) return v2(bytes); + else return v1(bytes); + } catch (IOException e) { throw new AssertionError(e); } @@ -55,10 +84,6 @@ public final class GroupId { return Hex.fromStringCondensed(encodedGroupId.split("!", 2)[1]); } - private static boolean isMmsGroup(@NonNull String groupId) { - return groupId.startsWith(ENCODED_MMS_GROUP_PREFIX); - } - public byte[] getDecodedId() { try { return extractDecodedId(encodedId); @@ -67,10 +92,6 @@ public final class GroupId { } } - public boolean isMmsGroup() { - return isMmsGroup(encodedId); - } - @Override public boolean equals(@Nullable Object obj) { if (obj instanceof GroupId) { @@ -90,4 +111,109 @@ public final class GroupId { public String toString() { return encodedId; } + + public abstract boolean isMms(); + + public abstract boolean isV1(); + + public abstract boolean isV2(); + + public abstract boolean isPush(); + + public GroupId.Mms requireMms() { + if (this instanceof GroupId.Mms) return (GroupId.Mms) this; + throw new AssertionError(); + } + + public GroupId.V1 requireV1() { + if (this instanceof GroupId.V1) return (GroupId.V1) this; + throw new AssertionError(); + } + + public GroupId.V2 requireV2() { + if (this instanceof GroupId.V2) return (GroupId.V2) this; + throw new AssertionError(); + } + + public GroupId.Push requirePush() { + if (this instanceof GroupId.Push) return (GroupId.Push) this; + throw new AssertionError(); + } + + public static final class Mms extends GroupId { + + private Mms(@NonNull byte[] bytes) { + super(ENCODED_MMS_GROUP_PREFIX, bytes); + } + + @Override + public boolean isMms() { + return true; + } + + @Override + public boolean isV1() { + return false; + } + + @Override + public boolean isV2() { + return false; + } + + @Override + public boolean isPush() { + return false; + } + } + + public static abstract class Push extends GroupId { + private Push(@NonNull byte[] bytes) { + super(ENCODED_SIGNAL_GROUP_PREFIX, bytes); + } + + @Override + public boolean isMms() { + return false; + } + + @Override + public boolean isPush() { + return true; + } + } + + public static final class V1 extends GroupId.Push { + + private V1(@NonNull byte[] bytes) { + super(bytes); + } + + @Override + public boolean isV1() { + return true; + } + + @Override + public boolean isV2() { + return false; + } + } + + public static final class V2 extends GroupId.Push { + + private V2(@NonNull byte[] bytes) { + super(bytes); + } + + @Override + public boolean isV1() { + return false; + } + + @Override + public boolean isV2() { + return true; + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java index 2293513a1..28c8e5014 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java @@ -53,7 +53,7 @@ public final class GroupManager { public static boolean leaveGroup(@NonNull Context context, @NonNull Recipient groupRecipient) { GroupId groupId = groupRecipient.requireGroupId(); - return V1GroupManager.leaveGroup(context, groupId, groupRecipient); + return V1GroupManager.leaveGroup(context, groupId.requireV1(), groupRecipient); } public static class GroupActionResult { diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/V1GroupManager.java b/app/src/main/java/org/thoughtcrime/securesms/groups/V1GroupManager.java index 6abee264f..f1b5bcb6c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/V1GroupManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/V1GroupManager.java @@ -68,7 +68,7 @@ final class V1GroupManager { } groupDatabase.onAvatarUpdated(groupId, avatarBytes != null); DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient.getId(), true); - return sendGroupUpdate(context, groupId, memberIds, name, avatarBytes); + return sendGroupUpdate(context, groupId.requireV1(), memberIds, name, avatarBytes); } else { long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION); return new GroupActionResult(groupRecipient, threadId); @@ -91,13 +91,13 @@ final class V1GroupManager { groupDatabase.updateTitle(groupId, name); groupDatabase.onAvatarUpdated(groupId, avatarBytes != null); - if (!groupId.isMmsGroup()) { + if (groupId.isPush()) { try { AvatarHelper.setAvatar(context, groupRecipientId, avatarBytes != null ? new ByteArrayInputStream(avatarBytes) : null); } catch (IOException e) { Log.w(TAG, "Failed to save avatar!", e); } - return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes); + return sendGroupUpdate(context, groupId.requireV1(), memberAddresses, name, avatarBytes); } else { Recipient groupRecipient = Recipient.resolved(groupRecipientId); long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); @@ -106,7 +106,7 @@ final class V1GroupManager { } private static GroupActionResult sendGroupUpdate(@NonNull Context context, - @NonNull GroupId groupId, + @NonNull GroupId.V1 groupId, @NonNull Set members, @Nullable String groupName, @Nullable byte[] avatar) @@ -143,7 +143,7 @@ final class V1GroupManager { } @WorkerThread - static boolean leaveGroup(@NonNull Context context, @NonNull GroupId groupId, @NonNull Recipient groupRecipient) { + static boolean leaveGroup(@NonNull Context context, @NonNull GroupId.V1 groupId, @NonNull Recipient groupRecipient) { long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); Optional leaveMessage = GroupUtil.createGroupLeaveMessage(context, groupRecipient); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupJob.java index be14b8086..4f91c71b1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupJob.java @@ -51,7 +51,7 @@ public class LeaveGroupJob extends BaseJob { private static final String KEY_MEMBERS = "members"; private static final String KEY_RECIPIENTS = "recipients"; - private final GroupId groupId; + private final GroupId.Push groupId; private final String name; private final List members; private final List recipients; @@ -60,7 +60,7 @@ public class LeaveGroupJob extends BaseJob { List members = Stream.of(group.resolve().getParticipants()).map(Recipient::getId).toList(); members.remove(Recipient.self().getId()); - return new LeaveGroupJob(group.getGroupId().get(), + return new LeaveGroupJob(group.getGroupId().get().requirePush(), group.resolve().getDisplayName(ApplicationDependencies.getApplication()), members, members, @@ -72,7 +72,7 @@ public class LeaveGroupJob extends BaseJob { .build()); } - private LeaveGroupJob(@NonNull GroupId groupId, + private LeaveGroupJob(@NonNull GroupId.Push groupId, @NonNull String name, @NonNull List members, @NonNull List recipients, diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/LiveRecipient.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/LiveRecipient.java index 2c8775a1e..eb91a9701 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/LiveRecipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/LiveRecipient.java @@ -197,7 +197,7 @@ public final class LiveRecipient { List members = Stream.of(groupRecord.get().getMembers()).filterNot(RecipientId::isUnknown).map(this::fetchRecipientFromDisk).toList(); Optional avatarId = Optional.absent(); - if (settings.getGroupId() != null && !settings.getGroupId().isMmsGroup() && title == null) { + if (settings.getGroupId() != null && settings.getGroupId().isPush() && title == null) { title = unnamedGroupName; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java index 6d92dfdcc..6350dcace 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java @@ -381,7 +381,7 @@ public class Recipient { } public @Nullable String getName(@NonNull Context context) { - if (this.name == null && groupId != null && groupId.isMmsGroup()) { + if (this.name == null && groupId != null && groupId.isMms()) { List names = new LinkedList<>(); for (Recipient recipient : participants) { @@ -567,12 +567,12 @@ public class Recipient { public boolean isMmsGroup() { GroupId groupId = resolve().groupId; - return groupId != null && groupId.isMmsGroup(); + return groupId != null && groupId.isMms(); } public boolean isPushGroup() { GroupId groupId = resolve().groupId; - return groupId != null && !groupId.isMmsGroup(); + return groupId != null && groupId.isPush(); } public @NonNull List getParticipants() { diff --git a/app/src/test/java/org/thoughtcrime/securesms/groups/GroupIdTest.java b/app/src/test/java/org/thoughtcrime/securesms/groups/GroupIdTest.java index 2b6bcc0ca..3d83c0abe 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/groups/GroupIdTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/groups/GroupIdTest.java @@ -1,6 +1,12 @@ package org.thoughtcrime.securesms.groups; import org.junit.Test; +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.groups.GroupIdentifier; +import org.signal.zkgroup.groups.GroupMasterKey; +import org.thoughtcrime.securesms.util.Hex; + +import java.io.IOException; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -8,42 +14,89 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.thoughtcrime.securesms.groups.ZkGroupLibraryUtil.assumeZkGroupSupportedOnOS; public final class GroupIdTest { @Test public void can_create_for_gv1() { - GroupId groupId = GroupId.v1(new byte[]{ 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15 }); + GroupId.V1 groupId = GroupId.v1(new byte[]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }); - assertEquals("__textsecure_group__!0001020305060708090b0c0d0e0f", groupId.toString()); - assertFalse(groupId.isMmsGroup()); + assertEquals("__textsecure_group__!000102030405060708090a0b0c0d0e0f", groupId.toString()); + assertFalse(groupId.isMms()); } @Test public void can_parse_gv1() { - GroupId groupId = GroupId.parse("__textsecure_group__!0001020305060708090b0c0d0e0f"); + GroupId groupId = GroupId.parse("__textsecure_group__!000102030405060708090a0b0c0d0e0f"); - assertEquals("__textsecure_group__!0001020305060708090b0c0d0e0f", groupId.toString()); - assertArrayEquals(new byte[]{ 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15 }, groupId.getDecodedId()); - assertFalse(groupId.isMmsGroup()); + assertEquals("__textsecure_group__!000102030405060708090a0b0c0d0e0f", groupId.toString()); + assertArrayEquals(new byte[]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, groupId.getDecodedId()); + assertFalse(groupId.isMms()); + assertTrue(groupId.isV1()); + assertFalse(groupId.isV2()); + assertTrue(groupId.isPush()); + } + + @Test + public void can_create_for_gv2_from_GroupIdentifier() throws IOException, InvalidInputException { + GroupId.V2 groupId = GroupId.v2(new GroupIdentifier(Hex.fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))); + + assertEquals("__textsecure_group__!0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", groupId.toString()); + assertFalse(groupId.isMms()); + assertFalse(groupId.isV1()); + assertTrue(groupId.isV2()); + assertTrue(groupId.isPush()); + } + + @Test + public void can_create_for_gv2_from_GroupMasterKey() throws IOException, InvalidInputException { + assumeZkGroupSupportedOnOS(); + + GroupId.V2 groupId = GroupId.v2(new GroupMasterKey(Hex.fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))); + + assertEquals("__textsecure_group__!9f475f59b2518bff6df22e820803f0e3585bd99e686fa7e7fbfc2f92fd5d953e", groupId.toString()); + assertFalse(groupId.isMms()); + assertFalse(groupId.isV1()); + assertTrue(groupId.isV2()); + assertTrue(groupId.isPush()); + } + + @Test + public void can_parse_gv2() throws IOException { + GroupId groupId = GroupId.parse("__textsecure_group__!9f475f59b2518bff6df22e820803f0e3585bd99e686fa7e7fbfc2f92fd5d953e"); + + assertEquals("__textsecure_group__!9f475f59b2518bff6df22e820803f0e3585bd99e686fa7e7fbfc2f92fd5d953e", groupId.toString()); + assertArrayEquals(Hex.fromStringCondensed("9f475f59b2518bff6df22e820803f0e3585bd99e686fa7e7fbfc2f92fd5d953e"), groupId.getDecodedId()); + assertFalse(groupId.isMms()); + assertFalse(groupId.isV1()); + assertTrue(groupId.isV2()); + assertTrue(groupId.isPush()); } @Test public void can_create_for_mms() { - GroupId groupId = GroupId.mms(new byte[]{ 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15 }); + GroupId.Mms groupId = GroupId.mms(new byte[]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }); - assertEquals("__signal_mms_group__!0001020305060708090b0c0d0e0f", groupId.toString()); - assertTrue(groupId.isMmsGroup()); + assertEquals("__signal_mms_group__!000102030405060708090a0b0c0d0e0f", groupId.toString()); + assertTrue(groupId.isMms()); + assertFalse(groupId.isV1()); + assertFalse(groupId.isV2()); + assertFalse(groupId.isPush()); } @Test public void can_parse_mms() { - GroupId groupId = GroupId.parse("__signal_mms_group__!0001020305060708090b0c0d0e0f"); + GroupId groupId = GroupId.parse("__signal_mms_group__!000102030405060708090a0b0c0d0e0f"); - assertEquals("__signal_mms_group__!0001020305060708090b0c0d0e0f", groupId.toString()); - assertArrayEquals(new byte[]{ 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15 }, groupId.getDecodedId()); - assertTrue(groupId.isMmsGroup()); + assertEquals("__signal_mms_group__!000102030405060708090a0b0c0d0e0f", groupId.toString()); + assertArrayEquals(new byte[]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, groupId.getDecodedId()); + assertTrue(groupId.isMms()); + assertFalse(groupId.isV1()); + assertFalse(groupId.isV2()); + assertFalse(groupId.isPush()); } @SuppressWarnings("ConstantConditions") @@ -56,16 +109,19 @@ public final class GroupIdTest { @Test public void can_parse_gv1_with_parseNullable() { - GroupId groupId = GroupId.parseNullable("__textsecure_group__!0001020305060708090b0c0d0e0f"); + GroupId groupId = GroupId.parseNullable("__textsecure_group__!000102030405060708090a0b0c0d0e0f"); - assertEquals("__textsecure_group__!0001020305060708090b0c0d0e0f", groupId.toString()); - assertArrayEquals(new byte[]{ 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15 }, groupId.getDecodedId()); - assertFalse(groupId.isMmsGroup()); + assertEquals("__textsecure_group__!000102030405060708090a0b0c0d0e0f", groupId.toString()); + assertArrayEquals(new byte[]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, groupId.getDecodedId()); + assertFalse(groupId.isMms()); + assertTrue(groupId.isV1()); + assertFalse(groupId.isV2()); + assertTrue(groupId.isPush()); } @Test(expected = AssertionError.class) public void bad_encoding__bad_prefix__parseNullable() { - GroupId.parseNullable("__BAD_PREFIX__!0001020305060708090b0c0d0e0f"); + GroupId.parseNullable("__BAD_PREFIX__!000102030405060708090a0b0c0d0e0f"); } @Test(expected = AssertionError.class) @@ -80,7 +136,7 @@ public final class GroupIdTest { @Test(expected = AssertionError.class) public void bad_encoding__bad_prefix__parse() { - GroupId.parse("__BAD_PREFIX__!0001020305060708090b0c0d0e0f"); + GroupId.parse("__BAD_PREFIX__!000102030405060708090a0b0c0d0e0f"); } @Test(expected = AssertionError.class) @@ -90,7 +146,7 @@ public final class GroupIdTest { @Test public void get_bytes() { - byte[] bytes = { 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15 }; + byte[] bytes = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }; GroupId groupId = GroupId.v1(bytes); assertArrayEquals(bytes, groupId.getDecodedId()); @@ -98,8 +154,8 @@ public final class GroupIdTest { @Test public void equality() { - GroupId groupId1 = GroupId.v1(new byte[]{ 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15 }); - GroupId groupId2 = GroupId.v1(new byte[]{ 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15 }); + GroupId groupId1 = GroupId.v1(new byte[]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }); + GroupId groupId2 = GroupId.v1(new byte[]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }); assertNotSame(groupId1, groupId2); assertEquals(groupId1, groupId2); @@ -108,8 +164,8 @@ public final class GroupIdTest { @Test public void inequality_by_bytes() { - GroupId groupId1 = GroupId.v1(new byte[]{ 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15 }); - GroupId groupId2 = GroupId.v1(new byte[]{ 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 16 }); + GroupId groupId1 = GroupId.v1(new byte[]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }); + GroupId groupId2 = GroupId.v1(new byte[]{ 0, 3, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }); assertNotSame(groupId1, groupId2); assertNotEquals(groupId1, groupId2); @@ -118,8 +174,8 @@ public final class GroupIdTest { @Test public void inequality_of_sms_and_mms() { - GroupId groupId1 = GroupId.v1(new byte[]{ 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15 }); - GroupId groupId2 = GroupId.mms(new byte[]{ 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15 }); + GroupId groupId1 = GroupId.v1(new byte[]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }); + GroupId groupId2 = GroupId.mms(new byte[]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }); assertNotSame(groupId1, groupId2); assertNotEquals(groupId1, groupId2); @@ -128,8 +184,84 @@ public final class GroupIdTest { @Test public void inequality_with_null() { - GroupId groupId = GroupId.v1(new byte[]{ 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15 }); + GroupId groupId = GroupId.v1(new byte[]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }); assertNotEquals(groupId, null); } + + @Test + public void require_mms() { + GroupId groupId = GroupId.parse("__signal_mms_group__!000102030405060708090a0b0c0d0e0f"); + + GroupId.Mms mms = groupId.requireMms(); + + assertSame(groupId, mms); + } + + @Test + public void require_v1_and_push() { + GroupId groupId = GroupId.parse("__textsecure_group__!000102030405060708090a0b0c0d0e0f"); + + GroupId.V1 v1 = groupId.requireV1(); + GroupId.Push push = groupId.requirePush(); + + assertSame(groupId, v1); + assertSame(groupId, push); + } + + @Test + public void require_v2_and_push() { + GroupId groupId = GroupId.parse("__textsecure_group__!9f475f59b2518bff6df22e820803f0e3585bd99e686fa7e7fbfc2f92fd5d953e"); + + GroupId.V2 v2 = groupId.requireV2 (); + GroupId.Push push = groupId.requirePush(); + + assertSame(groupId, v2); + assertSame(groupId, push); + } + + @Test(expected = AssertionError.class) + public void cannot_require_push_of_mms() { + GroupId groupId = GroupId.parse("__signal_mms_group__!000102030405060708090a0b0c0d0e0f"); + + groupId.requirePush(); + } + + @Test(expected = AssertionError.class) + public void cannot_require_v1_of_mms() { + GroupId groupId = GroupId.parse("__signal_mms_group__!000102030405060708090a0b0c0d0e0f"); + + groupId.requireV1(); + } + + @Test(expected = AssertionError.class) + public void cannot_require_v2_of_mms() { + GroupId groupId = GroupId.parse("__signal_mms_group__!000102030405060708090a0b0c0d0e0f"); + + groupId.requireV2(); + } + + @Test(expected = AssertionError.class) + public void cannot_require_v1_of_v2() { + GroupId groupId = GroupId.parse("__textsecure_group__!9f475f59b2518bff6df22e820803f0e3585bd99e686fa7e7fbfc2f92fd5d953e"); + + groupId.requireV1(); + } + + @Test(expected = AssertionError.class) + public void cannot_require_v2_of_v1() { + GroupId groupId = GroupId.parse("__textsecure_group__!000102030405060708090a0b0c0d0e0f"); + + groupId.requireV2(); + } + + @Test(expected = AssertionError.class) + public void cannot_create_v1_with_a_v2_length() throws IOException { + GroupId.v1(Hex.fromStringCondensed("9f475f59b2518bff6df22e820803f0e3585bd99e686fa7e7fbfc2f92fd5d953e")); + } + + @Test(expected = AssertionError.class) + public void cannot_create_v2_with_a_v1_length() throws IOException { + GroupId.v2(Hex.fromStringCondensed("000102030405060708090a0b0c0d0e0f")); + } } diff --git a/app/src/test/java/org/thoughtcrime/securesms/groups/ZkGroupLibraryUtil.java b/app/src/test/java/org/thoughtcrime/securesms/groups/ZkGroupLibraryUtil.java new file mode 100644 index 000000000..0914e9160 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/groups/ZkGroupLibraryUtil.java @@ -0,0 +1,39 @@ +package org.thoughtcrime.securesms.groups; + +import org.signal.zkgroup.internal.Native; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeNoException; + +class ZkGroupLibraryUtil { + + /** + * Attempts to initialize the ZkGroup Native class, which will load the native binaries. + *

+ * If that fails to link, then on Unix, it will fail as we rely on that for CI. + *

+ * If that fails to link, and it's not Unix, it will skip the test via assumption violation. + */ + static void assumeZkGroupSupportedOnOS() { + try { + Class.forName(Native.class.getName()); + } catch (ClassNotFoundException e) { + fail(); + } catch (UnsatisfiedLinkError e) { + String osName = System.getProperty("os.name"); + + if (isUnix(osName)) { + fail("Not able to link native ZkGroup on a key OS: " + osName); + } else { + assumeNoException("Not able to link native ZkGroup on this operating system: " + osName, e); + } + } + } + + private static boolean isUnix(String osName) { + assertNotNull(osName); + osName = osName.toLowerCase(); + return osName.contains("nix") || osName.contains("nux") || osName.contains("aix"); + } +}