Refactor to use GroupId class to wrap the byte array

Helps distinguish between group v1 and v2 ids
This commit is contained in:
AsamK 2020-12-24 16:36:47 +01:00
parent 67f62947c6
commit 9942d967a4
31 changed files with 358 additions and 228 deletions

View file

@ -65,7 +65,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
} else if (content.getDataMessage().isPresent()) { } else if (content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get(); SignalServiceDataMessage message = content.getDataMessage().get();
byte[] groupId = getGroupId(m, message); byte[] groupId = getGroupId(message);
if (!message.isEndSession() && ( if (!message.isEndSession() && (
groupId == null groupId == null
|| message.getGroupContext().get().getGroupV1Type() == null || message.getGroupContext().get().getGroupV1Type() == null
@ -91,7 +91,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
.getGroupContext() .getGroupContext()
.isPresent()) { .isPresent()) {
SignalServiceDataMessage message = transcript.getMessage(); SignalServiceDataMessage message = transcript.getMessage();
byte[] groupId = getGroupId(m, message); byte[] groupId = getGroupId(message);
try { try {
conn.sendMessage(new Signal.SyncMessageReceived(objectPath, 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) { private static byte[] getGroupId(final SignalServiceDataMessage message) {
byte[] groupId; return message.getGroupContext().isPresent() ? GroupUtils.getGroupId(message.getGroupContext().get())
if (message.getGroupContext().isPresent()) { .serialize() : null;
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;
} }
static private List<String> getAttachments(SignalServiceDataMessage message, Manager m) { static private List<String> getAttachments(SignalServiceDataMessage message, Manager m) {

View file

@ -1,5 +1,6 @@
package org.asamk.signal; package org.asamk.signal;
import org.asamk.signal.manager.GroupId;
import org.asamk.signal.manager.GroupUtils; import org.asamk.signal.manager.GroupUtils;
import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.Manager;
import org.asamk.signal.storage.contacts.ContactInfo; 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())); System.out.println(" - Timestamp: " + DateUtils.formatTimestamp(typingMessage.getTimestamp()));
if (typingMessage.getGroupId().isPresent()) { if (typingMessage.getGroupId().isPresent()) {
System.out.println(" - Group Info:"); System.out.println(" - Group Info:");
System.out.println(" Id: " + Base64.encodeBytes(typingMessage.getGroupId().get())); final GroupId groupId = GroupId.unknownVersion(typingMessage.getGroupId().get());
GroupInfo group = m.getGroup(typingMessage.getGroupId().get()); System.out.println(" Id: " + groupId.toBase64());
GroupInfo group = m.getGroup(groupId);
if (group != null) { if (group != null) {
System.out.println(" Name: " + group.getTitle()); System.out.println(" Name: " + group.getTitle());
} else { } else {
@ -356,13 +358,14 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
if (message.getGroupContext().isPresent()) { if (message.getGroupContext().isPresent()) {
System.out.println("Group info:"); System.out.println("Group info:");
final SignalServiceGroupContext groupContext = message.getGroupContext().get(); final SignalServiceGroupContext groupContext = message.getGroupContext().get();
final GroupId groupId = GroupUtils.getGroupId(groupContext);
if (groupContext.getGroupV1().isPresent()) { if (groupContext.getGroupV1().isPresent()) {
SignalServiceGroup groupInfo = groupContext.getGroupV1().get(); 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()) { if (groupInfo.getType() == SignalServiceGroup.Type.UPDATE && groupInfo.getName().isPresent()) {
System.out.println(" Name: " + groupInfo.getName().get()); System.out.println(" Name: " + groupInfo.getName().get());
} else { } else {
GroupInfo group = m.getGroup(groupInfo.getGroupId()); GroupInfo group = m.getGroup(groupId);
if (group != null) { if (group != null) {
System.out.println(" Name: " + group.getTitle()); System.out.println(" Name: " + group.getTitle());
} else { } else {
@ -381,8 +384,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
} }
} else if (groupContext.getGroupV2().isPresent()) { } else if (groupContext.getGroupV2().isPresent()) {
final SignalServiceGroupV2 groupInfo = groupContext.getGroupV2().get(); final SignalServiceGroupV2 groupInfo = groupContext.getGroupV2().get();
byte[] groupId = GroupUtils.getGroupId(groupInfo.getMasterKey()); System.out.println(" Id: " + groupId.toBase64());
System.out.println(" Id: " + Base64.encodeBytes(groupId));
GroupInfo group = m.getGroup(groupId); GroupInfo group = m.getGroup(groupId);
if (group != null) { if (group != null) {
System.out.println(" Name: " + group.getTitle()); System.out.println(" Name: " + group.getTitle());

View file

@ -3,9 +3,10 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser; 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.GroupNotFoundException;
import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.Manager;
import org.asamk.signal.util.GroupIdFormatException;
import org.asamk.signal.util.Util; import org.asamk.signal.util.Util;
import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.InvalidNumberException;
@ -36,7 +37,7 @@ public class BlockCommand implements LocalCommand {
if (ns.<String>getList("group") != null) { if (ns.<String>getList("group") != null) {
for (String groupIdString : ns.<String>getList("group")) { for (String groupIdString : ns.<String>getList("group")) {
try { try {
byte[] groupId = Util.decodeGroupId(groupIdString); GroupId groupId = Util.decodeGroupId(groupIdString);
m.setGroupBlocked(groupId, true); m.setGroupBlocked(groupId, true);
} catch (GroupIdFormatException | GroupNotFoundException e) { } catch (GroupIdFormatException | GroupNotFoundException e) {
System.err.println(e.getMessage()); System.err.println(e.getMessage());

View file

@ -4,6 +4,7 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser; import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.Signal; import org.asamk.Signal;
import org.asamk.signal.manager.GroupId;
import org.asamk.signal.manager.GroupInviteLinkUrl; import org.asamk.signal.manager.GroupInviteLinkUrl;
import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.Manager;
import org.freedesktop.dbus.exceptions.DBusExecutionException; 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.groupsv2.GroupLinkNotActiveException;
import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.internal.push.exceptions.GroupPatchNotAcceptedException; import org.whispersystems.signalservice.internal.push.exceptions.GroupPatchNotAcceptedException;
import org.whispersystems.util.Base64;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
@ -52,12 +52,12 @@ public class JoinGroupCommand implements LocalCommand {
} }
try { try {
final Pair<byte[], List<SendMessageResult>> results = m.joinGroup(linkUrl); final Pair<GroupId, List<SendMessageResult>> results = m.joinGroup(linkUrl);
byte[] newGroupId = results.first(); GroupId newGroupId = results.first();
if (!m.getGroup(newGroupId).isMember(m.getSelfAddress())) { 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 { } else {
System.out.println("Joined group \"" + Base64.encodeBytes(newGroupId) + "\""); System.out.println("Joined group \"" + newGroupId.toBase64() + "\"");
} }
return handleTimestampAndSendMessageResults(0, results.second()); return handleTimestampAndSendMessageResults(0, results.second());
} catch (AssertionError e) { } catch (AssertionError e) {

View file

@ -8,7 +8,6 @@ import org.asamk.signal.manager.GroupInviteLinkUrl;
import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.Manager;
import org.asamk.signal.storage.groups.GroupInfo; import org.asamk.signal.storage.groups.GroupInfo;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.util.Base64;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -40,7 +39,7 @@ public class ListGroupsCommand implements LocalCommand {
System.out.println(String.format( System.out.println(String.format(
"Id: %s Name: %s Active: %s Blocked: %b Members: %s Pending members: %s Requesting members: %s Link: %s", "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.getTitle(),
group.isMember(m.getSelfAddress()), group.isMember(m.getSelfAddress()),
group.isBlocked(), group.isBlocked(),
@ -50,7 +49,7 @@ public class ListGroupsCommand implements LocalCommand {
groupInviteLink == null ? '-' : groupInviteLink.getUrl())); groupInviteLink == null ? '-' : groupInviteLink.getUrl()));
} else { } else {
System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b", System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b",
Base64.encodeBytes(group.groupId), group.getGroupId().toBase64(),
group.getTitle(), group.getTitle(),
group.isMember(m.getSelfAddress()), group.isMember(m.getSelfAddress()),
group.isBlocked())); group.isBlocked()));

View file

@ -3,10 +3,11 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser; 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.GroupNotFoundException;
import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.NotAGroupMemberException; import org.asamk.signal.manager.NotAGroupMemberException;
import org.asamk.signal.util.GroupIdFormatException;
import org.asamk.signal.util.Util; import org.asamk.signal.util.Util;
import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SendMessageResult;
@ -36,7 +37,7 @@ public class QuitGroupCommand implements LocalCommand {
} }
try { try {
final byte[] groupId = Util.decodeGroupId(ns.getString("group")); final GroupId groupId = Util.decodeGroupId(ns.getString("group"));
final Pair<Long, List<SendMessageResult>> results = m.sendQuitGroupMessage(groupId); final Pair<Long, List<SendMessageResult>> results = m.sendQuitGroupMessage(groupId);
return handleTimestampAndSendMessageResults(results.first(), results.second()); return handleTimestampAndSendMessageResults(results.first(), results.second());
} catch (IOException e) { } catch (IOException e) {

View file

@ -5,7 +5,7 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser; import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.Signal; 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.IOUtils;
import org.asamk.signal.util.Util; import org.asamk.signal.util.Util;
import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.freedesktop.dbus.exceptions.DBusExecutionException;
@ -79,7 +79,7 @@ public class SendCommand implements DbusCommand {
if (ns.getString("group") != null) { if (ns.getString("group") != null) {
byte[] groupId; byte[] groupId;
try { try {
groupId = Util.decodeGroupId(ns.getString("group")); groupId = Util.decodeGroupId(ns.getString("group")).serialize();
} catch (GroupIdFormatException e) { } catch (GroupIdFormatException e) {
handleGroupIdFormatException(e); handleGroupIdFormatException(e);
return 1; return 1;

View file

@ -4,10 +4,11 @@ import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser; 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.GroupNotFoundException;
import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.NotAGroupMemberException; import org.asamk.signal.manager.NotAGroupMemberException;
import org.asamk.signal.util.GroupIdFormatException;
import org.asamk.signal.util.Util; import org.asamk.signal.util.Util;
import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SendMessageResult;
@ -65,7 +66,7 @@ public class SendReactionCommand implements LocalCommand {
try { try {
final Pair<Long, List<SendMessageResult>> results; final Pair<Long, List<SendMessageResult>> results;
if (ns.getString("group") != null) { 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); results = m.sendGroupMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, groupId);
} else { } else {
results = m.sendMessageReaction(emoji, results = m.sendMessageReaction(emoji,

View file

@ -3,9 +3,10 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser; 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.GroupNotFoundException;
import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.Manager;
import org.asamk.signal.util.GroupIdFormatException;
import org.asamk.signal.util.Util; import org.asamk.signal.util.Util;
import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.InvalidNumberException;
@ -36,7 +37,7 @@ public class UnblockCommand implements LocalCommand {
if (ns.<String>getList("group") != null) { if (ns.<String>getList("group") != null) {
for (String groupIdString : ns.<String>getList("group")) { for (String groupIdString : ns.<String>getList("group")) {
try { try {
byte[] groupId = Util.decodeGroupId(groupIdString); GroupId groupId = Util.decodeGroupId(groupIdString);
m.setGroupBlocked(groupId, false); m.setGroupBlocked(groupId, false);
} catch (GroupIdFormatException | GroupNotFoundException e) { } catch (GroupIdFormatException | GroupNotFoundException e) {
System.err.println(e.getMessage()); System.err.println(e.getMessage());

View file

@ -4,7 +4,7 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser; import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.Signal; import org.asamk.Signal;
import org.asamk.signal.util.GroupIdFormatException; import org.asamk.signal.manager.GroupIdFormatException;
import org.asamk.signal.util.Util; import org.asamk.signal.util.Util;
import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.whispersystems.util.Base64; import org.whispersystems.util.Base64;
@ -35,7 +35,7 @@ public class UpdateGroupCommand implements DbusCommand {
byte[] groupId = null; byte[] groupId = null;
if (ns.getString("group") != null) { if (ns.getString("group") != null) {
try { try {
groupId = Util.decodeGroupId(ns.getString("group")); groupId = Util.decodeGroupId(ns.getString("group")).serialize();
} catch (GroupIdFormatException e) { } catch (GroupIdFormatException e) {
handleGroupIdFormatException(e); handleGroupIdFormatException(e);
return 1; return 1;

View file

@ -2,6 +2,7 @@ package org.asamk.signal.dbus;
import org.asamk.Signal; import org.asamk.Signal;
import org.asamk.signal.manager.AttachmentInvalidException; import org.asamk.signal.manager.AttachmentInvalidException;
import org.asamk.signal.manager.GroupId;
import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.GroupNotFoundException;
import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.NotAGroupMemberException; import org.asamk.signal.manager.NotAGroupMemberException;
@ -92,7 +93,9 @@ public class DbusSignalImpl implements Signal {
@Override @Override
public long sendGroupMessage(final String message, final List<String> attachments, final byte[] groupId) { public long sendGroupMessage(final String message, final List<String> attachments, final byte[] groupId) {
try { try {
Pair<Long, List<SendMessageResult>> results = m.sendGroupMessage(message, attachments, groupId); Pair<Long, List<SendMessageResult>> results = m.sendGroupMessage(message,
attachments,
GroupId.unknownVersion(groupId));
checkSendMessageResults(results.first(), results.second()); checkSendMessageResults(results.first(), results.second());
return results.first(); return results.first();
} catch (IOException e) { } catch (IOException e) {
@ -134,7 +137,7 @@ public class DbusSignalImpl implements Signal {
@Override @Override
public void setGroupBlocked(final byte[] groupId, final boolean blocked) { public void setGroupBlocked(final byte[] groupId, final boolean blocked) {
try { try {
m.setGroupBlocked(groupId, blocked); m.setGroupBlocked(GroupId.unknownVersion(groupId), blocked);
} catch (GroupNotFoundException e) { } catch (GroupNotFoundException e) {
throw new Error.GroupNotFound(e.getMessage()); throw new Error.GroupNotFound(e.getMessage());
} }
@ -145,14 +148,14 @@ public class DbusSignalImpl implements Signal {
List<GroupInfo> groups = m.getGroups(); List<GroupInfo> groups = m.getGroups();
List<byte[]> ids = new ArrayList<>(groups.size()); List<byte[]> ids = new ArrayList<>(groups.size());
for (GroupInfo group : groups) { for (GroupInfo group : groups) {
ids.add(group.groupId); ids.add(group.getGroupId().serialize());
} }
return ids; return ids;
} }
@Override @Override
public String getGroupName(final byte[] groupId) { public String getGroupName(final byte[] groupId) {
GroupInfo group = m.getGroup(groupId); GroupInfo group = m.getGroup(GroupId.unknownVersion(groupId));
if (group == null) { if (group == null) {
return ""; return "";
} else { } else {
@ -162,7 +165,7 @@ public class DbusSignalImpl implements Signal {
@Override @Override
public List<String> getGroupMembers(final byte[] groupId) { public List<String> getGroupMembers(final byte[] groupId) {
GroupInfo group = m.getGroup(groupId); GroupInfo group = m.getGroup(GroupId.unknownVersion(groupId));
if (group == null) { if (group == null) {
return Collections.emptyList(); return Collections.emptyList();
} else { } else {
@ -189,9 +192,11 @@ public class DbusSignalImpl implements Signal {
if (avatar.isEmpty()) { if (avatar.isEmpty()) {
avatar = null; avatar = null;
} }
final Pair<byte[], List<SendMessageResult>> results = m.updateGroup(groupId, name, members, avatar); final Pair<GroupId, List<SendMessageResult>> results = m.updateGroup(groupId == null
? null
: GroupId.unknownVersion(groupId), name, members, avatar);
checkSendMessageResults(0, results.second()); checkSendMessageResults(0, results.second());
return results.first(); return results.first().serialize();
} catch (IOException e) { } catch (IOException e) {
throw new Error.Failure(e.getMessage()); throw new Error.Failure(e.getMessage());
} catch (GroupNotFoundException | NotAGroupMemberException e) { } catch (GroupNotFoundException | NotAGroupMemberException e) {

View file

@ -31,7 +31,7 @@ class JsonGroupInfo {
} }
JsonGroupInfo(SignalServiceGroupV2 groupInfo) { JsonGroupInfo(SignalServiceGroupV2 groupInfo) {
this.groupId = Base64.encodeBytes(GroupUtils.getGroupId(groupInfo.getMasterKey())); this.groupId = GroupUtils.getGroupIdV2(groupInfo.getMasterKey()).toBase64();
this.type = groupInfo.hasSignedGroupChange() ? "UPDATE" : "DELIVER"; this.type = groupInfo.hasSignedGroupChange() ? "UPDATE" : "DELIVER";
} }

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -1,10 +1,8 @@
package org.asamk.signal.manager; package org.asamk.signal.manager;
import org.whispersystems.util.Base64;
public class GroupNotFoundException extends Exception { public class GroupNotFoundException extends Exception {
public GroupNotFoundException(byte[] groupId) { public GroupNotFoundException(GroupId groupId) {
super("Group not found: " + Base64.encodeBytes(groupId)); super("Group not found: " + groupId.toBase64());
} }
} }

View file

@ -9,6 +9,7 @@ import org.signal.zkgroup.groups.GroupSecretParams;
import org.whispersystems.libsignal.kdf.HKDFv3; import org.whispersystems.libsignal.kdf.HKDFv3;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup; 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.SignalServiceGroupV2;
public class GroupUtils { public class GroupUtils {
@ -18,7 +19,7 @@ public class GroupUtils {
) { ) {
if (groupInfo instanceof GroupInfoV1) { if (groupInfo instanceof GroupInfoV1) {
SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER) SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)
.withId(groupInfo.groupId) .withId(groupInfo.getGroupId().serialize())
.build(); .build();
messageBuilder.asGroupMessage(group); messageBuilder.asGroupMessage(group);
} else { } else {
@ -30,14 +31,34 @@ public class GroupUtils {
} }
} }
public static byte[] getGroupId(GroupMasterKey groupMasterKey) { public static GroupId getGroupId(SignalServiceGroupContext context) {
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); if (context.getGroupV1().isPresent()) {
return groupSecretParams.getPublicParams().getGroupIdentifier().serialize(); return GroupId.v1(context.getGroupV1().get().getGroupId());
} else if (context.getGroupV2().isPresent()) {
return getGroupIdV2(context.getGroupV2().get().getMasterKey());
} else {
return null;
}
} }
public static GroupMasterKey deriveV2MigrationMasterKey(byte[] groupIdV1) { 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 getGroupIdV2(groupSecretParams);
}
public static GroupIdV2 getGroupIdV2(GroupIdV1 groupIdV1) {
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(deriveV2MigrationMasterKey(
groupIdV1));
return getGroupIdV2(groupSecretParams);
}
private static GroupMasterKey deriveV2MigrationMasterKey(GroupIdV1 groupIdV1) {
try { try {
return new GroupMasterKey(new HKDFv3().deriveSecrets(groupIdV1, return new GroupMasterKey(new HKDFv3().deriveSecrets(groupIdV1.serialize(),
"GV2 Migration".getBytes(), "GV2 Migration".getBytes(),
GroupMasterKey.SIZE)); GroupMasterKey.SIZE));
} catch (InvalidInputException e) { } catch (InvalidInputException e) {

View file

@ -2,7 +2,6 @@ package org.asamk.signal.manager;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.Arrays;
import java.util.Objects; import java.util.Objects;
interface HandleAction { interface HandleAction {
@ -93,9 +92,9 @@ class SendSyncBlockedListAction implements HandleAction {
class SendGroupInfoRequestAction implements HandleAction { class SendGroupInfoRequestAction implements HandleAction {
private final SignalServiceAddress address; 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.address = address;
this.groupId = groupId; this.groupId = groupId;
} }
@ -109,14 +108,17 @@ class SendGroupInfoRequestAction implements HandleAction {
public boolean equals(final Object o) { public boolean equals(final Object o) {
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
final SendGroupInfoRequestAction that = (SendGroupInfoRequestAction) o; 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 @Override
public int hashCode() { public int hashCode() {
int result = Objects.hash(address); int result = address.hashCode();
result = 31 * result + Arrays.hashCode(groupId); result = 31 * result + groupId.hashCode();
return result; return result;
} }
} }
@ -124,9 +126,9 @@ class SendGroupInfoRequestAction implements HandleAction {
class SendGroupUpdateAction implements HandleAction { class SendGroupUpdateAction implements HandleAction {
private final SignalServiceAddress address; 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.address = address;
this.groupId = groupId; this.groupId = groupId;
} }
@ -140,14 +142,17 @@ class SendGroupUpdateAction implements HandleAction {
public boolean equals(final Object o) { public boolean equals(final Object o) {
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
final SendGroupUpdateAction that = (SendGroupUpdateAction) o; 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 @Override
public int hashCode() { public int hashCode() {
int result = Objects.hash(address); int result = address.hashCode();
result = 31 * result + Arrays.hashCode(groupId); result = 31 * result + groupId.hashCode();
return result; return result;
} }
} }

View file

@ -26,10 +26,6 @@ class KeyUtils {
return getSecret(18); return getSecret(18);
} }
static byte[] createGroupId() {
return getSecretBytes(16);
}
static byte[] createStickerUploadKey() { static byte[] createStickerUploadKey() {
return getSecretBytes(32); return getSecretBytes(32);
} }

View file

@ -679,7 +679,7 @@ public class Manager implements Closeable {
} }
} }
private Optional<SignalServiceAttachmentStream> createGroupAvatarAttachment(byte[] groupId) throws IOException { private Optional<SignalServiceAttachmentStream> createGroupAvatarAttachment(GroupId groupId) throws IOException {
File file = getGroupAvatarFile(groupId); File file = getGroupAvatarFile(groupId);
if (!file.exists()) { if (!file.exists()) {
return Optional.absent(); return Optional.absent();
@ -697,7 +697,7 @@ public class Manager implements Closeable {
return Optional.of(Utils.createAttachment(file)); 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); GroupInfo g = account.getGroupStore().getGroup(groupId);
if (g == null) { if (g == null) {
throw new GroupNotFoundException(groupId); throw new GroupNotFoundException(groupId);
@ -708,7 +708,7 @@ public class Manager implements Closeable {
return g; return g;
} }
private GroupInfo getGroupForUpdating(byte[] groupId) throws GroupNotFoundException, NotAGroupMemberException { private GroupInfo getGroupForUpdating(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
GroupInfo g = account.getGroupStore().getGroup(groupId); GroupInfo g = account.getGroupStore().getGroup(groupId);
if (g == null) { if (g == null) {
throw new GroupNotFoundException(groupId); throw new GroupNotFoundException(groupId);
@ -724,7 +724,7 @@ public class Manager implements Closeable {
} }
public Pair<Long, List<SendMessageResult>> sendGroupMessage( public Pair<Long, List<SendMessageResult>> sendGroupMessage(
SignalServiceDataMessage.Builder messageBuilder, byte[] groupId SignalServiceDataMessage.Builder messageBuilder, GroupId groupId
) throws IOException, GroupNotFoundException, NotAGroupMemberException { ) throws IOException, GroupNotFoundException, NotAGroupMemberException {
final GroupInfo g = getGroupForSending(groupId); final GroupInfo g = getGroupForSending(groupId);
@ -735,7 +735,7 @@ public class Manager implements Closeable {
} }
public Pair<Long, List<SendMessageResult>> sendGroupMessage( public Pair<Long, List<SendMessageResult>> sendGroupMessage(
String messageText, List<String> attachments, byte[] groupId String messageText, List<String> attachments, GroupId groupId
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
.withBody(messageText); .withBody(messageText);
@ -747,7 +747,7 @@ public class Manager implements Closeable {
} }
public Pair<Long, List<SendMessageResult>> sendGroupMessageReaction( public Pair<Long, List<SendMessageResult>> 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 { ) throws IOException, InvalidNumberException, NotAGroupMemberException, GroupNotFoundException {
SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji,
remove, remove,
@ -759,7 +759,7 @@ public class Manager implements Closeable {
return sendGroupMessage(messageBuilder, groupId); return sendGroupMessage(messageBuilder, groupId);
} }
public Pair<Long, List<SendMessageResult>> sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, NotAGroupMemberException { public Pair<Long, List<SendMessageResult>> sendQuitGroupMessage(GroupId groupId) throws GroupNotFoundException, IOException, NotAGroupMemberException {
SignalServiceDataMessage.Builder messageBuilder; SignalServiceDataMessage.Builder messageBuilder;
@ -767,7 +767,7 @@ public class Manager implements Closeable {
if (g instanceof GroupInfoV1) { if (g instanceof GroupInfoV1) {
GroupInfoV1 groupInfoV1 = (GroupInfoV1) g; GroupInfoV1 groupInfoV1 = (GroupInfoV1) g;
SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT) SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
.withId(groupId) .withId(groupId.serialize())
.build(); .build();
messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group); messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group);
groupInfoV1.removeMember(account.getSelfAddress()); groupInfoV1.removeMember(account.getSelfAddress());
@ -783,8 +783,8 @@ public class Manager implements Closeable {
return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress())); return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
} }
private Pair<byte[], List<SendMessageResult>> sendUpdateGroupMessage( private Pair<GroupId, List<SendMessageResult>> sendUpdateGroupMessage(
byte[] groupId, String name, Collection<SignalServiceAddress> members, String avatarFile GroupId groupId, String name, Collection<SignalServiceAddress> members, String avatarFile
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
GroupInfo g; GroupInfo g;
SignalServiceDataMessage.Builder messageBuilder; SignalServiceDataMessage.Builder messageBuilder;
@ -792,7 +792,7 @@ public class Manager implements Closeable {
// Create new group // Create new group
GroupInfoV2 gv2 = groupHelper.createGroupV2(name, members, avatarFile); GroupInfoV2 gv2 = groupHelper.createGroupV2(name, members, avatarFile);
if (gv2 == null) { if (gv2 == null) {
GroupInfoV1 gv1 = new GroupInfoV1(KeyUtils.createGroupId()); GroupInfoV1 gv1 = new GroupInfoV1(GroupIdV1.createRandom());
gv1.addMembers(Collections.singleton(account.getSelfAddress())); gv1.addMembers(Collections.singleton(account.getSelfAddress()));
updateGroupV1(gv1, name, members, avatarFile); updateGroupV1(gv1, name, members, avatarFile);
messageBuilder = getGroupUpdateMessageBuilder(gv1); messageBuilder = getGroupUpdateMessageBuilder(gv1);
@ -834,7 +834,7 @@ public class Manager implements Closeable {
groupGroupChangePair.second()); groupGroupChangePair.second());
} }
return new Pair<>(group.groupId, result.second()); return new Pair<>(group.getGroupId(), result.second());
} else { } else {
GroupInfoV1 gv1 = (GroupInfoV1) group; GroupInfoV1 gv1 = (GroupInfoV1) group;
updateGroupV1(gv1, name, members, avatarFile); updateGroupV1(gv1, name, members, avatarFile);
@ -847,16 +847,16 @@ public class Manager implements Closeable {
final Pair<Long, List<SendMessageResult>> result = sendMessage(messageBuilder, final Pair<Long, List<SendMessageResult>> result = sendMessage(messageBuilder,
g.getMembersIncludingPendingWithout(account.getSelfAddress())); g.getMembersIncludingPendingWithout(account.getSelfAddress()));
return new Pair<>(g.groupId, result.second()); return new Pair<>(g.getGroupId(), result.second());
} }
public Pair<byte[], List<SendMessageResult>> joinGroup( public Pair<GroupId, List<SendMessageResult>> joinGroup(
GroupInviteLinkUrl inviteLinkUrl GroupInviteLinkUrl inviteLinkUrl
) throws IOException, GroupLinkNotActiveException { ) throws IOException, GroupLinkNotActiveException {
return sendJoinGroupMessage(inviteLinkUrl); return sendJoinGroupMessage(inviteLinkUrl);
} }
private Pair<byte[], List<SendMessageResult>> sendJoinGroupMessage( private Pair<GroupId, List<SendMessageResult>> sendJoinGroupMessage(
GroupInviteLinkUrl inviteLinkUrl GroupInviteLinkUrl inviteLinkUrl
) throws IOException, GroupLinkNotActiveException { ) throws IOException, GroupLinkNotActiveException {
final DecryptedGroupJoinInfo groupJoinInfo = groupHelper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(), final DecryptedGroupJoinInfo groupJoinInfo = groupHelper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(),
@ -870,12 +870,12 @@ public class Manager implements Closeable {
if (group.getGroup() == null) { if (group.getGroup() == null) {
// Only requested member, can't send update to group members // 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<Long, List<SendMessageResult>> result = sendUpdateGroupMessage(group, group.getGroup(), groupChange); final Pair<Long, List<SendMessageResult>> result = sendUpdateGroupMessage(group, group.getGroup(), groupChange);
return new Pair<>(group.groupId, result.second()); return new Pair<>(group.getGroupId(), result.second());
} }
private Pair<Long, List<SendMessageResult>> sendUpdateGroupMessage( private Pair<Long, List<SendMessageResult>> sendUpdateGroupMessage(
@ -923,13 +923,13 @@ public class Manager implements Closeable {
if (avatarFile != null) { if (avatarFile != null) {
IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath()); IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath());
File aFile = getGroupAvatarFile(g.groupId); File aFile = getGroupAvatarFile(g.getGroupId());
Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING); Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
} }
} }
Pair<Long, List<SendMessageResult>> sendUpdateGroupMessage( Pair<Long, List<SendMessageResult>> sendUpdateGroupMessage(
byte[] groupId, SignalServiceAddress recipient GroupIdV1 groupId, SignalServiceAddress recipient
) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException { ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException {
GroupInfoV1 g; GroupInfoV1 g;
GroupInfo group = getGroupForSending(groupId); GroupInfo group = getGroupForSending(groupId);
@ -950,11 +950,11 @@ public class Manager implements Closeable {
private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException { private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException {
SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE) SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
.withId(g.groupId) .withId(g.getGroupId().serialize())
.withName(g.name) .withName(g.name)
.withMembers(new ArrayList<>(g.getMembers())); .withMembers(new ArrayList<>(g.getMembers()));
File aFile = getGroupAvatarFile(g.groupId); File aFile = getGroupAvatarFile(g.getGroupId());
if (aFile.exists()) { if (aFile.exists()) {
try { try {
group.withAvatar(Utils.createAttachment(aFile)); group.withAvatar(Utils.createAttachment(aFile));
@ -978,10 +978,10 @@ public class Manager implements Closeable {
} }
Pair<Long, List<SendMessageResult>> sendGroupInfoRequest( Pair<Long, List<SendMessageResult>> sendGroupInfoRequest(
byte[] groupId, SignalServiceAddress recipient GroupIdV1 groupId, SignalServiceAddress recipient
) throws IOException { ) throws IOException {
SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO) SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO)
.withId(groupId); .withId(groupId.serialize());
SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
.asGroupMessage(group.build()); .asGroupMessage(group.build());
@ -1087,7 +1087,7 @@ public class Manager implements Closeable {
account.save(); 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); GroupInfo group = getGroup(groupId);
if (group == null) { if (group == null) {
throw new GroupNotFoundException(groupId); throw new GroupNotFoundException(groupId);
@ -1098,8 +1098,8 @@ public class Manager implements Closeable {
account.save(); account.save();
} }
public Pair<byte[], List<SendMessageResult>> updateGroup( public Pair<GroupId, List<SendMessageResult>> updateGroup(
byte[] groupId, String name, List<String> members, String avatar GroupId groupId, String name, List<String> members, String avatar
) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException { ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException {
return sendUpdateGroupMessage(groupId, return sendUpdateGroupMessage(groupId,
name, name,
@ -1137,7 +1137,7 @@ public class Manager implements Closeable {
/** /**
* Change the expiration timer for a group * 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); GroupInfo g = account.getGroupStore().getGroup(groupId);
if (g instanceof GroupInfoV1) { if (g instanceof GroupInfoV1) {
GroupInfoV1 groupInfoV1 = (GroupInfoV1) g; GroupInfoV1 groupInfoV1 = (GroupInfoV1) g;
@ -1551,20 +1551,21 @@ public class Manager implements Closeable {
if (message.getGroupContext().isPresent()) { if (message.getGroupContext().isPresent()) {
if (message.getGroupContext().get().getGroupV1().isPresent()) { if (message.getGroupContext().get().getGroupV1().isPresent()) {
SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get(); 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) { if (group == null || group instanceof GroupInfoV1) {
GroupInfoV1 groupV1 = (GroupInfoV1) group; GroupInfoV1 groupV1 = (GroupInfoV1) group;
switch (groupInfo.getType()) { switch (groupInfo.getType()) {
case UPDATE: { case UPDATE: {
if (groupV1 == null) { if (groupV1 == null) {
groupV1 = new GroupInfoV1(groupInfo.getGroupId()); groupV1 = new GroupInfoV1(groupId);
} }
if (groupInfo.getAvatar().isPresent()) { if (groupInfo.getAvatar().isPresent()) {
SignalServiceAttachment avatar = groupInfo.getAvatar().get(); SignalServiceAttachment avatar = groupInfo.getAvatar().get();
if (avatar.isPointer()) { if (avatar.isPointer()) {
try { try {
retrieveGroupAvatarAttachment(avatar.asPointer(), groupV1.groupId); retrieveGroupAvatarAttachment(avatar.asPointer(), groupV1.getGroupId());
} catch (IOException | InvalidMessageException | MissingConfigurationException e) { } catch (IOException | InvalidMessageException | MissingConfigurationException e) {
System.err.println("Failed to retrieve group avatar (" + avatar.asPointer() System.err.println("Failed to retrieve group avatar (" + avatar.asPointer()
.getRemoteId() + "): " + e.getMessage()); .getRemoteId() + "): " + e.getMessage());
@ -1589,7 +1590,7 @@ public class Manager implements Closeable {
} }
case DELIVER: case DELIVER:
if (groupV1 == null && !isSync) { if (groupV1 == null && !isSync) {
actions.add(new SendGroupInfoRequestAction(source, groupInfo.getGroupId())); actions.add(new SendGroupInfoRequestAction(source, groupV1.getGroupId()));
} }
break; break;
case QUIT: { case QUIT: {
@ -1601,7 +1602,7 @@ public class Manager implements Closeable {
} }
case REQUEST_INFO: case REQUEST_INFO:
if (groupV1 != null && !isSync) { if (groupV1 != null && !isSync) {
actions.add(new SendGroupUpdateAction(source, groupV1.groupId)); actions.add(new SendGroupUpdateAction(source, groupV1.getGroupId()));
} }
break; break;
} }
@ -1627,7 +1628,7 @@ public class Manager implements Closeable {
if (message.getGroupContext().isPresent()) { if (message.getGroupContext().isPresent()) {
if (message.getGroupContext().get().getGroupV1().isPresent()) { if (message.getGroupContext().get().getGroupV1().isPresent()) {
SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get(); 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 != null) {
if (group.messageExpirationTime != message.getExpiresInSeconds()) { if (group.messageExpirationTime != message.getExpiresInSeconds()) {
group.messageExpirationTime = message.getExpiresInSeconds(); group.messageExpirationTime = message.getExpiresInSeconds();
@ -1723,17 +1724,17 @@ public class Manager implements Closeable {
) { ) {
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize(); GroupIdV2 groupId = GroupUtils.getGroupIdV2(groupSecretParams);
GroupInfo groupInfo = account.getGroupStore().getGroupByV2Id(groupId); GroupInfo groupInfo = account.getGroupStore().getGroup(groupId);
final GroupInfoV2 groupInfoV2; final GroupInfoV2 groupInfoV2;
if (groupInfo instanceof GroupInfoV1) { if (groupInfo instanceof GroupInfoV1) {
// Received a v2 group message for a v1 group, we need to locally migrate the group // 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); groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey);
System.err.println("Locally migrated group " System.err.println("Locally migrated group "
+ Base64.encodeBytes(groupInfo.groupId) + groupInfo.getGroupId().toBase64()
+ " to group v2, id: " + " to group v2, id: "
+ Base64.encodeBytes(groupInfoV2.groupId) + groupInfoV2.getGroupId().toBase64()
+ " !!!"); + " !!!");
} else if (groupInfo instanceof GroupInfoV2) { } else if (groupInfo instanceof GroupInfoV2) {
groupInfoV2 = (GroupInfoV2) groupInfo; groupInfoV2 = (GroupInfoV2) groupInfo;
@ -1970,19 +1971,14 @@ public class Manager implements Closeable {
if (content != null && content.getDataMessage().isPresent()) { if (content != null && content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get(); SignalServiceDataMessage message = content.getDataMessage().get();
if (message.getGroupContext().isPresent()) { if (message.getGroupContext().isPresent()) {
GroupInfo group = null;
if (message.getGroupContext().get().getGroupV1().isPresent()) { if (message.getGroupContext().get().getGroupV1().isPresent()) {
SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get(); SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
if (groupInfo.getType() == SignalServiceGroup.Type.DELIVER) { if (groupInfo.getType() != SignalServiceGroup.Type.DELIVER) {
group = getGroup(groupInfo.getGroupId()); return false;
} }
} }
if (message.getGroupContext().get().getGroupV2().isPresent()) { GroupId groupId = GroupUtils.getGroupId(message.getGroupContext().get());
SignalServiceGroupV2 groupContext = message.getGroupContext().get().getGroupV2().get(); GroupInfo group = account.getGroupStore().getGroup(groupId);
final GroupMasterKey groupMasterKey = groupContext.getMasterKey();
byte[] groupId = GroupUtils.getGroupId(groupMasterKey);
group = account.getGroupStore().getGroupByV2Id(groupId);
}
if (group != null && group.isBlocked()) { if (group != null && group.isBlocked()) {
return true; return true;
} }
@ -2055,7 +2051,8 @@ public class Manager implements Closeable {
DeviceGroupsInputStream s = new DeviceGroupsInputStream(attachmentAsStream); DeviceGroupsInputStream s = new DeviceGroupsInputStream(attachmentAsStream);
DeviceGroup g; DeviceGroup g;
while ((g = s.read()) != null) { 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 (syncGroup != null) {
if (g.getName().isPresent()) { if (g.getName().isPresent()) {
syncGroup.name = g.getName().get(); syncGroup.name = g.getName().get();
@ -2076,7 +2073,7 @@ public class Manager implements Closeable {
} }
if (g.getAvatar().isPresent()) { if (g.getAvatar().isPresent()) {
retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId); retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.getGroupId());
} }
syncGroup.inboxPosition = g.getInboxPosition().orNull(); syncGroup.inboxPosition = g.getInboxPosition().orNull();
syncGroup.archived = g.isArchived(); syncGroup.archived = g.isArchived();
@ -2104,12 +2101,15 @@ public class Manager implements Closeable {
for (SignalServiceAddress address : blockedListMessage.getAddresses()) { for (SignalServiceAddress address : blockedListMessage.getAddresses()) {
setContactBlocked(resolveSignalServiceAddress(address), true); setContactBlocked(resolveSignalServiceAddress(address), true);
} }
for (byte[] groupId : blockedListMessage.getGroupIds()) { for (GroupId groupId : blockedListMessage.getGroupIds()
.stream()
.map(GroupId::unknownVersion)
.collect(Collectors.toSet())) {
try { try {
setGroupBlocked(groupId, true); setGroupBlocked(groupId, true);
} catch (GroupNotFoundException e) { } catch (GroupNotFoundException e) {
System.err.println("BlockedListMessage contained groupID that was not found in GroupStore: " 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) { private File getGroupAvatarFile(GroupId groupId) {
return new File(pathConfig.getAvatarsPath(), "group-" + Base64.encodeBytes(groupId).replace("/", "_")); return new File(pathConfig.getAvatarsPath(), "group-" + groupId.toBase64().replace("/", "_"));
} }
private File retrieveGroupAvatarAttachment( private File retrieveGroupAvatarAttachment(
SignalServiceAttachment attachment, byte[] groupId SignalServiceAttachment attachment, GroupId groupId
) throws IOException, InvalidMessageException, MissingConfigurationException { ) throws IOException, InvalidMessageException, MissingConfigurationException {
IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath()); IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath());
if (attachment.isPointer()) { if (attachment.isPointer()) {
@ -2333,10 +2333,10 @@ public class Manager implements Closeable {
for (GroupInfo record : account.getGroupStore().getGroups()) { for (GroupInfo record : account.getGroupStore().getGroups()) {
if (record instanceof GroupInfoV1) { if (record instanceof GroupInfoV1) {
GroupInfoV1 groupInfo = (GroupInfoV1) record; GroupInfoV1 groupInfo = (GroupInfoV1) record;
out.write(new DeviceGroup(groupInfo.groupId, out.write(new DeviceGroup(groupInfo.getGroupId().serialize(),
Optional.fromNullable(groupInfo.name), Optional.fromNullable(groupInfo.name),
new ArrayList<>(groupInfo.getMembers()), new ArrayList<>(groupInfo.getMembers()),
createGroupAvatarAttachment(groupInfo.groupId), createGroupAvatarAttachment(groupInfo.getGroupId()),
groupInfo.isMember(account.getSelfAddress()), groupInfo.isMember(account.getSelfAddress()),
Optional.of(groupInfo.messageExpirationTime), Optional.of(groupInfo.messageExpirationTime),
Optional.fromNullable(groupInfo.color), Optional.fromNullable(groupInfo.color),
@ -2442,7 +2442,7 @@ public class Manager implements Closeable {
List<byte[]> groupIds = new ArrayList<>(); List<byte[]> groupIds = new ArrayList<>();
for (GroupInfo record : account.getGroupStore().getGroups()) { for (GroupInfo record : account.getGroupStore().getGroups()) {
if (record.isBlocked()) { if (record.isBlocked()) {
groupIds.add(record.groupId); groupIds.add(record.getGroupId().serialize());
} }
} }
sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds))); sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds)));
@ -2466,7 +2466,7 @@ public class Manager implements Closeable {
return account.getContactStore().getContact(Util.getSignalServiceAddressFromIdentifier(number)); return account.getContactStore().getContact(Util.getSignalServiceAddressFromIdentifier(number));
} }
public GroupInfo getGroup(byte[] groupId) { public GroupInfo getGroup(GroupId groupId) {
return account.getGroupStore().getGroup(groupId); return account.getGroupStore().getGroup(groupId);
} }

View file

@ -1,10 +1,8 @@
package org.asamk.signal.manager; package org.asamk.signal.manager;
import org.whispersystems.util.Base64;
public class NotAGroupMemberException extends Exception { public class NotAGroupMemberException extends Exception {
public NotAGroupMemberException(byte[] groupId, String groupName) { public NotAGroupMemberException(GroupId groupId, String groupName) {
super("User is not a member in group: " + groupName + " (" + Base64.encodeBytes(groupId) + ")"); super("User is not a member in group: " + groupName + " (" + groupId.toBase64() + ")");
} }
} }

View file

@ -2,7 +2,9 @@ package org.asamk.signal.manager.helper;
import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.InvalidProtocolBufferException;
import org.asamk.signal.manager.GroupIdV2;
import org.asamk.signal.manager.GroupLinkPassword; import org.asamk.signal.manager.GroupLinkPassword;
import org.asamk.signal.manager.GroupUtils;
import org.asamk.signal.storage.groups.GroupInfoV2; import org.asamk.signal.storage.groups.GroupInfoV2;
import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.IOUtils;
import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.AccessControl;
@ -117,7 +119,7 @@ public class GroupHelper {
return null; return null;
} }
final byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize(); final GroupIdV2 groupId = GroupUtils.getGroupIdV2(groupSecretParams);
final GroupMasterKey masterKey = groupSecretParams.getMasterKey(); final GroupMasterKey masterKey = groupSecretParams.getMasterKey();
GroupInfoV2 g = new GroupInfoV2(groupId, masterKey); GroupInfoV2 g = new GroupInfoV2(groupId, masterKey);
g.setGroup(decryptedGroup); g.setGroup(decryptedGroup);

View file

@ -10,6 +10,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ObjectNode; 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.ContactInfo;
import org.asamk.signal.storage.contacts.JsonContactsStore; import org.asamk.signal.storage.contacts.JsonContactsStore;
import org.asamk.signal.storage.groups.GroupInfo; import org.asamk.signal.storage.groups.GroupInfo;
@ -309,7 +310,7 @@ public class SignalAccount implements Closeable {
contactInfo.messageExpirationTime = thread.messageExpirationTime; contactInfo.messageExpirationTime = thread.messageExpirationTime;
contactStore.updateContact(contactInfo); contactStore.updateContact(contactInfo);
} else { } else {
GroupInfo groupInfo = groupStore.getGroup(Base64.decode(thread.id)); GroupInfo groupInfo = groupStore.getGroup(GroupId.fromBase64(thread.id));
if (groupInfo instanceof GroupInfoV1) { if (groupInfo instanceof GroupInfoV1) {
((GroupInfoV1) groupInfo).messageExpirationTime = thread.messageExpirationTime; ((GroupInfoV1) groupInfo).messageExpirationTime = thread.messageExpirationTime;
groupStore.updateGroup(groupInfo); groupStore.updateGroup(groupInfo);

View file

@ -1,8 +1,8 @@
package org.asamk.signal.storage.groups; package org.asamk.signal.storage.groups;
import com.fasterxml.jackson.annotation.JsonIgnore; 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.asamk.signal.manager.GroupInviteLinkUrl;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@ -12,12 +12,8 @@ import java.util.stream.Stream;
public abstract class GroupInfo { public abstract class GroupInfo {
@JsonProperty @JsonIgnore
public final byte[] groupId; public abstract GroupId getGroupId();
public GroupInfo(byte[] groupId) {
this.groupId = groupId;
}
@JsonIgnore @JsonIgnore
public abstract String getTitle(); public abstract String getTitle();

View file

@ -13,7 +13,11 @@ import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize; 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.GroupInviteLinkUrl;
import org.asamk.signal.manager.GroupUtils;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.io.IOException; import java.io.IOException;
@ -26,8 +30,9 @@ public class GroupInfoV1 extends GroupInfo {
private static final ObjectMapper jsonProcessor = new ObjectMapper(); private static final ObjectMapper jsonProcessor = new ObjectMapper();
@JsonProperty private final GroupIdV1 groupId;
public byte[] expectedV2Id;
private GroupIdV2 expectedV2Id;
@JsonProperty @JsonProperty
public String name; public String name;
@ -47,18 +52,8 @@ public class GroupInfoV1 extends GroupInfo {
@JsonProperty(defaultValue = "false") @JsonProperty(defaultValue = "false")
public boolean archived; public boolean archived;
public GroupInfoV1(byte[] groupId) { public GroupInfoV1(GroupIdV1 groupId) {
super(groupId); this.groupId = groupId;
}
@Override
public String getTitle() {
return name;
}
@Override
public GroupInviteLinkUrl getGroupInviteLink() {
return null;
} }
public GroupInfoV1( public GroupInfoV1(
@ -74,8 +69,8 @@ public class GroupInfoV1 extends GroupInfo {
@JsonProperty("messageExpirationTime") int messageExpirationTime, @JsonProperty("messageExpirationTime") int messageExpirationTime,
@JsonProperty("active") boolean _ignored_active @JsonProperty("active") boolean _ignored_active
) { ) {
super(groupId); this.groupId = GroupId.v1(groupId);
this.expectedV2Id = expectedV2Id; this.expectedV2Id = GroupId.v2(expectedV2Id);
this.name = name; this.name = name;
this.members.addAll(members); this.members.addAll(members);
this.color = color; this.color = color;
@ -85,6 +80,40 @@ public class GroupInfoV1 extends GroupInfo {
this.messageExpirationTime = messageExpirationTime; 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 @JsonIgnore
public Set<SignalServiceAddress> getMembers() { public Set<SignalServiceAddress> getMembers() {
return members; return members;

View file

@ -1,5 +1,6 @@
package org.asamk.signal.storage.groups; package org.asamk.signal.storage.groups;
import org.asamk.signal.manager.GroupIdV2;
import org.asamk.signal.manager.GroupInviteLinkUrl; import org.asamk.signal.manager.GroupInviteLinkUrl;
import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroup;
@ -13,16 +14,22 @@ import java.util.stream.Collectors;
public class GroupInfoV2 extends GroupInfo { public class GroupInfoV2 extends GroupInfo {
private final GroupIdV2 groupId;
private final GroupMasterKey masterKey; private final GroupMasterKey masterKey;
private boolean blocked; private boolean blocked;
private DecryptedGroup group; // stored as a file with hexadecimal groupId as name private DecryptedGroup group; // stored as a file with hexadecimal groupId as name
public GroupInfoV2(final byte[] groupId, final GroupMasterKey masterKey) { public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey) {
super(groupId); this.groupId = groupId;
this.masterKey = masterKey; this.masterKey = masterKey;
} }
@Override
public GroupIdV2 getGroupId() {
return groupId;
}
public GroupMasterKey getMasterKey() { public GroupMasterKey getMasterKey() {
return masterKey; return masterKey;
} }

View file

@ -12,6 +12,9 @@ import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize; 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.manager.GroupUtils;
import org.asamk.signal.util.Hex; import org.asamk.signal.util.Hex;
import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.IOUtils;
@ -25,7 +28,6 @@ import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -39,7 +41,7 @@ public class JsonGroupStore {
@JsonProperty("groups") @JsonProperty("groups")
@JsonSerialize(using = GroupsSerializer.class) @JsonSerialize(using = GroupsSerializer.class)
@JsonDeserialize(using = GroupsDeserializer.class) @JsonDeserialize(using = GroupsDeserializer.class)
private final Map<String, GroupInfo> groups = new HashMap<>(); private final Map<GroupId, GroupInfo> groups = new HashMap<>();
private JsonGroupStore() { private JsonGroupStore() {
} }
@ -49,11 +51,11 @@ public class JsonGroupStore {
} }
public void updateGroup(GroupInfo group) { 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) { if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() != null) {
try { try {
IOUtils.createPrivateDirectories(groupCachePath); IOUtils.createPrivateDirectories(groupCachePath);
try (FileOutputStream stream = new FileOutputStream(getGroupFile(group.groupId))) { try (FileOutputStream stream = new FileOutputStream(getGroupFile(group.getGroupId()))) {
((GroupInfoV2) group).getGroup().writeTo(stream); ((GroupInfoV2) group).getGroup().writeTo(stream);
} }
} catch (IOException e) { } catch (IOException e) {
@ -62,57 +64,50 @@ public class JsonGroupStore {
} }
} }
public void deleteGroup(byte[] groupId) { public void deleteGroup(GroupId groupId) {
groups.remove(Base64.encodeBytes(groupId)); groups.remove(groupId);
} }
public GroupInfo getGroup(byte[] groupId) { public GroupInfo getGroup(GroupId groupId) {
final GroupInfo group = groups.get(Base64.encodeBytes(groupId)); GroupInfo group = groups.get(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));
if (group == null) { if (group == null) {
group = groups.get(Base64.encodeBytes(GroupUtils.getGroupId(GroupUtils.deriveV2MigrationMasterKey(groupIdV1)))); if (groupId instanceof GroupIdV1) {
} group = groups.get(GroupUtils.getGroupIdV2((GroupIdV1) groupId));
loadDecryptedGroup(group); } else if (groupId instanceof GroupIdV2) {
return group; group = getGroupV1ByV2Id((GroupIdV2) groupId);
}
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;
}
} }
} }
loadDecryptedGroup(group); loadDecryptedGroup(group);
return group; return group;
} }
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;
}
}
}
return null;
}
private void loadDecryptedGroup(final GroupInfo group) { private void loadDecryptedGroup(final GroupInfo group) {
if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) { 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)); ((GroupInfoV2) group).setGroup(DecryptedGroup.parseFrom(stream));
} catch (IOException ignored) { } catch (IOException ignored) {
} }
} }
} }
private File getGroupFile(final byte[] groupId) { private File getGroupFile(final GroupId groupId) {
return new File(groupCachePath, Hex.toStringCondensed(groupId)); return new File(groupCachePath, Hex.toStringCondensed(groupId.serialize()));
} }
public GroupInfoV1 getOrCreateGroupV1(byte[] groupId) { public GroupInfoV1 getOrCreateGroupV1(GroupIdV1 groupId) {
GroupInfo group = groups.get(Base64.encodeBytes(groupId)); GroupInfo group = getGroup(groupId);
if (group instanceof GroupInfoV1) { if (group instanceof GroupInfoV1) {
return (GroupInfoV1) group; return (GroupInfoV1) group;
} }
@ -146,7 +141,7 @@ public class JsonGroupStore {
} else if (group instanceof GroupInfoV2) { } else if (group instanceof GroupInfoV2) {
final GroupInfoV2 groupV2 = (GroupInfoV2) group; final GroupInfoV2 groupV2 = (GroupInfoV2) group;
jgen.writeStartObject(); jgen.writeStartObject();
jgen.writeStringField("groupId", Base64.encodeBytes(groupV2.groupId)); jgen.writeStringField("groupId", groupV2.getGroupId().toBase64());
jgen.writeStringField("masterKey", Base64.encodeBytes(groupV2.getMasterKey().serialize())); jgen.writeStringField("masterKey", Base64.encodeBytes(groupV2.getMasterKey().serialize()));
jgen.writeBooleanField("blocked", groupV2.isBlocked()); jgen.writeBooleanField("blocked", groupV2.isBlocked());
jgen.writeEndObject(); jgen.writeEndObject();
@ -158,34 +153,31 @@ public class JsonGroupStore {
} }
} }
private static class GroupsDeserializer extends JsonDeserializer<Map<String, GroupInfo>> { private static class GroupsDeserializer extends JsonDeserializer<Map<GroupId, GroupInfo>> {
@Override @Override
public Map<String, GroupInfo> deserialize( public Map<GroupId, GroupInfo> deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException { ) throws IOException {
Map<String, GroupInfo> groups = new HashMap<>(); Map<GroupId, GroupInfo> groups = new HashMap<>();
JsonNode node = jsonParser.getCodec().readTree(jsonParser); JsonNode node = jsonParser.getCodec().readTree(jsonParser);
for (JsonNode n : node) { for (JsonNode n : node) {
GroupInfo g; GroupInfo g;
if (n.has("masterKey")) { if (n.has("masterKey")) {
// a v2 group // a v2 group
byte[] groupId = Base64.decode(n.get("groupId").asText()); GroupIdV2 groupId = GroupIdV2.fromBase64(n.get("groupId").asText());
try { try {
GroupMasterKey masterKey = new GroupMasterKey(Base64.decode(n.get("masterKey").asText())); GroupMasterKey masterKey = new GroupMasterKey(Base64.decode(n.get("masterKey").asText()));
g = new GroupInfoV2(groupId, masterKey); g = new GroupInfoV2(groupId, masterKey);
} catch (InvalidInputException e) { } 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)); g.setBlocked(n.get("blocked").asBoolean(false));
} else { } else {
GroupInfoV1 gv1 = jsonProcessor.treeToValue(n, GroupInfoV1.class); GroupInfoV1 gv1 = jsonProcessor.treeToValue(n, GroupInfoV1.class);
if (gv1.expectedV2Id == null) {
gv1.expectedV2Id = GroupUtils.getGroupId(GroupUtils.deriveV2MigrationMasterKey(gv1.groupId));
}
g = gv1; g = gv1;
} }
groups.put(Base64.encodeBytes(g.groupId), g); groups.put(g.getGroupId(), g);
} }
return groups; return groups;

View file

@ -1,5 +1,6 @@
package org.asamk.signal.util; package org.asamk.signal.util;
import org.asamk.signal.manager.GroupIdFormatException;
import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.GroupNotFoundException;
import org.asamk.signal.manager.NotAGroupMemberException; import org.asamk.signal.manager.NotAGroupMemberException;
import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SendMessageResult;

View file

@ -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());
}
}

View file

@ -2,13 +2,13 @@ package org.asamk.signal.util;
import com.fasterxml.jackson.databind.JsonNode; 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.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.util.Base64;
import java.io.IOException;
import java.io.InvalidObjectException; import java.io.InvalidObjectException;
public class Util { public class Util {
@ -48,12 +48,8 @@ public class Util {
return node; return node;
} }
public static byte[] decodeGroupId(String groupId) throws GroupIdFormatException { public static GroupId decodeGroupId(String groupId) throws GroupIdFormatException {
try { return GroupId.fromBase64(groupId);
return Base64.decode(groupId);
} catch (IOException e) {
throw new GroupIdFormatException(groupId, e);
}
} }
public static String canonicalizeNumber(String number, String localNumber) throws InvalidNumberException { public static String canonicalizeNumber(String number, String localNumber) throws InvalidNumberException {