#2c6bed
- #2c6bed
+ #6191f3
#1851b4
#3a76f0
#552c6bed
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 581110ed6..b4331eb68 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -53,7 +53,7 @@
- dark
- @style/ThemeOverlay.AppCompat.Dark
- @color/core_grey_95
- - @color/core_ultramarine
+ - @color/core_ultramarine_light
- #d00000
- #66eeeeee
@@ -110,7 +110,7 @@
- false
- true
- true
- - @color/core_ultramarine
+ - @color/core_ultramarine_light
- @color/white
- @color/core_grey_25
@@ -401,6 +401,7 @@
- @drawable/preference_divider_light
- @color/core_grey_60
+ - @color/core_grey_70
- @drawable/ic_group_outline_24
- @style/PreferenceThemeOverlay.Fix
@@ -450,7 +451,7 @@
- @style/TextSecure.ActionModeStyle
- @color/text_color_dark_theme
- @color/text_color_secondary_dark_theme
- - @color/core_ultramarine
+ - @color/core_ultramarine_light
- @color/core_ultramarine_light
- @color/core_ultramarine_light
- @color/core_ultramarine_light
@@ -673,6 +674,7 @@
- @drawable/preference_divider_dark
- @color/core_grey_05
+ - @color/core_white
- @drawable/ic_group_solid_24
- @style/PreferenceThemeOverlay.Fix
@@ -697,7 +699,7 @@
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..b835ddee0 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,90 @@ 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;
+import static org.thoughtcrime.securesms.testutil.SecureRandomTestUtil.mockRandom;
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 +110,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 +137,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 +147,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 +155,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 +165,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 +175,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 +185,100 @@ 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"));
+ }
+
+ @Test
+ public void create_mms() {
+ GroupId.Mms mms = GroupId.createMms(mockRandom(new byte[]{ 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8 }));
+
+ assertEquals("__signal_mms_group__!090a0b0c0d0e0f000102030405060708", mms.toString());
+ assertTrue(mms.isMms());
+ }
+
+ @Test
+ public void create_v1() {
+ GroupId.V1 v1 = GroupId.createV1(mockRandom(new byte[]{ 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8 }));
+
+ assertEquals("__textsecure_group__!090a0b0c0d0e0f000102030405060708", v1.toString());
+ assertTrue(v1.isV1());
+ }
}
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");
+ }
+}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java
index d863b2aa2..74961c334 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java
@@ -113,7 +113,7 @@ public class SignalServiceMessageReceiver {
* @throws IOException
* @throws InvalidMessageException
*/
- public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, int maxSizeBytes)
+ public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, long maxSizeBytes)
throws IOException, InvalidMessageException
{
return retrieveAttachment(pointer, destination, maxSizeBytes, null);
@@ -142,7 +142,7 @@ public class SignalServiceMessageReceiver {
return socket.retrieveProfileByUsername(username, unidentifiedAccess);
}
- public InputStream retrieveProfileAvatar(String path, File destination, ProfileKey profileKey, int maxSizeBytes)
+ public InputStream retrieveProfileAvatar(String path, File destination, ProfileKey profileKey, long maxSizeBytes)
throws IOException
{
socket.retrieveProfileAvatar(path, destination, maxSizeBytes);
@@ -162,7 +162,7 @@ public class SignalServiceMessageReceiver {
* @throws IOException
* @throws InvalidMessageException
*/
- public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, int maxSizeBytes, ProgressListener listener)
+ public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, long maxSizeBytes, ProgressListener listener)
throws IOException, InvalidMessageException
{
if (!pointer.getDigest().isPresent()) throw new InvalidMessageException("No attachment digest!");
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java
index 9c28a3a95..80e8ce059 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java
@@ -27,6 +27,8 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPoin
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
+import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext;
+import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
@@ -64,6 +66,7 @@ import org.whispersystems.signalservice.internal.push.SignalServiceProtos.CallMe
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
+import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.NullMessage;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.ReceiptMessage;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage;
@@ -451,8 +454,15 @@ public class SignalServiceMessageSender {
builder.setBody(message.getBody().get());
}
- if (message.getGroupInfo().isPresent()) {
- builder.setGroup(createGroupContent(message.getGroupInfo().get()));
+ if (message.getGroupContext().isPresent()) {
+ SignalServiceGroupContext groupContext = message.getGroupContext().get();
+ if (groupContext.getGroupV1().isPresent()) {
+ builder.setGroup(createGroupContent(groupContext.getGroupV1().get()));
+ }
+
+ if (groupContext.getGroupV2().isPresent()) {
+ builder.setGroupV2(createGroupContent(groupContext.getGroupV2().get()));
+ }
}
if (message.isEndSession()) {
@@ -966,6 +976,19 @@ public class SignalServiceMessageSender {
return builder.build();
}
+ private static GroupContextV2 createGroupContent(SignalServiceGroupV2 group) {
+ GroupContextV2.Builder builder = GroupContextV2.newBuilder()
+ .setMasterKey(ByteString.copyFrom(group.getMasterKey().serialize()))
+ .setRevision(group.getRevision());
+
+ byte[] signedGroupChange = group.getSignedGroupChange();
+ if (signedGroupChange != null) {
+ builder.setGroupChange(ByteString.copyFrom(signedGroupChange));
+ }
+
+ return builder.build();
+ }
+
private List createSharedContactContent(List contacts) throws IOException {
List results = new LinkedList<>();
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java
index e9d009124..2ac40641e 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java
@@ -11,6 +11,8 @@ import com.google.protobuf.InvalidProtocolBufferException;
import org.signal.libsignal.metadata.ProtocolInvalidKeyException;
import org.signal.libsignal.metadata.ProtocolInvalidMessageException;
+import org.signal.zkgroup.InvalidInputException;
+import org.signal.zkgroup.groups.GroupMasterKey;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.InvalidMessageException;
@@ -253,7 +255,17 @@ public final class SignalServiceContent {
private static SignalServiceDataMessage createSignalServiceMessage(SignalServiceMetadata metadata, SignalServiceProtos.DataMessage content)
throws ProtocolInvalidMessageException, UnsupportedDataMessageException
{
- SignalServiceGroup groupInfo = createGroupInfo(content);
+ SignalServiceGroup groupInfoV1 = createGroupV1Info(content);
+ SignalServiceGroupV2 groupInfoV2 = createGroupV2Info(content);
+ Optional groupContext;
+
+ try {
+ groupContext = SignalServiceGroupContext.createOptional(groupInfoV1, groupInfoV2);
+ } catch (InvalidMessageException e) {
+ throw new ProtocolInvalidMessageException(e, null, 0);
+ }
+
+
List attachments = new LinkedList<>();
boolean endSession = ((content.getFlags() & SignalServiceProtos.DataMessage.Flags.END_SESSION_VALUE ) != 0);
boolean expirationUpdate = ((content.getFlags() & SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE) != 0);
@@ -269,7 +281,7 @@ public final class SignalServiceContent {
content.getRequiredProtocolVersion(),
metadata.getSender().getIdentifier(),
metadata.getSenderDevice(),
- Optional.fromNullable(groupInfo));
+ groupContext);
}
for (SignalServiceProtos.AttachmentPointer pointer : content.getAttachmentsList()) {
@@ -283,7 +295,7 @@ public final class SignalServiceContent {
}
return new SignalServiceDataMessage(metadata.getTimestamp(),
- groupInfo,
+ groupInfoV1, groupInfoV2,
attachments,
content.getBody(),
endSession,
@@ -310,7 +322,7 @@ public final class SignalServiceContent {
? Optional.of(new SignalServiceAddress(UuidUtil.parseOrNull(sentContent.getDestinationUuid()), sentContent.getDestinationE164()))
: Optional.absent();
- if (!address.isPresent() && !dataMessage.getGroupInfo().isPresent()) {
+ if (!address.isPresent() && !dataMessage.getGroupContext().isPresent()) {
throw new ProtocolInvalidMessageException(new InvalidMessageException("SyncMessage missing both destination and group ID!"), null, 0);
}
@@ -739,7 +751,7 @@ public final class SignalServiceContent {
}
- private static SignalServiceGroup createGroupInfo(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException {
+ private static SignalServiceGroup createGroupV1Info(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException {
if (!content.hasGroup()) return null;
SignalServiceGroup.Type type;
@@ -799,4 +811,30 @@ public final class SignalServiceContent {
return new SignalServiceGroup(content.getGroup().getId().toByteArray());
}
+
+ private static SignalServiceGroupV2 createGroupV2Info(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException {
+ if (!content.hasGroupV2()) return null;
+
+ SignalServiceProtos.GroupContextV2 groupV2 = content.getGroupV2();
+ if (!groupV2.hasMasterKey()) {
+ throw new ProtocolInvalidMessageException(new InvalidMessageException("No GV2 master key on message"), null, 0);
+ }
+ if (!groupV2.hasRevision()) {
+ throw new ProtocolInvalidMessageException(new InvalidMessageException("No GV2 revision on message"), null, 0);
+ }
+
+ SignalServiceGroupV2.Builder builder;
+ try {
+ builder = SignalServiceGroupV2.newBuilder(new GroupMasterKey(groupV2.getMasterKey().toByteArray()))
+ .withRevision(groupV2.getRevision());
+ } catch (InvalidInputException e) {
+ throw new ProtocolInvalidMessageException(new InvalidMessageException(e), null, 0);
+ }
+
+ if (groupV2.hasGroupChange()) {
+ builder.withSignedGroupChange(groupV2.getGroupChange().toByteArray());
+ }
+
+ return builder.build();
+ }
}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java
index 28b8a5a50..a3d4067f4 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java
@@ -6,6 +6,7 @@
package org.whispersystems.signalservice.api.messages;
+import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@@ -21,7 +22,7 @@ public class SignalServiceDataMessage {
private final long timestamp;
private final Optional> attachments;
private final Optional body;
- private final Optional group;
+ private final Optional group;
private final Optional profileKey;
private final boolean endSession;
private final boolean expirationUpdate;
@@ -34,101 +35,33 @@ public class SignalServiceDataMessage {
private final boolean viewOnce;
private final Optional reaction;
- /**
- * Construct a SignalServiceDataMessage with a body and no attachments.
- *
- * @param timestamp The sent timestamp.
- * @param body The message contents.
- */
- public SignalServiceDataMessage(long timestamp, String body) {
- this(timestamp, body, 0);
- }
-
- /**
- * Construct an expiring SignalServiceDataMessage with a body and no attachments.
- *
- * @param timestamp The sent timestamp.
- * @param body The message contents.
- * @param expiresInSeconds The number of seconds in which the message should expire after having been seen.
- */
- public SignalServiceDataMessage(long timestamp, String body, int expiresInSeconds) {
- this(timestamp, (List)null, body, expiresInSeconds);
- }
-
-
- public SignalServiceDataMessage(final long timestamp, final SignalServiceAttachment attachment, final String body) {
- this(timestamp, new LinkedList() {{add(attachment);}}, body);
- }
-
- /**
- * Construct a SignalServiceDataMessage with a body and list of attachments.
- *
- * @param timestamp The sent timestamp.
- * @param attachments The attachments.
- * @param body The message contents.
- */
- public SignalServiceDataMessage(long timestamp, List attachments, String body) {
- this(timestamp, attachments, body, 0);
- }
-
- /**
- * Construct an expiring SignalServiceDataMessage with a body and list of attachments.
- *
- * @param timestamp The sent timestamp.
- * @param attachments The attachments.
- * @param body The message contents.
- * @param expiresInSeconds The number of seconds in which the message should expire after having been seen.
- */
- public SignalServiceDataMessage(long timestamp, List attachments, String body, int expiresInSeconds) {
- this(timestamp, null, attachments, body, expiresInSeconds);
- }
-
- /**
- * Construct a SignalServiceDataMessage group message with attachments and body.
- *
- * @param timestamp The sent timestamp.
- * @param group The group information.
- * @param attachments The attachments.
- * @param body The message contents.
- */
- public SignalServiceDataMessage(long timestamp, SignalServiceGroup group, List attachments, String body) {
- this(timestamp, group, attachments, body, 0);
- }
-
-
- /**
- * Construct an expiring SignalServiceDataMessage group message with attachments and body.
- *
- * @param timestamp The sent timestamp.
- * @param group The group information.
- * @param attachments The attachments.
- * @param body The message contents.
- * @param expiresInSeconds The number of seconds in which a message should disappear after having been seen.
- */
- public SignalServiceDataMessage(long timestamp, SignalServiceGroup group, List attachments, String body, int expiresInSeconds) {
- this(timestamp, group, attachments, body, false, expiresInSeconds, false, null, false, null, null, null, null, false, null);
- }
-
/**
* Construct a SignalServiceDataMessage.
*
* @param timestamp The sent timestamp.
* @param group The group information (or null if none).
+ * @param groupV2 The group information (or null if none).
* @param attachments The attachments (or null if none).
* @param body The message contents.
* @param endSession Flag indicating whether this message should close a session.
* @param expiresInSeconds Number of seconds in which the message should disappear after being seen.
*/
- public SignalServiceDataMessage(long timestamp, SignalServiceGroup group,
- List attachments,
- String body, boolean endSession, int expiresInSeconds,
- boolean expirationUpdate, byte[] profileKey, boolean profileKeyUpdate,
- Quote quote, List sharedContacts, List previews,
- Sticker sticker, boolean viewOnce, Reaction reaction)
+ SignalServiceDataMessage(long timestamp,
+ SignalServiceGroup group, SignalServiceGroupV2 groupV2,
+ List attachments,
+ String body, boolean endSession, int expiresInSeconds,
+ boolean expirationUpdate, byte[] profileKey, boolean profileKeyUpdate,
+ Quote quote, List sharedContacts, List previews,
+ Sticker sticker, boolean viewOnce, Reaction reaction)
{
+ try {
+ this.group = SignalServiceGroupContext.createOptional(group, groupV2);
+ } catch (InvalidMessageException e) {
+ throw new AssertionError(e);
+ }
+
this.timestamp = timestamp;
this.body = Optional.fromNullable(body);
- this.group = Optional.fromNullable(group);
this.endSession = endSession;
this.expiresInSeconds = expiresInSeconds;
this.expirationUpdate = expirationUpdate;
@@ -184,9 +117,9 @@ public class SignalServiceDataMessage {
}
/**
- * @return The message group info (if any).
+ * @return The message group context (if any).
*/
- public Optional getGroupInfo() {
+ public Optional getGroupContext() {
return group;
}
@@ -202,8 +135,10 @@ public class SignalServiceDataMessage {
return profileKeyUpdate;
}
- public boolean isGroupUpdate() {
- return group.isPresent() && group.get().getType() != SignalServiceGroup.Type.DELIVER;
+ public boolean isGroupV1Update() {
+ return group.isPresent() &&
+ group.get().getGroupV1().isPresent() &&
+ group.get().getGroupV1().get().getType() != SignalServiceGroup.Type.DELIVER;
}
public int getExpiresInSeconds() {
@@ -244,18 +179,19 @@ public class SignalServiceDataMessage {
private List sharedContacts = new LinkedList<>();
private List previews = new LinkedList<>();
- private long timestamp;
- private SignalServiceGroup group;
- private String body;
- private boolean endSession;
- private int expiresInSeconds;
- private boolean expirationUpdate;
- private byte[] profileKey;
- private boolean profileKeyUpdate;
- private Quote quote;
- private Sticker sticker;
- private boolean viewOnce;
- private Reaction reaction;
+ private long timestamp;
+ private SignalServiceGroup group;
+ private SignalServiceGroupV2 groupV2;
+ private String body;
+ private boolean endSession;
+ private int expiresInSeconds;
+ private boolean expirationUpdate;
+ private byte[] profileKey;
+ private boolean profileKeyUpdate;
+ private Quote quote;
+ private Sticker sticker;
+ private boolean viewOnce;
+ private Reaction reaction;
private Builder() {}
@@ -265,10 +201,21 @@ public class SignalServiceDataMessage {
}
public Builder asGroupMessage(SignalServiceGroup group) {
+ if (this.groupV2 != null) {
+ throw new AssertionError("Can not contain both V1 and V2 group contexts.");
+ }
this.group = group;
return this;
}
+ public Builder asGroupMessage(SignalServiceGroupV2 group) {
+ if (this.group != null) {
+ throw new AssertionError("Can not contain both V1 and V2 group contexts.");
+ }
+ this.groupV2 = group;
+ return this;
+ }
+
public Builder withAttachment(SignalServiceAttachment attachment) {
this.attachments.add(attachment);
return this;
@@ -354,7 +301,7 @@ public class SignalServiceDataMessage {
public SignalServiceDataMessage build() {
if (timestamp == 0) timestamp = System.currentTimeMillis();
- return new SignalServiceDataMessage(timestamp, group, attachments, body, endSession,
+ return new SignalServiceDataMessage(timestamp, group, groupV2, attachments, body, endSession,
expiresInSeconds, expirationUpdate, profileKey,
profileKeyUpdate, quote, sharedContacts, previews,
sticker, viewOnce, reaction);
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java
index 218f111b8..321557aaf 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java
@@ -1,4 +1,4 @@
-/**
+/*
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceGroupContext.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceGroupContext.java
new file mode 100644
index 000000000..6f1ca258e
--- /dev/null
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceGroupContext.java
@@ -0,0 +1,59 @@
+package org.whispersystems.signalservice.api.messages;
+
+import org.whispersystems.libsignal.InvalidMessageException;
+import org.whispersystems.libsignal.util.guava.Optional;
+
+public final class SignalServiceGroupContext {
+
+ private final Optional groupV1;
+ private final Optional groupV2;
+
+ private SignalServiceGroupContext(SignalServiceGroup groupV1) {
+ this.groupV1 = Optional.of(groupV1);
+ this.groupV2 = Optional.absent();
+ }
+
+ private SignalServiceGroupContext(SignalServiceGroupV2 groupV2) {
+ this.groupV1 = Optional.absent();
+ this.groupV2 = Optional.of(groupV2);
+ }
+
+ public Optional getGroupV1() {
+ return groupV1;
+ }
+
+ public Optional getGroupV2() {
+ return groupV2;
+ }
+
+ static Optional createOptional(SignalServiceGroup groupV1, SignalServiceGroupV2 groupV2)
+ throws InvalidMessageException
+ {
+ return Optional.fromNullable(create(groupV1, groupV2));
+ }
+
+ public static SignalServiceGroupContext create(SignalServiceGroup groupV1, SignalServiceGroupV2 groupV2)
+ throws InvalidMessageException
+ {
+ if (groupV1 == null && groupV2 == null) {
+ return null;
+ }
+
+ if (groupV1 != null && groupV2 != null) {
+ throw new InvalidMessageException("Message cannot have both V1 and V2 group contexts.");
+ }
+
+ if (groupV1 != null) {
+ return new SignalServiceGroupContext(groupV1);
+ } else {
+ return new SignalServiceGroupContext(groupV2);
+ }
+ }
+
+ public SignalServiceGroup.Type getGroupV1Type() {
+ if (groupV1.isPresent()) {
+ return groupV1.get().getType();
+ }
+ return null;
+ }
+}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceGroupV2.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceGroupV2.java
new file mode 100644
index 000000000..1362ad4e1
--- /dev/null
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceGroupV2.java
@@ -0,0 +1,66 @@
+package org.whispersystems.signalservice.api.messages;
+
+import org.signal.zkgroup.groups.GroupMasterKey;
+
+/**
+ * Group information to include in SignalServiceMessages destined to v2 groups.
+ *
+ * This class represents a "context" that is included with Signal Service messages
+ * to make them group messages.
+ */
+public final class SignalServiceGroupV2 {
+
+ private final GroupMasterKey masterKey;
+ private final int revision;
+ private final byte[] signedGroupChange;
+
+ private SignalServiceGroupV2(Builder builder) {
+ this.masterKey = builder.masterKey;
+ this.revision = builder.revision;
+ this.signedGroupChange = builder.signedGroupChange != null ? builder.signedGroupChange.clone() : null;
+ }
+
+ public GroupMasterKey getMasterKey() {
+ return masterKey;
+ }
+
+ public int getRevision() {
+ return revision;
+ }
+
+ public byte[] getSignedGroupChange() {
+ return signedGroupChange;
+ }
+
+ public static Builder newBuilder(GroupMasterKey masterKey) {
+ return new Builder(masterKey);
+ }
+
+ public static class Builder {
+
+ private final GroupMasterKey masterKey;
+ private int revision;
+ private byte[] signedGroupChange;
+
+ private Builder(GroupMasterKey masterKey) {
+ if (masterKey == null) {
+ throw new IllegalArgumentException();
+ }
+ this.masterKey = masterKey;
+ }
+
+ Builder withRevision(int revision) {
+ this.revision = revision;
+ return this;
+ }
+
+ Builder withSignedGroupChange(byte[] signedGroupChange) {
+ this.signedGroupChange = signedGroupChange;
+ return this;
+ }
+
+ public SignalServiceGroupV2 build() {
+ return new SignalServiceGroupV2(this);
+ }
+ }
+}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/groupsv2/DecryptedGroupUtil.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/groupsv2/DecryptedGroupUtil.java
new file mode 100644
index 000000000..88cb3df05
--- /dev/null
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/groupsv2/DecryptedGroupUtil.java
@@ -0,0 +1,120 @@
+package org.whispersystems.signalservice.internal.groupsv2;
+
+import com.google.protobuf.ByteString;
+
+import org.signal.storageservice.protos.groups.local.DecryptedGroup;
+import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
+import org.signal.storageservice.protos.groups.local.DecryptedMember;
+import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
+import org.signal.zkgroup.util.UUIDUtil;
+import org.whispersystems.libsignal.util.guava.Optional;
+import org.whispersystems.signalservice.api.util.UuidUtil;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.UUID;
+
+public final class DecryptedGroupUtil {
+
+ public static Set toUuidSet(Collection membersList) {
+ HashSet uuids = new HashSet<>(membersList.size());
+
+ for (DecryptedMember member : membersList) {
+ uuids.add(toUuid(member));
+ }
+
+ return uuids;
+ }
+
+ public static ArrayList toUuidList(Collection membersList) {
+ ArrayList uuidList = new ArrayList<>(membersList.size());
+
+ for (DecryptedMember member : membersList) {
+ uuidList.add(toUuid(member));
+ }
+
+ return uuidList;
+ }
+
+ public static ArrayList pendingToUuidList(Collection membersList) {
+ ArrayList uuidList = new ArrayList<>(membersList.size());
+
+ for (DecryptedPendingMember member : membersList) {
+ uuidList.add(toUuid(member));
+ }
+
+ return uuidList;
+ }
+
+ public static UUID toUuid(DecryptedMember member) {
+ return UUIDUtil.deserialize(member.getUuid().toByteArray());
+ }
+
+ public static UUID toUuid(DecryptedPendingMember member) {
+ return UUIDUtil.deserialize(member.getUuid().toByteArray());
+ }
+
+ /**
+ * The UUID of the member that made the change.
+ */
+ public static UUID editorUuid(DecryptedGroupChange change) {
+ return UuidUtil.fromByteString(change.getEditor());
+ }
+
+ public static Optional findMemberByUuid(Collection members, UUID uuid) {
+ ByteString uuidBytes = ByteString.copyFrom(UUIDUtil.serialize(uuid));
+
+ for (DecryptedMember member : members) {
+ if (uuidBytes.equals(member.getUuid())) {
+ return Optional.of(member);
+ }
+ }
+
+ return Optional.absent();
+ }
+
+ public static Optional findPendingByUuid(Collection members, UUID uuid) {
+ ByteString uuidBytes = ByteString.copyFrom(UUIDUtil.serialize(uuid));
+
+ for (DecryptedPendingMember member : members) {
+ if (uuidBytes.equals(member.getUuid())) {
+ return Optional.of(member);
+ }
+ }
+
+ return Optional.absent();
+ }
+
+ /**
+ * Removes the uuid from the full members of a group.
+ *
+ * Generally not expected to have to do this, just in the case of leaving a group where you cannot
+ * get the new group state as you are not in the group any longer.
+ */
+ public static DecryptedGroup removeMember(DecryptedGroup group, UUID uuid, int revision) {
+ DecryptedGroup.Builder builder = DecryptedGroup.newBuilder(group);
+ ByteString uuidString = UuidUtil.toByteString(uuid);
+ boolean removed = false;
+ ArrayList decryptedMembers = new ArrayList<>(builder.getMembersList());
+ Iterator membersList = decryptedMembers.iterator();
+
+ while (membersList.hasNext()) {
+ if (uuidString.equals(membersList.next().getUuid())) {
+ membersList.remove();
+ removed = true;
+ }
+ }
+
+ if (removed) {
+ return builder.clearMembers()
+ .addAllMembers(decryptedMembers)
+ .setVersion(revision)
+ .build();
+ } else {
+ return group;
+ }
+ }
+}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java
index eb74f0c72..eaa7e6ea5 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java
@@ -498,7 +498,7 @@ public class PushServiceSocket {
makeServiceRequest(SIGNED_PREKEY_PATH, "PUT", JsonUtil.toJson(signedPreKeyEntity));
}
- public void retrieveAttachment(long attachmentId, File destination, int maxSizeBytes, ProgressListener listener)
+ public void retrieveAttachment(long attachmentId, File destination, long maxSizeBytes, ProgressListener listener)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
downloadFromCdn(destination, String.format(Locale.US, ATTACHMENT_DOWNLOAD_PATH, attachmentId), maxSizeBytes, listener);
@@ -590,7 +590,7 @@ public class PushServiceSocket {
}
}
- public void retrieveProfileAvatar(String path, File destination, int maxSizeBytes)
+ public void retrieveProfileAvatar(String path, File destination, long maxSizeBytes)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
downloadFromCdn(destination, path, maxSizeBytes, null);
@@ -874,7 +874,7 @@ public class PushServiceSocket {
return new Pair<>(id, digest);
}
- private void downloadFromCdn(File destination, String path, int maxSizeBytes, ProgressListener listener)
+ private void downloadFromCdn(File destination, String path, long maxSizeBytes, ProgressListener listener)
throws PushNetworkException, NonSuccessfulResponseCodeException
{
try (FileOutputStream outputStream = new FileOutputStream(destination, true)) {
@@ -884,7 +884,7 @@ public class PushServiceSocket {
}
}
- private void downloadFromCdn(OutputStream outputStream, long offset, String path, int maxSizeBytes, ProgressListener listener)
+ private void downloadFromCdn(OutputStream outputStream, long offset, String path, long maxSizeBytes, ProgressListener listener)
throws PushNetworkException, NonSuccessfulResponseCodeException
{
ConnectionHolder connectionHolder = getRandom(cdnClients, random);
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/UnsupportedDataMessageException.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/UnsupportedDataMessageException.java
index 3f89c8e6c..1891aa762 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/UnsupportedDataMessageException.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/UnsupportedDataMessageException.java
@@ -1,8 +1,7 @@
package org.whispersystems.signalservice.internal.push;
import org.whispersystems.libsignal.util.guava.Optional;
-import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
-import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
+import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext;
/**
* Exception that indicates that the data message has a higher required protocol version than the
@@ -10,16 +9,16 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
*/
public class UnsupportedDataMessageException extends Exception {
- private final int requiredVersion;
- private final String sender;
- private final int senderDevice;
- private final Optional group;
+ private final int requiredVersion;
+ private final String sender;
+ private final int senderDevice;
+ private final Optional group;
public UnsupportedDataMessageException(int currentVersion,
int requiredVersion,
String sender,
int senderDevice,
- Optional group)
+ Optional group)
{
super("Required version: " + requiredVersion + ", Our version: " + currentVersion);
this.requiredVersion = requiredVersion;
@@ -40,7 +39,7 @@ public class UnsupportedDataMessageException extends Exception {
return senderDevice;
}
- public Optional getGroup() {
+ public Optional getGroup() {
return group;
}
}
diff --git a/libsignal/service/src/main/proto/SignalService.proto b/libsignal/service/src/main/proto/SignalService.proto
index e583f5e97..37121b2ba 100644
--- a/libsignal/service/src/main/proto/SignalService.proto
+++ b/libsignal/service/src/main/proto/SignalService.proto
@@ -198,6 +198,7 @@ message DataMessage {
optional string body = 1;
repeated AttachmentPointer attachments = 2;
optional GroupContext group = 3;
+ optional GroupContextV2 groupV2 = 15;
optional uint32 flags = 4;
optional uint32 expireTimer = 5;
optional bytes profileKey = 6;
@@ -412,6 +413,12 @@ message GroupContext {
optional AttachmentPointer avatar = 5;
}
+message GroupContextV2 {
+ optional bytes masterKey = 1;
+ optional uint32 revision = 2;
+ optional bytes groupChange = 3;
+}
+
message ContactDetails {
message Avatar {
optional string contentType = 1;
diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/groupsv2/DecryptedGroupUtilTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/groupsv2/DecryptedGroupUtilTest.java
new file mode 100644
index 000000000..b17fc4ec6
--- /dev/null
+++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/groupsv2/DecryptedGroupUtilTest.java
@@ -0,0 +1,41 @@
+package org.whispersystems.signalservice.internal.groupsv2;
+
+import com.google.protobuf.ByteString;
+
+import org.junit.Test;
+import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
+import org.signal.storageservice.protos.groups.local.DecryptedMember;
+import org.signal.zkgroup.util.UUIDUtil;
+
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+
+public final class DecryptedGroupUtilTest {
+
+ @Test
+ public void can_extract_uuid_from_decrypted_member() {
+ UUID uuid = UUID.randomUUID();
+ DecryptedMember decryptedMember = DecryptedMember.newBuilder()
+ .setUuid(ByteString.copyFrom(UUIDUtil.serialize(uuid)))
+ .build();
+
+ UUID parsed = DecryptedGroupUtil.toUuid(decryptedMember);
+
+ assertEquals(uuid, parsed);
+ }
+
+ @Test
+ public void can_extract_editor_uuid_from_decrypted_group_change() {
+ UUID uuid = UUID.randomUUID();
+ ByteString editor = ByteString.copyFrom(UUIDUtil.serialize(uuid));
+ DecryptedGroupChange groupChange = DecryptedGroupChange.newBuilder()
+ .setEditor(editor)
+ .build();
+
+ UUID parsed = DecryptedGroupUtil.editorUuid(groupChange);
+
+ assertEquals(uuid, parsed);
+ }
+
+}