From 9942d967a4a83230aadd21c86344c6f1ac246611 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 24 Dec 2020 16:36:47 +0100 Subject: [PATCH] Refactor to use GroupId class to wrap the byte array Helps distinguish between group v1 and v2 ids --- .../signal/JsonDbusReceiveMessageHandler.java | 21 +--- .../asamk/signal/ReceiveMessageHandler.java | 14 ++- .../asamk/signal/commands/BlockCommand.java | 5 +- .../signal/commands/JoinGroupCommand.java | 10 +- .../signal/commands/ListGroupsCommand.java | 5 +- .../signal/commands/QuitGroupCommand.java | 5 +- .../asamk/signal/commands/SendCommand.java | 4 +- .../signal/commands/SendReactionCommand.java | 5 +- .../asamk/signal/commands/UnblockCommand.java | 5 +- .../signal/commands/UpdateGroupCommand.java | 4 +- .../org/asamk/signal/dbus/DbusSignalImpl.java | 19 +-- .../org/asamk/signal/json/JsonGroupInfo.java | 2 +- .../org/asamk/signal/manager/GroupId.java | 63 ++++++++++ .../manager/GroupIdFormatException.java | 8 ++ .../org/asamk/signal/manager/GroupIdV1.java | 14 +++ .../org/asamk/signal/manager/GroupIdV2.java | 14 +++ .../manager/GroupNotFoundException.java | 6 +- .../org/asamk/signal/manager/GroupUtils.java | 31 ++++- .../asamk/signal/manager/HandleAction.java | 27 ++-- .../org/asamk/signal/manager/KeyUtils.java | 4 - .../org/asamk/signal/manager/Manager.java | 116 +++++++++--------- .../manager/NotAGroupMemberException.java | 6 +- .../signal/manager/helper/GroupHelper.java | 4 +- .../asamk/signal/storage/SignalAccount.java | 3 +- .../signal/storage/groups/GroupInfo.java | 10 +- .../signal/storage/groups/GroupInfoV1.java | 61 ++++++--- .../signal/storage/groups/GroupInfoV2.java | 11 +- .../signal/storage/groups/JsonGroupStore.java | 76 +++++------- .../org/asamk/signal/util/ErrorUtils.java | 1 + .../signal/util/GroupIdFormatException.java | 10 -- src/main/java/org/asamk/signal/util/Util.java | 12 +- 31 files changed, 353 insertions(+), 223 deletions(-) create mode 100644 src/main/java/org/asamk/signal/manager/GroupId.java create mode 100644 src/main/java/org/asamk/signal/manager/GroupIdFormatException.java create mode 100644 src/main/java/org/asamk/signal/manager/GroupIdV1.java create mode 100644 src/main/java/org/asamk/signal/manager/GroupIdV2.java delete mode 100644 src/main/java/org/asamk/signal/util/GroupIdFormatException.java diff --git a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java index 41b91a48..50eb9f9b 100644 --- a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java @@ -65,7 +65,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { } else if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); - byte[] groupId = getGroupId(m, message); + byte[] groupId = getGroupId(message); if (!message.isEndSession() && ( groupId == null || message.getGroupContext().get().getGroupV1Type() == null @@ -91,7 +91,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { .getGroupContext() .isPresent()) { SignalServiceDataMessage message = transcript.getMessage(); - byte[] groupId = getGroupId(m, message); + byte[] groupId = getGroupId(message); try { conn.sendMessage(new Signal.SyncMessageReceived(objectPath, @@ -112,20 +112,9 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { } } - private static byte[] getGroupId(final Manager m, final SignalServiceDataMessage message) { - byte[] groupId; - if (message.getGroupContext().isPresent()) { - if (message.getGroupContext().get().getGroupV1().isPresent()) { - groupId = message.getGroupContext().get().getGroupV1().get().getGroupId(); - } else if (message.getGroupContext().get().getGroupV2().isPresent()) { - groupId = GroupUtils.getGroupId(message.getGroupContext().get().getGroupV2().get().getMasterKey()); - } else { - groupId = null; - } - } else { - groupId = null; - } - return groupId; + private static byte[] getGroupId(final SignalServiceDataMessage message) { + return message.getGroupContext().isPresent() ? GroupUtils.getGroupId(message.getGroupContext().get()) + .serialize() : null; } static private List getAttachments(SignalServiceDataMessage message, Manager m) { diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 711f503d..99010e13 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -1,5 +1,6 @@ package org.asamk.signal; +import org.asamk.signal.manager.GroupId; import org.asamk.signal.manager.GroupUtils; import org.asamk.signal.manager.Manager; import org.asamk.signal.storage.contacts.ContactInfo; @@ -328,8 +329,9 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { System.out.println(" - Timestamp: " + DateUtils.formatTimestamp(typingMessage.getTimestamp())); if (typingMessage.getGroupId().isPresent()) { System.out.println(" - Group Info:"); - System.out.println(" Id: " + Base64.encodeBytes(typingMessage.getGroupId().get())); - GroupInfo group = m.getGroup(typingMessage.getGroupId().get()); + final GroupId groupId = GroupId.unknownVersion(typingMessage.getGroupId().get()); + System.out.println(" Id: " + groupId.toBase64()); + GroupInfo group = m.getGroup(groupId); if (group != null) { System.out.println(" Name: " + group.getTitle()); } else { @@ -356,13 +358,14 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { if (message.getGroupContext().isPresent()) { System.out.println("Group info:"); final SignalServiceGroupContext groupContext = message.getGroupContext().get(); + final GroupId groupId = GroupUtils.getGroupId(groupContext); if (groupContext.getGroupV1().isPresent()) { SignalServiceGroup groupInfo = groupContext.getGroupV1().get(); - System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId())); + System.out.println(" Id: " + groupId.toBase64()); if (groupInfo.getType() == SignalServiceGroup.Type.UPDATE && groupInfo.getName().isPresent()) { System.out.println(" Name: " + groupInfo.getName().get()); } else { - GroupInfo group = m.getGroup(groupInfo.getGroupId()); + GroupInfo group = m.getGroup(groupId); if (group != null) { System.out.println(" Name: " + group.getTitle()); } else { @@ -381,8 +384,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { } } else if (groupContext.getGroupV2().isPresent()) { final SignalServiceGroupV2 groupInfo = groupContext.getGroupV2().get(); - byte[] groupId = GroupUtils.getGroupId(groupInfo.getMasterKey()); - System.out.println(" Id: " + Base64.encodeBytes(groupId)); + System.out.println(" Id: " + groupId.toBase64()); GroupInfo group = m.getGroup(groupId); if (group != null) { System.out.println(" Name: " + group.getTitle()); diff --git a/src/main/java/org/asamk/signal/commands/BlockCommand.java b/src/main/java/org/asamk/signal/commands/BlockCommand.java index 95c3738c..2a9bc4e9 100644 --- a/src/main/java/org/asamk/signal/commands/BlockCommand.java +++ b/src/main/java/org/asamk/signal/commands/BlockCommand.java @@ -3,9 +3,10 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.manager.GroupId; +import org.asamk.signal.manager.GroupIdFormatException; import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.Manager; -import org.asamk.signal.util.GroupIdFormatException; import org.asamk.signal.util.Util; import org.whispersystems.signalservice.api.util.InvalidNumberException; @@ -36,7 +37,7 @@ public class BlockCommand implements LocalCommand { if (ns.getList("group") != null) { for (String groupIdString : ns.getList("group")) { try { - byte[] groupId = Util.decodeGroupId(groupIdString); + GroupId groupId = Util.decodeGroupId(groupIdString); m.setGroupBlocked(groupId, true); } catch (GroupIdFormatException | GroupNotFoundException e) { System.err.println(e.getMessage()); diff --git a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java index 62b996cf..8438e1fa 100644 --- a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java @@ -4,6 +4,7 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.Signal; +import org.asamk.signal.manager.GroupId; import org.asamk.signal.manager.GroupInviteLinkUrl; import org.asamk.signal.manager.Manager; import org.freedesktop.dbus.exceptions.DBusExecutionException; @@ -11,7 +12,6 @@ import org.whispersystems.libsignal.util.Pair; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.internal.push.exceptions.GroupPatchNotAcceptedException; -import org.whispersystems.util.Base64; import java.io.IOException; import java.util.List; @@ -52,12 +52,12 @@ public class JoinGroupCommand implements LocalCommand { } try { - final Pair> results = m.joinGroup(linkUrl); - byte[] newGroupId = results.first(); + final Pair> results = m.joinGroup(linkUrl); + GroupId newGroupId = results.first(); if (!m.getGroup(newGroupId).isMember(m.getSelfAddress())) { - System.out.println("Requested to join group \"" + Base64.encodeBytes(newGroupId) + "\""); + System.out.println("Requested to join group \"" + newGroupId.toBase64() + "\""); } else { - System.out.println("Joined group \"" + Base64.encodeBytes(newGroupId) + "\""); + System.out.println("Joined group \"" + newGroupId.toBase64() + "\""); } return handleTimestampAndSendMessageResults(0, results.second()); } catch (AssertionError e) { diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index e7844fda..4d1032a2 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -8,7 +8,6 @@ import org.asamk.signal.manager.GroupInviteLinkUrl; import org.asamk.signal.manager.Manager; import org.asamk.signal.storage.groups.GroupInfo; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.util.Base64; import java.util.List; import java.util.Set; @@ -40,7 +39,7 @@ public class ListGroupsCommand implements LocalCommand { System.out.println(String.format( "Id: %s Name: %s Active: %s Blocked: %b Members: %s Pending members: %s Requesting members: %s Link: %s", - Base64.encodeBytes(group.groupId), + group.getGroupId().toBase64(), group.getTitle(), group.isMember(m.getSelfAddress()), group.isBlocked(), @@ -50,7 +49,7 @@ public class ListGroupsCommand implements LocalCommand { groupInviteLink == null ? '-' : groupInviteLink.getUrl())); } else { System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b", - Base64.encodeBytes(group.groupId), + group.getGroupId().toBase64(), group.getTitle(), group.isMember(m.getSelfAddress()), group.isBlocked())); diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java index 20d06eba..efc63f8f 100644 --- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -3,10 +3,11 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.manager.GroupId; +import org.asamk.signal.manager.GroupIdFormatException; import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotAGroupMemberException; -import org.asamk.signal.util.GroupIdFormatException; import org.asamk.signal.util.Util; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.signalservice.api.messages.SendMessageResult; @@ -36,7 +37,7 @@ public class QuitGroupCommand implements LocalCommand { } try { - final byte[] groupId = Util.decodeGroupId(ns.getString("group")); + final GroupId groupId = Util.decodeGroupId(ns.getString("group")); final Pair> results = m.sendQuitGroupMessage(groupId); return handleTimestampAndSendMessageResults(results.first(), results.second()); } catch (IOException e) { diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index 551cf938..04b06434 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -5,7 +5,7 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.Signal; -import org.asamk.signal.util.GroupIdFormatException; +import org.asamk.signal.manager.GroupIdFormatException; import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.Util; import org.freedesktop.dbus.exceptions.DBusExecutionException; @@ -79,7 +79,7 @@ public class SendCommand implements DbusCommand { if (ns.getString("group") != null) { byte[] groupId; try { - groupId = Util.decodeGroupId(ns.getString("group")); + groupId = Util.decodeGroupId(ns.getString("group")).serialize(); } catch (GroupIdFormatException e) { handleGroupIdFormatException(e); return 1; diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index 000a1349..345c9180 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -4,10 +4,11 @@ import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.manager.GroupId; +import org.asamk.signal.manager.GroupIdFormatException; import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotAGroupMemberException; -import org.asamk.signal.util.GroupIdFormatException; import org.asamk.signal.util.Util; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.signalservice.api.messages.SendMessageResult; @@ -65,7 +66,7 @@ public class SendReactionCommand implements LocalCommand { try { final Pair> results; if (ns.getString("group") != null) { - byte[] groupId = Util.decodeGroupId(ns.getString("group")); + GroupId groupId = Util.decodeGroupId(ns.getString("group")); results = m.sendGroupMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, groupId); } else { results = m.sendMessageReaction(emoji, diff --git a/src/main/java/org/asamk/signal/commands/UnblockCommand.java b/src/main/java/org/asamk/signal/commands/UnblockCommand.java index b4f6cc3b..73e578ac 100644 --- a/src/main/java/org/asamk/signal/commands/UnblockCommand.java +++ b/src/main/java/org/asamk/signal/commands/UnblockCommand.java @@ -3,9 +3,10 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.manager.GroupId; +import org.asamk.signal.manager.GroupIdFormatException; import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.Manager; -import org.asamk.signal.util.GroupIdFormatException; import org.asamk.signal.util.Util; import org.whispersystems.signalservice.api.util.InvalidNumberException; @@ -36,7 +37,7 @@ public class UnblockCommand implements LocalCommand { if (ns.getList("group") != null) { for (String groupIdString : ns.getList("group")) { try { - byte[] groupId = Util.decodeGroupId(groupIdString); + GroupId groupId = Util.decodeGroupId(groupIdString); m.setGroupBlocked(groupId, false); } catch (GroupIdFormatException | GroupNotFoundException e) { System.err.println(e.getMessage()); diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 4216fd9b..dae06b86 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -4,7 +4,7 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.Signal; -import org.asamk.signal.util.GroupIdFormatException; +import org.asamk.signal.manager.GroupIdFormatException; import org.asamk.signal.util.Util; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.util.Base64; @@ -35,7 +35,7 @@ public class UpdateGroupCommand implements DbusCommand { byte[] groupId = null; if (ns.getString("group") != null) { try { - groupId = Util.decodeGroupId(ns.getString("group")); + groupId = Util.decodeGroupId(ns.getString("group")).serialize(); } catch (GroupIdFormatException e) { handleGroupIdFormatException(e); return 1; diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 396063f2..cbb72835 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -2,6 +2,7 @@ package org.asamk.signal.dbus; import org.asamk.Signal; import org.asamk.signal.manager.AttachmentInvalidException; +import org.asamk.signal.manager.GroupId; import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotAGroupMemberException; @@ -92,7 +93,9 @@ public class DbusSignalImpl implements Signal { @Override public long sendGroupMessage(final String message, final List attachments, final byte[] groupId) { try { - Pair> results = m.sendGroupMessage(message, attachments, groupId); + Pair> results = m.sendGroupMessage(message, + attachments, + GroupId.unknownVersion(groupId)); checkSendMessageResults(results.first(), results.second()); return results.first(); } catch (IOException e) { @@ -134,7 +137,7 @@ public class DbusSignalImpl implements Signal { @Override public void setGroupBlocked(final byte[] groupId, final boolean blocked) { try { - m.setGroupBlocked(groupId, blocked); + m.setGroupBlocked(GroupId.unknownVersion(groupId), blocked); } catch (GroupNotFoundException e) { throw new Error.GroupNotFound(e.getMessage()); } @@ -145,14 +148,14 @@ public class DbusSignalImpl implements Signal { List groups = m.getGroups(); List ids = new ArrayList<>(groups.size()); for (GroupInfo group : groups) { - ids.add(group.groupId); + ids.add(group.getGroupId().serialize()); } return ids; } @Override public String getGroupName(final byte[] groupId) { - GroupInfo group = m.getGroup(groupId); + GroupInfo group = m.getGroup(GroupId.unknownVersion(groupId)); if (group == null) { return ""; } else { @@ -162,7 +165,7 @@ public class DbusSignalImpl implements Signal { @Override public List getGroupMembers(final byte[] groupId) { - GroupInfo group = m.getGroup(groupId); + GroupInfo group = m.getGroup(GroupId.unknownVersion(groupId)); if (group == null) { return Collections.emptyList(); } else { @@ -189,9 +192,11 @@ public class DbusSignalImpl implements Signal { if (avatar.isEmpty()) { avatar = null; } - final Pair> results = m.updateGroup(groupId, name, members, avatar); + final Pair> results = m.updateGroup(groupId == null + ? null + : GroupId.unknownVersion(groupId), name, members, avatar); checkSendMessageResults(0, results.second()); - return results.first(); + return results.first().serialize(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException e) { diff --git a/src/main/java/org/asamk/signal/json/JsonGroupInfo.java b/src/main/java/org/asamk/signal/json/JsonGroupInfo.java index 970cde52..9709be20 100644 --- a/src/main/java/org/asamk/signal/json/JsonGroupInfo.java +++ b/src/main/java/org/asamk/signal/json/JsonGroupInfo.java @@ -31,7 +31,7 @@ class JsonGroupInfo { } JsonGroupInfo(SignalServiceGroupV2 groupInfo) { - this.groupId = Base64.encodeBytes(GroupUtils.getGroupId(groupInfo.getMasterKey())); + this.groupId = GroupUtils.getGroupIdV2(groupInfo.getMasterKey()).toBase64(); this.type = groupInfo.hasSignedGroupChange() ? "UPDATE" : "DELIVER"; } diff --git a/src/main/java/org/asamk/signal/manager/GroupId.java b/src/main/java/org/asamk/signal/manager/GroupId.java new file mode 100644 index 00000000..34e18e8e --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/GroupId.java @@ -0,0 +1,63 @@ +package org.asamk.signal.manager; + +import org.whispersystems.util.Base64; + +import java.util.Arrays; + +public abstract class GroupId { + + private final byte[] id; + + public static GroupIdV1 v1(byte[] id) { + return new GroupIdV1(id); + } + + public static GroupIdV2 v2(byte[] id) { + return new GroupIdV2(id); + } + + public static GroupId unknownVersion(byte[] id) { + if (id.length == 16) { + return new GroupIdV1(id); + } else if (id.length == 32) { + return new GroupIdV2(id); + } + + throw new AssertionError("Invalid group id of size " + id.length); + } + + public static GroupId fromBase64(String id) throws GroupIdFormatException { + try { + return unknownVersion(java.util.Base64.getDecoder().decode(id)); + } catch (Throwable e) { + throw new GroupIdFormatException(id, e); + } + } + + public GroupId(final byte[] id) { + this.id = id; + } + + public byte[] serialize() { + return id; + } + + public String toBase64() { + return Base64.encodeBytes(id); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final GroupId groupId = (GroupId) o; + + return Arrays.equals(id, groupId.id); + } + + @Override + public int hashCode() { + return Arrays.hashCode(id); + } +} diff --git a/src/main/java/org/asamk/signal/manager/GroupIdFormatException.java b/src/main/java/org/asamk/signal/manager/GroupIdFormatException.java new file mode 100644 index 00000000..83afd15b --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/GroupIdFormatException.java @@ -0,0 +1,8 @@ +package org.asamk.signal.manager; + +public class GroupIdFormatException extends Exception { + + public GroupIdFormatException(String groupId, Throwable e) { + super("Failed to decode groupId (must be base64) \"" + groupId + "\": " + e.getMessage(), e); + } +} diff --git a/src/main/java/org/asamk/signal/manager/GroupIdV1.java b/src/main/java/org/asamk/signal/manager/GroupIdV1.java new file mode 100644 index 00000000..40862f07 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/GroupIdV1.java @@ -0,0 +1,14 @@ +package org.asamk.signal.manager; + +import static org.asamk.signal.manager.KeyUtils.getSecretBytes; + +public class GroupIdV1 extends GroupId { + + public static GroupIdV1 createRandom() { + return new GroupIdV1(getSecretBytes(16)); + } + + public GroupIdV1(final byte[] id) { + super(id); + } +} diff --git a/src/main/java/org/asamk/signal/manager/GroupIdV2.java b/src/main/java/org/asamk/signal/manager/GroupIdV2.java new file mode 100644 index 00000000..b329be1d --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/GroupIdV2.java @@ -0,0 +1,14 @@ +package org.asamk.signal.manager; + +import java.util.Base64; + +public class GroupIdV2 extends GroupId { + + public static GroupIdV2 fromBase64(String groupId) { + return new GroupIdV2(Base64.getDecoder().decode(groupId)); + } + + public GroupIdV2(final byte[] id) { + super(id); + } +} diff --git a/src/main/java/org/asamk/signal/manager/GroupNotFoundException.java b/src/main/java/org/asamk/signal/manager/GroupNotFoundException.java index 0c0d6d2d..d7efa923 100644 --- a/src/main/java/org/asamk/signal/manager/GroupNotFoundException.java +++ b/src/main/java/org/asamk/signal/manager/GroupNotFoundException.java @@ -1,10 +1,8 @@ package org.asamk.signal.manager; -import org.whispersystems.util.Base64; - public class GroupNotFoundException extends Exception { - public GroupNotFoundException(byte[] groupId) { - super("Group not found: " + Base64.encodeBytes(groupId)); + public GroupNotFoundException(GroupId groupId) { + super("Group not found: " + groupId.toBase64()); } } diff --git a/src/main/java/org/asamk/signal/manager/GroupUtils.java b/src/main/java/org/asamk/signal/manager/GroupUtils.java index a0e95c7a..d86dfbe9 100644 --- a/src/main/java/org/asamk/signal/manager/GroupUtils.java +++ b/src/main/java/org/asamk/signal/manager/GroupUtils.java @@ -9,6 +9,7 @@ import org.signal.zkgroup.groups.GroupSecretParams; import org.whispersystems.libsignal.kdf.HKDFv3; 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; public class GroupUtils { @@ -18,7 +19,7 @@ public class GroupUtils { ) { if (groupInfo instanceof GroupInfoV1) { SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER) - .withId(groupInfo.groupId) + .withId(groupInfo.getGroupId().serialize()) .build(); messageBuilder.asGroupMessage(group); } else { @@ -30,14 +31,34 @@ public class GroupUtils { } } - public static byte[] getGroupId(GroupMasterKey groupMasterKey) { + public static GroupId getGroupId(SignalServiceGroupContext context) { + if (context.getGroupV1().isPresent()) { + return GroupId.v1(context.getGroupV1().get().getGroupId()); + } else if (context.getGroupV2().isPresent()) { + return getGroupIdV2(context.getGroupV2().get().getMasterKey()); + } else { + return null; + } + } + + public static GroupIdV2 getGroupIdV2(GroupSecretParams groupSecretParams) { + return GroupId.v2(groupSecretParams.getPublicParams().getGroupIdentifier().serialize()); + } + + public static GroupIdV2 getGroupIdV2(GroupMasterKey groupMasterKey) { final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); - return groupSecretParams.getPublicParams().getGroupIdentifier().serialize(); + return getGroupIdV2(groupSecretParams); + } + + public static GroupIdV2 getGroupIdV2(GroupIdV1 groupIdV1) { + final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(deriveV2MigrationMasterKey( + groupIdV1)); + return getGroupIdV2(groupSecretParams); } - public static GroupMasterKey deriveV2MigrationMasterKey(byte[] groupIdV1) { + private static GroupMasterKey deriveV2MigrationMasterKey(GroupIdV1 groupIdV1) { try { - return new GroupMasterKey(new HKDFv3().deriveSecrets(groupIdV1, + return new GroupMasterKey(new HKDFv3().deriveSecrets(groupIdV1.serialize(), "GV2 Migration".getBytes(), GroupMasterKey.SIZE)); } catch (InvalidInputException e) { diff --git a/src/main/java/org/asamk/signal/manager/HandleAction.java b/src/main/java/org/asamk/signal/manager/HandleAction.java index 9bdd3885..aa25d8c5 100644 --- a/src/main/java/org/asamk/signal/manager/HandleAction.java +++ b/src/main/java/org/asamk/signal/manager/HandleAction.java @@ -2,7 +2,6 @@ package org.asamk.signal.manager; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import java.util.Arrays; import java.util.Objects; interface HandleAction { @@ -93,9 +92,9 @@ class SendSyncBlockedListAction implements HandleAction { class SendGroupInfoRequestAction implements HandleAction { private final SignalServiceAddress address; - private final byte[] groupId; + private final GroupIdV1 groupId; - public SendGroupInfoRequestAction(final SignalServiceAddress address, final byte[] groupId) { + public SendGroupInfoRequestAction(final SignalServiceAddress address, final GroupIdV1 groupId) { this.address = address; this.groupId = groupId; } @@ -109,14 +108,17 @@ class SendGroupInfoRequestAction implements HandleAction { public boolean equals(final Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; + final SendGroupInfoRequestAction that = (SendGroupInfoRequestAction) o; - return address.equals(that.address) && Arrays.equals(groupId, that.groupId); + + if (!address.equals(that.address)) return false; + return groupId.equals(that.groupId); } @Override public int hashCode() { - int result = Objects.hash(address); - result = 31 * result + Arrays.hashCode(groupId); + int result = address.hashCode(); + result = 31 * result + groupId.hashCode(); return result; } } @@ -124,9 +126,9 @@ class SendGroupInfoRequestAction implements HandleAction { class SendGroupUpdateAction implements HandleAction { private final SignalServiceAddress address; - private final byte[] groupId; + private final GroupIdV1 groupId; - public SendGroupUpdateAction(final SignalServiceAddress address, final byte[] groupId) { + public SendGroupUpdateAction(final SignalServiceAddress address, final GroupIdV1 groupId) { this.address = address; this.groupId = groupId; } @@ -140,14 +142,17 @@ class SendGroupUpdateAction implements HandleAction { public boolean equals(final Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; + final SendGroupUpdateAction that = (SendGroupUpdateAction) o; - return address.equals(that.address) && Arrays.equals(groupId, that.groupId); + + if (!address.equals(that.address)) return false; + return groupId.equals(that.groupId); } @Override public int hashCode() { - int result = Objects.hash(address); - result = 31 * result + Arrays.hashCode(groupId); + int result = address.hashCode(); + result = 31 * result + groupId.hashCode(); return result; } } diff --git a/src/main/java/org/asamk/signal/manager/KeyUtils.java b/src/main/java/org/asamk/signal/manager/KeyUtils.java index d6f332c0..21f6037f 100644 --- a/src/main/java/org/asamk/signal/manager/KeyUtils.java +++ b/src/main/java/org/asamk/signal/manager/KeyUtils.java @@ -26,10 +26,6 @@ class KeyUtils { return getSecret(18); } - static byte[] createGroupId() { - return getSecretBytes(16); - } - static byte[] createStickerUploadKey() { return getSecretBytes(32); } diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 8a46846f..b5d425d8 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -679,7 +679,7 @@ public class Manager implements Closeable { } } - private Optional createGroupAvatarAttachment(byte[] groupId) throws IOException { + private Optional createGroupAvatarAttachment(GroupId groupId) throws IOException { File file = getGroupAvatarFile(groupId); if (!file.exists()) { return Optional.absent(); @@ -697,7 +697,7 @@ public class Manager implements Closeable { return Optional.of(Utils.createAttachment(file)); } - private GroupInfo getGroupForSending(byte[] groupId) throws GroupNotFoundException, NotAGroupMemberException { + private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException { GroupInfo g = account.getGroupStore().getGroup(groupId); if (g == null) { throw new GroupNotFoundException(groupId); @@ -708,7 +708,7 @@ public class Manager implements Closeable { return g; } - private GroupInfo getGroupForUpdating(byte[] groupId) throws GroupNotFoundException, NotAGroupMemberException { + private GroupInfo getGroupForUpdating(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException { GroupInfo g = account.getGroupStore().getGroup(groupId); if (g == null) { throw new GroupNotFoundException(groupId); @@ -724,7 +724,7 @@ public class Manager implements Closeable { } public Pair> sendGroupMessage( - SignalServiceDataMessage.Builder messageBuilder, byte[] groupId + SignalServiceDataMessage.Builder messageBuilder, GroupId groupId ) throws IOException, GroupNotFoundException, NotAGroupMemberException { final GroupInfo g = getGroupForSending(groupId); @@ -735,7 +735,7 @@ public class Manager implements Closeable { } public Pair> sendGroupMessage( - String messageText, List attachments, byte[] groupId + String messageText, List attachments, GroupId groupId ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .withBody(messageText); @@ -747,7 +747,7 @@ public class Manager implements Closeable { } public Pair> sendGroupMessageReaction( - String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, byte[] groupId + String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, GroupId groupId ) throws IOException, InvalidNumberException, NotAGroupMemberException, GroupNotFoundException { SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, @@ -759,7 +759,7 @@ public class Manager implements Closeable { return sendGroupMessage(messageBuilder, groupId); } - public Pair> sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, NotAGroupMemberException { + public Pair> sendQuitGroupMessage(GroupId groupId) throws GroupNotFoundException, IOException, NotAGroupMemberException { SignalServiceDataMessage.Builder messageBuilder; @@ -767,7 +767,7 @@ public class Manager implements Closeable { if (g instanceof GroupInfoV1) { GroupInfoV1 groupInfoV1 = (GroupInfoV1) g; SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT) - .withId(groupId) + .withId(groupId.serialize()) .build(); messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group); groupInfoV1.removeMember(account.getSelfAddress()); @@ -783,8 +783,8 @@ public class Manager implements Closeable { return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress())); } - private Pair> sendUpdateGroupMessage( - byte[] groupId, String name, Collection members, String avatarFile + private Pair> sendUpdateGroupMessage( + GroupId groupId, String name, Collection members, String avatarFile ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { GroupInfo g; SignalServiceDataMessage.Builder messageBuilder; @@ -792,7 +792,7 @@ public class Manager implements Closeable { // Create new group GroupInfoV2 gv2 = groupHelper.createGroupV2(name, members, avatarFile); if (gv2 == null) { - GroupInfoV1 gv1 = new GroupInfoV1(KeyUtils.createGroupId()); + GroupInfoV1 gv1 = new GroupInfoV1(GroupIdV1.createRandom()); gv1.addMembers(Collections.singleton(account.getSelfAddress())); updateGroupV1(gv1, name, members, avatarFile); messageBuilder = getGroupUpdateMessageBuilder(gv1); @@ -834,7 +834,7 @@ public class Manager implements Closeable { groupGroupChangePair.second()); } - return new Pair<>(group.groupId, result.second()); + return new Pair<>(group.getGroupId(), result.second()); } else { GroupInfoV1 gv1 = (GroupInfoV1) group; updateGroupV1(gv1, name, members, avatarFile); @@ -847,16 +847,16 @@ public class Manager implements Closeable { final Pair> result = sendMessage(messageBuilder, g.getMembersIncludingPendingWithout(account.getSelfAddress())); - return new Pair<>(g.groupId, result.second()); + return new Pair<>(g.getGroupId(), result.second()); } - public Pair> joinGroup( + public Pair> joinGroup( GroupInviteLinkUrl inviteLinkUrl ) throws IOException, GroupLinkNotActiveException { return sendJoinGroupMessage(inviteLinkUrl); } - private Pair> sendJoinGroupMessage( + private Pair> sendJoinGroupMessage( GroupInviteLinkUrl inviteLinkUrl ) throws IOException, GroupLinkNotActiveException { final DecryptedGroupJoinInfo groupJoinInfo = groupHelper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(), @@ -870,12 +870,12 @@ public class Manager implements Closeable { if (group.getGroup() == null) { // Only requested member, can't send update to group members - return new Pair<>(group.groupId, List.of()); + return new Pair<>(group.getGroupId(), List.of()); } final Pair> result = sendUpdateGroupMessage(group, group.getGroup(), groupChange); - return new Pair<>(group.groupId, result.second()); + return new Pair<>(group.getGroupId(), result.second()); } private Pair> sendUpdateGroupMessage( @@ -923,13 +923,13 @@ public class Manager implements Closeable { if (avatarFile != null) { IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath()); - File aFile = getGroupAvatarFile(g.groupId); + File aFile = getGroupAvatarFile(g.getGroupId()); Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } } Pair> sendUpdateGroupMessage( - byte[] groupId, SignalServiceAddress recipient + GroupIdV1 groupId, SignalServiceAddress recipient ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException { GroupInfoV1 g; GroupInfo group = getGroupForSending(groupId); @@ -950,11 +950,11 @@ public class Manager implements Closeable { private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException { SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE) - .withId(g.groupId) + .withId(g.getGroupId().serialize()) .withName(g.name) .withMembers(new ArrayList<>(g.getMembers())); - File aFile = getGroupAvatarFile(g.groupId); + File aFile = getGroupAvatarFile(g.getGroupId()); if (aFile.exists()) { try { group.withAvatar(Utils.createAttachment(aFile)); @@ -978,10 +978,10 @@ public class Manager implements Closeable { } Pair> sendGroupInfoRequest( - byte[] groupId, SignalServiceAddress recipient + GroupIdV1 groupId, SignalServiceAddress recipient ) throws IOException { SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO) - .withId(groupId); + .withId(groupId.serialize()); SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .asGroupMessage(group.build()); @@ -1087,7 +1087,7 @@ public class Manager implements Closeable { account.save(); } - public void setGroupBlocked(final byte[] groupId, final boolean blocked) throws GroupNotFoundException { + public void setGroupBlocked(final GroupId groupId, final boolean blocked) throws GroupNotFoundException { GroupInfo group = getGroup(groupId); if (group == null) { throw new GroupNotFoundException(groupId); @@ -1098,8 +1098,8 @@ public class Manager implements Closeable { account.save(); } - public Pair> updateGroup( - byte[] groupId, String name, List members, String avatar + public Pair> updateGroup( + GroupId groupId, String name, List members, String avatar ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException { return sendUpdateGroupMessage(groupId, name, @@ -1137,7 +1137,7 @@ public class Manager implements Closeable { /** * Change the expiration timer for a group */ - public void setExpirationTimer(byte[] groupId, int messageExpirationTimer) { + public void setExpirationTimer(GroupId groupId, int messageExpirationTimer) { GroupInfo g = account.getGroupStore().getGroup(groupId); if (g instanceof GroupInfoV1) { GroupInfoV1 groupInfoV1 = (GroupInfoV1) g; @@ -1551,20 +1551,21 @@ public class Manager implements Closeable { if (message.getGroupContext().isPresent()) { if (message.getGroupContext().get().getGroupV1().isPresent()) { SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get(); - GroupInfo group = account.getGroupStore().getGroupByV1Id(groupInfo.getGroupId()); + GroupIdV1 groupId = GroupId.v1(groupInfo.getGroupId()); + GroupInfo group = account.getGroupStore().getGroup(groupId); if (group == null || group instanceof GroupInfoV1) { GroupInfoV1 groupV1 = (GroupInfoV1) group; switch (groupInfo.getType()) { case UPDATE: { if (groupV1 == null) { - groupV1 = new GroupInfoV1(groupInfo.getGroupId()); + groupV1 = new GroupInfoV1(groupId); } if (groupInfo.getAvatar().isPresent()) { SignalServiceAttachment avatar = groupInfo.getAvatar().get(); if (avatar.isPointer()) { try { - retrieveGroupAvatarAttachment(avatar.asPointer(), groupV1.groupId); + retrieveGroupAvatarAttachment(avatar.asPointer(), groupV1.getGroupId()); } catch (IOException | InvalidMessageException | MissingConfigurationException e) { System.err.println("Failed to retrieve group avatar (" + avatar.asPointer() .getRemoteId() + "): " + e.getMessage()); @@ -1589,7 +1590,7 @@ public class Manager implements Closeable { } case DELIVER: if (groupV1 == null && !isSync) { - actions.add(new SendGroupInfoRequestAction(source, groupInfo.getGroupId())); + actions.add(new SendGroupInfoRequestAction(source, groupV1.getGroupId())); } break; case QUIT: { @@ -1601,7 +1602,7 @@ public class Manager implements Closeable { } case REQUEST_INFO: if (groupV1 != null && !isSync) { - actions.add(new SendGroupUpdateAction(source, groupV1.groupId)); + actions.add(new SendGroupUpdateAction(source, groupV1.getGroupId())); } break; } @@ -1627,7 +1628,7 @@ public class Manager implements Closeable { if (message.getGroupContext().isPresent()) { if (message.getGroupContext().get().getGroupV1().isPresent()) { SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get(); - GroupInfoV1 group = account.getGroupStore().getOrCreateGroupV1(groupInfo.getGroupId()); + GroupInfoV1 group = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(groupInfo.getGroupId())); if (group != null) { if (group.messageExpirationTime != message.getExpiresInSeconds()) { group.messageExpirationTime = message.getExpiresInSeconds(); @@ -1723,17 +1724,17 @@ public class Manager implements Closeable { ) { final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); - byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize(); - GroupInfo groupInfo = account.getGroupStore().getGroupByV2Id(groupId); + GroupIdV2 groupId = GroupUtils.getGroupIdV2(groupSecretParams); + GroupInfo groupInfo = account.getGroupStore().getGroup(groupId); final GroupInfoV2 groupInfoV2; if (groupInfo instanceof GroupInfoV1) { // Received a v2 group message for a v1 group, we need to locally migrate the group - account.getGroupStore().deleteGroup(groupInfo.groupId); + account.getGroupStore().deleteGroup(groupInfo.getGroupId()); groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey); System.err.println("Locally migrated group " - + Base64.encodeBytes(groupInfo.groupId) + + groupInfo.getGroupId().toBase64() + " to group v2, id: " - + Base64.encodeBytes(groupInfoV2.groupId) + + groupInfoV2.getGroupId().toBase64() + " !!!"); } else if (groupInfo instanceof GroupInfoV2) { groupInfoV2 = (GroupInfoV2) groupInfo; @@ -1970,19 +1971,14 @@ public class Manager implements Closeable { if (content != null && content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); if (message.getGroupContext().isPresent()) { - GroupInfo group = null; if (message.getGroupContext().get().getGroupV1().isPresent()) { SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get(); - if (groupInfo.getType() == SignalServiceGroup.Type.DELIVER) { - group = getGroup(groupInfo.getGroupId()); + if (groupInfo.getType() != SignalServiceGroup.Type.DELIVER) { + return false; } } - if (message.getGroupContext().get().getGroupV2().isPresent()) { - SignalServiceGroupV2 groupContext = message.getGroupContext().get().getGroupV2().get(); - final GroupMasterKey groupMasterKey = groupContext.getMasterKey(); - byte[] groupId = GroupUtils.getGroupId(groupMasterKey); - group = account.getGroupStore().getGroupByV2Id(groupId); - } + GroupId groupId = GroupUtils.getGroupId(message.getGroupContext().get()); + GroupInfo group = account.getGroupStore().getGroup(groupId); if (group != null && group.isBlocked()) { return true; } @@ -2055,7 +2051,8 @@ public class Manager implements Closeable { DeviceGroupsInputStream s = new DeviceGroupsInputStream(attachmentAsStream); DeviceGroup g; while ((g = s.read()) != null) { - GroupInfoV1 syncGroup = account.getGroupStore().getOrCreateGroupV1(g.getId()); + GroupInfoV1 syncGroup = account.getGroupStore() + .getOrCreateGroupV1(GroupId.v1(g.getId())); if (syncGroup != null) { if (g.getName().isPresent()) { syncGroup.name = g.getName().get(); @@ -2076,7 +2073,7 @@ public class Manager implements Closeable { } if (g.getAvatar().isPresent()) { - retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId); + retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.getGroupId()); } syncGroup.inboxPosition = g.getInboxPosition().orNull(); syncGroup.archived = g.isArchived(); @@ -2104,12 +2101,15 @@ public class Manager implements Closeable { for (SignalServiceAddress address : blockedListMessage.getAddresses()) { setContactBlocked(resolveSignalServiceAddress(address), true); } - for (byte[] groupId : blockedListMessage.getGroupIds()) { + for (GroupId groupId : blockedListMessage.getGroupIds() + .stream() + .map(GroupId::unknownVersion) + .collect(Collectors.toSet())) { try { setGroupBlocked(groupId, true); } catch (GroupNotFoundException e) { System.err.println("BlockedListMessage contained groupID that was not found in GroupStore: " - + Base64.encodeBytes(groupId)); + + groupId.toBase64()); } } } @@ -2229,12 +2229,12 @@ public class Manager implements Closeable { } } - private File getGroupAvatarFile(byte[] groupId) { - return new File(pathConfig.getAvatarsPath(), "group-" + Base64.encodeBytes(groupId).replace("/", "_")); + private File getGroupAvatarFile(GroupId groupId) { + return new File(pathConfig.getAvatarsPath(), "group-" + groupId.toBase64().replace("/", "_")); } private File retrieveGroupAvatarAttachment( - SignalServiceAttachment attachment, byte[] groupId + SignalServiceAttachment attachment, GroupId groupId ) throws IOException, InvalidMessageException, MissingConfigurationException { IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath()); if (attachment.isPointer()) { @@ -2333,10 +2333,10 @@ public class Manager implements Closeable { for (GroupInfo record : account.getGroupStore().getGroups()) { if (record instanceof GroupInfoV1) { GroupInfoV1 groupInfo = (GroupInfoV1) record; - out.write(new DeviceGroup(groupInfo.groupId, + out.write(new DeviceGroup(groupInfo.getGroupId().serialize(), Optional.fromNullable(groupInfo.name), new ArrayList<>(groupInfo.getMembers()), - createGroupAvatarAttachment(groupInfo.groupId), + createGroupAvatarAttachment(groupInfo.getGroupId()), groupInfo.isMember(account.getSelfAddress()), Optional.of(groupInfo.messageExpirationTime), Optional.fromNullable(groupInfo.color), @@ -2442,7 +2442,7 @@ public class Manager implements Closeable { List groupIds = new ArrayList<>(); for (GroupInfo record : account.getGroupStore().getGroups()) { if (record.isBlocked()) { - groupIds.add(record.groupId); + groupIds.add(record.getGroupId().serialize()); } } sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds))); @@ -2466,7 +2466,7 @@ public class Manager implements Closeable { return account.getContactStore().getContact(Util.getSignalServiceAddressFromIdentifier(number)); } - public GroupInfo getGroup(byte[] groupId) { + public GroupInfo getGroup(GroupId groupId) { return account.getGroupStore().getGroup(groupId); } diff --git a/src/main/java/org/asamk/signal/manager/NotAGroupMemberException.java b/src/main/java/org/asamk/signal/manager/NotAGroupMemberException.java index 8c0e9be0..2c9b3f33 100644 --- a/src/main/java/org/asamk/signal/manager/NotAGroupMemberException.java +++ b/src/main/java/org/asamk/signal/manager/NotAGroupMemberException.java @@ -1,10 +1,8 @@ package org.asamk.signal.manager; -import org.whispersystems.util.Base64; - public class NotAGroupMemberException extends Exception { - public NotAGroupMemberException(byte[] groupId, String groupName) { - super("User is not a member in group: " + groupName + " (" + Base64.encodeBytes(groupId) + ")"); + public NotAGroupMemberException(GroupId groupId, String groupName) { + super("User is not a member in group: " + groupName + " (" + groupId.toBase64() + ")"); } } diff --git a/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index d66831bb..6fd35003 100644 --- a/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -2,7 +2,9 @@ package org.asamk.signal.manager.helper; import com.google.protobuf.InvalidProtocolBufferException; +import org.asamk.signal.manager.GroupIdV2; import org.asamk.signal.manager.GroupLinkPassword; +import org.asamk.signal.manager.GroupUtils; import org.asamk.signal.storage.groups.GroupInfoV2; import org.asamk.signal.util.IOUtils; import org.signal.storageservice.protos.groups.AccessControl; @@ -117,7 +119,7 @@ public class GroupHelper { return null; } - final byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize(); + final GroupIdV2 groupId = GroupUtils.getGroupIdV2(groupSecretParams); final GroupMasterKey masterKey = groupSecretParams.getMasterKey(); GroupInfoV2 g = new GroupInfoV2(groupId, masterKey); g.setGroup(decryptedGroup); diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java index 4f9d8628..d3145ecd 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ObjectNode; +import org.asamk.signal.manager.GroupId; import org.asamk.signal.storage.contacts.ContactInfo; import org.asamk.signal.storage.contacts.JsonContactsStore; import org.asamk.signal.storage.groups.GroupInfo; @@ -309,7 +310,7 @@ public class SignalAccount implements Closeable { contactInfo.messageExpirationTime = thread.messageExpirationTime; contactStore.updateContact(contactInfo); } else { - GroupInfo groupInfo = groupStore.getGroup(Base64.decode(thread.id)); + GroupInfo groupInfo = groupStore.getGroup(GroupId.fromBase64(thread.id)); if (groupInfo instanceof GroupInfoV1) { ((GroupInfoV1) groupInfo).messageExpirationTime = thread.messageExpirationTime; groupStore.updateGroup(groupInfo); diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java index 3571985b..40b8c884 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java +++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java @@ -1,8 +1,8 @@ package org.asamk.signal.storage.groups; import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; +import org.asamk.signal.manager.GroupId; import org.asamk.signal.manager.GroupInviteLinkUrl; import org.whispersystems.signalservice.api.push.SignalServiceAddress; @@ -12,12 +12,8 @@ import java.util.stream.Stream; public abstract class GroupInfo { - @JsonProperty - public final byte[] groupId; - - public GroupInfo(byte[] groupId) { - this.groupId = groupId; - } + @JsonIgnore + public abstract GroupId getGroupId(); @JsonIgnore public abstract String getTitle(); diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java b/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java index b06e2436..90b26b81 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java +++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java @@ -13,7 +13,11 @@ import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.asamk.signal.manager.GroupId; +import org.asamk.signal.manager.GroupIdV1; +import org.asamk.signal.manager.GroupIdV2; import org.asamk.signal.manager.GroupInviteLinkUrl; +import org.asamk.signal.manager.GroupUtils; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.io.IOException; @@ -26,8 +30,9 @@ public class GroupInfoV1 extends GroupInfo { private static final ObjectMapper jsonProcessor = new ObjectMapper(); - @JsonProperty - public byte[] expectedV2Id; + private final GroupIdV1 groupId; + + private GroupIdV2 expectedV2Id; @JsonProperty public String name; @@ -47,18 +52,8 @@ public class GroupInfoV1 extends GroupInfo { @JsonProperty(defaultValue = "false") public boolean archived; - public GroupInfoV1(byte[] groupId) { - super(groupId); - } - - @Override - public String getTitle() { - return name; - } - - @Override - public GroupInviteLinkUrl getGroupInviteLink() { - return null; + public GroupInfoV1(GroupIdV1 groupId) { + this.groupId = groupId; } public GroupInfoV1( @@ -74,8 +69,8 @@ public class GroupInfoV1 extends GroupInfo { @JsonProperty("messageExpirationTime") int messageExpirationTime, @JsonProperty("active") boolean _ignored_active ) { - super(groupId); - this.expectedV2Id = expectedV2Id; + this.groupId = GroupId.v1(groupId); + this.expectedV2Id = GroupId.v2(expectedV2Id); this.name = name; this.members.addAll(members); this.color = color; @@ -85,6 +80,40 @@ public class GroupInfoV1 extends GroupInfo { this.messageExpirationTime = messageExpirationTime; } + @Override + @JsonIgnore + public GroupIdV1 getGroupId() { + return groupId; + } + + @JsonProperty("groupId") + private byte[] getGroupIdJackson() { + return groupId.serialize(); + } + + @JsonIgnore + public GroupIdV2 getExpectedV2Id() { + if (expectedV2Id == null) { + expectedV2Id = GroupUtils.getGroupIdV2(groupId); + } + return expectedV2Id; + } + + @JsonProperty("expectedV2Id") + private byte[] getExpectedV2IdJackson() { + return expectedV2Id.serialize(); + } + + @Override + public String getTitle() { + return name; + } + + @Override + public GroupInviteLinkUrl getGroupInviteLink() { + return null; + } + @JsonIgnore public Set getMembers() { return members; diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java b/src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java index 65f8c9a4..1b00caaa 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java +++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java @@ -1,5 +1,6 @@ package org.asamk.signal.storage.groups; +import org.asamk.signal.manager.GroupIdV2; import org.asamk.signal.manager.GroupInviteLinkUrl; import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.local.DecryptedGroup; @@ -13,16 +14,22 @@ import java.util.stream.Collectors; public class GroupInfoV2 extends GroupInfo { + private final GroupIdV2 groupId; private final GroupMasterKey masterKey; private boolean blocked; private DecryptedGroup group; // stored as a file with hexadecimal groupId as name - public GroupInfoV2(final byte[] groupId, final GroupMasterKey masterKey) { - super(groupId); + public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey) { + this.groupId = groupId; this.masterKey = masterKey; } + @Override + public GroupIdV2 getGroupId() { + return groupId; + } + public GroupMasterKey getMasterKey() { return masterKey; } diff --git a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java index 2175e293..d6a99f1c 100644 --- a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java +++ b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java @@ -12,6 +12,9 @@ import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.asamk.signal.manager.GroupId; +import org.asamk.signal.manager.GroupIdV1; +import org.asamk.signal.manager.GroupIdV2; import org.asamk.signal.manager.GroupUtils; import org.asamk.signal.util.Hex; import org.asamk.signal.util.IOUtils; @@ -25,7 +28,6 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -39,7 +41,7 @@ public class JsonGroupStore { @JsonProperty("groups") @JsonSerialize(using = GroupsSerializer.class) @JsonDeserialize(using = GroupsDeserializer.class) - private final Map groups = new HashMap<>(); + private final Map groups = new HashMap<>(); private JsonGroupStore() { } @@ -49,11 +51,11 @@ public class JsonGroupStore { } public void updateGroup(GroupInfo group) { - groups.put(Base64.encodeBytes(group.groupId), group); + groups.put(group.getGroupId(), group); if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() != null) { try { IOUtils.createPrivateDirectories(groupCachePath); - try (FileOutputStream stream = new FileOutputStream(getGroupFile(group.groupId))) { + try (FileOutputStream stream = new FileOutputStream(getGroupFile(group.getGroupId()))) { ((GroupInfoV2) group).getGroup().writeTo(stream); } } catch (IOException e) { @@ -62,57 +64,50 @@ public class JsonGroupStore { } } - public void deleteGroup(byte[] groupId) { - groups.remove(Base64.encodeBytes(groupId)); + public void deleteGroup(GroupId groupId) { + groups.remove(groupId); } - public GroupInfo getGroup(byte[] groupId) { - final GroupInfo group = groups.get(Base64.encodeBytes(groupId)); - if (group == null & groupId.length == 16) { - return getGroupByV1Id(groupId); - } - loadDecryptedGroup(group); - return group; - } - - public GroupInfo getGroupByV1Id(byte[] groupIdV1) { - GroupInfo group = groups.get(Base64.encodeBytes(groupIdV1)); + public GroupInfo getGroup(GroupId groupId) { + GroupInfo group = groups.get(groupId); if (group == null) { - group = groups.get(Base64.encodeBytes(GroupUtils.getGroupId(GroupUtils.deriveV2MigrationMasterKey(groupIdV1)))); + if (groupId instanceof GroupIdV1) { + group = groups.get(GroupUtils.getGroupIdV2((GroupIdV1) groupId)); + } else if (groupId instanceof GroupIdV2) { + group = getGroupV1ByV2Id((GroupIdV2) groupId); + } } loadDecryptedGroup(group); return group; } - public GroupInfo getGroupByV2Id(byte[] groupIdV2) { - GroupInfo group = groups.get(Base64.encodeBytes(groupIdV2)); - if (group == null) { - for (GroupInfo g : groups.values()) { - if (g instanceof GroupInfoV1 && Arrays.equals(groupIdV2, ((GroupInfoV1) g).expectedV2Id)) { - group = g; - break; + private GroupInfoV1 getGroupV1ByV2Id(GroupIdV2 groupIdV2) { + for (GroupInfo g : groups.values()) { + if (g instanceof GroupInfoV1) { + final GroupInfoV1 gv1 = (GroupInfoV1) g; + if (groupIdV2.equals(gv1.getExpectedV2Id())) { + return gv1; } } } - loadDecryptedGroup(group); - return group; + return null; } private void loadDecryptedGroup(final GroupInfo group) { if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) { - try (FileInputStream stream = new FileInputStream(getGroupFile(group.groupId))) { + try (FileInputStream stream = new FileInputStream(getGroupFile(group.getGroupId()))) { ((GroupInfoV2) group).setGroup(DecryptedGroup.parseFrom(stream)); } catch (IOException ignored) { } } } - private File getGroupFile(final byte[] groupId) { - return new File(groupCachePath, Hex.toStringCondensed(groupId)); + private File getGroupFile(final GroupId groupId) { + return new File(groupCachePath, Hex.toStringCondensed(groupId.serialize())); } - public GroupInfoV1 getOrCreateGroupV1(byte[] groupId) { - GroupInfo group = groups.get(Base64.encodeBytes(groupId)); + public GroupInfoV1 getOrCreateGroupV1(GroupIdV1 groupId) { + GroupInfo group = getGroup(groupId); if (group instanceof GroupInfoV1) { return (GroupInfoV1) group; } @@ -146,7 +141,7 @@ public class JsonGroupStore { } else if (group instanceof GroupInfoV2) { final GroupInfoV2 groupV2 = (GroupInfoV2) group; jgen.writeStartObject(); - jgen.writeStringField("groupId", Base64.encodeBytes(groupV2.groupId)); + jgen.writeStringField("groupId", groupV2.getGroupId().toBase64()); jgen.writeStringField("masterKey", Base64.encodeBytes(groupV2.getMasterKey().serialize())); jgen.writeBooleanField("blocked", groupV2.isBlocked()); jgen.writeEndObject(); @@ -158,34 +153,31 @@ public class JsonGroupStore { } } - private static class GroupsDeserializer extends JsonDeserializer> { + private static class GroupsDeserializer extends JsonDeserializer> { @Override - public Map deserialize( + public Map deserialize( JsonParser jsonParser, DeserializationContext deserializationContext ) throws IOException { - Map groups = new HashMap<>(); + Map groups = new HashMap<>(); JsonNode node = jsonParser.getCodec().readTree(jsonParser); for (JsonNode n : node) { GroupInfo g; if (n.has("masterKey")) { // a v2 group - byte[] groupId = Base64.decode(n.get("groupId").asText()); + GroupIdV2 groupId = GroupIdV2.fromBase64(n.get("groupId").asText()); try { GroupMasterKey masterKey = new GroupMasterKey(Base64.decode(n.get("masterKey").asText())); g = new GroupInfoV2(groupId, masterKey); } catch (InvalidInputException e) { - throw new AssertionError("Invalid master key for group " + Base64.encodeBytes(groupId)); + throw new AssertionError("Invalid master key for group " + groupId.toBase64()); } g.setBlocked(n.get("blocked").asBoolean(false)); } else { GroupInfoV1 gv1 = jsonProcessor.treeToValue(n, GroupInfoV1.class); - if (gv1.expectedV2Id == null) { - gv1.expectedV2Id = GroupUtils.getGroupId(GroupUtils.deriveV2MigrationMasterKey(gv1.groupId)); - } g = gv1; } - groups.put(Base64.encodeBytes(g.groupId), g); + groups.put(g.getGroupId(), g); } return groups; diff --git a/src/main/java/org/asamk/signal/util/ErrorUtils.java b/src/main/java/org/asamk/signal/util/ErrorUtils.java index 8e65d440..44d505be 100644 --- a/src/main/java/org/asamk/signal/util/ErrorUtils.java +++ b/src/main/java/org/asamk/signal/util/ErrorUtils.java @@ -1,5 +1,6 @@ package org.asamk.signal.util; +import org.asamk.signal.manager.GroupIdFormatException; import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.NotAGroupMemberException; import org.whispersystems.signalservice.api.messages.SendMessageResult; diff --git a/src/main/java/org/asamk/signal/util/GroupIdFormatException.java b/src/main/java/org/asamk/signal/util/GroupIdFormatException.java deleted file mode 100644 index 5a5c4570..00000000 --- a/src/main/java/org/asamk/signal/util/GroupIdFormatException.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.asamk.signal.util; - -import java.io.IOException; - -public class GroupIdFormatException extends Exception { - - public GroupIdFormatException(String groupId, IOException e) { - super("Failed to decode groupId (must be base64) \"" + groupId + "\": " + e.getMessage()); - } -} diff --git a/src/main/java/org/asamk/signal/util/Util.java b/src/main/java/org/asamk/signal/util/Util.java index bc2d3377..3cd5619a 100644 --- a/src/main/java/org/asamk/signal/util/Util.java +++ b/src/main/java/org/asamk/signal/util/Util.java @@ -2,13 +2,13 @@ package org.asamk.signal.util; import com.fasterxml.jackson.databind.JsonNode; +import org.asamk.signal.manager.GroupId; +import org.asamk.signal.manager.GroupIdFormatException; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.api.util.UuidUtil; -import org.whispersystems.util.Base64; -import java.io.IOException; import java.io.InvalidObjectException; public class Util { @@ -48,12 +48,8 @@ public class Util { return node; } - public static byte[] decodeGroupId(String groupId) throws GroupIdFormatException { - try { - return Base64.decode(groupId); - } catch (IOException e) { - throw new GroupIdFormatException(groupId, e); - } + public static GroupId decodeGroupId(String groupId) throws GroupIdFormatException { + return GroupId.fromBase64(groupId); } public static String canonicalizeNumber(String number, String localNumber) throws InvalidNumberException { -- 2.50.1