import org.signal.libsignal.metadata.SelfSendException;
import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
+import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.VerificationFailedException;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
+import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
-import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
+import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
+import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
+import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
+import java.security.SignatureException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
+import static org.asamk.signal.manager.ServiceConfig.CDS_MRENCLAVE;
import static org.asamk.signal.manager.ServiceConfig.capabilities;
+import static org.asamk.signal.manager.ServiceConfig.getIasKeyStore;
public class Manager implements Closeable {
contact.profileKey = null;
account.getProfileStore().storeProfileKey(contact.getAddress(), profileKey);
}
+ // Ensure our profile key is stored in profile store
+ account.getProfileStore().storeProfileKey(getSelfAddress(), account.getProfileKey());
}
public void checkAccountState() throws IOException {
return g;
}
+ private GroupInfo getGroupForUpdating(byte[] groupId) throws GroupNotFoundException, NotAGroupMemberException {
+ GroupInfo g = account.getGroupStore().getGroup(groupId);
+ if (g == null) {
+ throw new GroupNotFoundException(groupId);
+ }
+ if (!g.isMember(account.getSelfAddress()) && !g.isPendingMember(account.getSelfAddress())) {
+ throw new NotAGroupMemberException(groupId, g.getTitle());
+ }
+ return g;
+ }
+
public List<GroupInfo> getGroups() {
return account.getGroupStore().getGroups();
}
SignalServiceDataMessage.Builder messageBuilder;
- final GroupInfo g = getGroupForSending(groupId);
+ final GroupInfo g = getGroupForUpdating(groupId);
if (g instanceof GroupInfoV1) {
GroupInfoV1 groupInfoV1 = (GroupInfoV1) g;
SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
g = gv2;
}
} else {
- GroupInfo group = getGroupForSending(groupId);
+ GroupInfo group = getGroupForUpdating(groupId);
if (group instanceof GroupInfoV2) {
- Pair<DecryptedGroup, GroupChange> groupGroupChangePair = null;
+ final GroupInfoV2 groupInfoV2 = (GroupInfoV2) group;
+
+ Pair<Long, List<SendMessageResult>> result = null;
+ if (groupInfoV2.isPendingMember(getSelfAddress())) {
+ Pair<DecryptedGroup, GroupChange> groupGroupChangePair = groupHelper.acceptInvite(groupInfoV2);
+ result = sendUpdateGroupMessage(groupInfoV2,
+ groupGroupChangePair.first(),
+ groupGroupChangePair.second());
+ }
+
if (members != null) {
final Set<SignalServiceAddress> newMembers = new HashSet<>(members);
newMembers.removeAll(group.getMembers());
if (newMembers.size() > 0) {
- groupGroupChangePair = groupHelper.updateGroupV2((GroupInfoV2) group, newMembers);
+ Pair<DecryptedGroup, GroupChange> groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2,
+ newMembers);
+ result = sendUpdateGroupMessage(groupInfoV2,
+ groupGroupChangePair.first(),
+ groupGroupChangePair.second());
}
}
- if (groupGroupChangePair == null || name != null || avatarFile != null) {
- if (groupGroupChangePair != null) {
- ((GroupInfoV2) group).setGroup(groupGroupChangePair.first());
- messageBuilder = getGroupUpdateMessageBuilder((GroupInfoV2) group,
- groupGroupChangePair.second().toByteArray());
- sendMessage(messageBuilder, group.getMembersWithout(account.getSelfAddress()));
- }
-
- groupGroupChangePair = groupHelper.updateGroupV2((GroupInfoV2) group, name, avatarFile);
+ if (result == null || name != null || avatarFile != null) {
+ Pair<DecryptedGroup, GroupChange> groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2,
+ name,
+ avatarFile);
+ result = sendUpdateGroupMessage(groupInfoV2,
+ groupGroupChangePair.first(),
+ groupGroupChangePair.second());
}
- ((GroupInfoV2) group).setGroup(groupGroupChangePair.first());
- messageBuilder = getGroupUpdateMessageBuilder((GroupInfoV2) group,
- groupGroupChangePair.second().toByteArray());
- g = group;
+ return new Pair<>(group.groupId, result.second());
} else {
GroupInfoV1 gv1 = (GroupInfoV1) group;
updateGroupV1(gv1, name, members, avatarFile);
account.getGroupStore().updateGroup(g);
final Pair<Long, List<SendMessageResult>> result = sendMessage(messageBuilder,
- g.getMembersWithout(account.getSelfAddress()));
+ g.getMembersIncludingPendingWithout(account.getSelfAddress()));
return new Pair<>(g.groupId, result.second());
}
+ public Pair<byte[], List<SendMessageResult>> joinGroup(
+ GroupInviteLinkUrl inviteLinkUrl
+ ) throws IOException, GroupLinkNotActiveException {
+ return sendJoinGroupMessage(inviteLinkUrl);
+ }
+
+ private Pair<byte[], List<SendMessageResult>> sendJoinGroupMessage(
+ GroupInviteLinkUrl inviteLinkUrl
+ ) throws IOException, GroupLinkNotActiveException {
+ final DecryptedGroupJoinInfo groupJoinInfo = groupHelper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(),
+ inviteLinkUrl.getPassword());
+ final GroupChange groupChange = groupHelper.joinGroup(inviteLinkUrl.getGroupMasterKey(),
+ inviteLinkUrl.getPassword(),
+ groupJoinInfo);
+ final GroupInfoV2 group = getOrMigrateGroup(inviteLinkUrl.getGroupMasterKey(),
+ groupJoinInfo.getRevision() + 1,
+ groupChange.toByteArray());
+
+ if (group.getGroup() == null) {
+ // Only requested member, can't send update to group members
+ return new Pair<>(group.groupId, List.of());
+ }
+
+ final Pair<Long, List<SendMessageResult>> result = sendUpdateGroupMessage(group, group.getGroup(), groupChange);
+
+ return new Pair<>(group.groupId, result.second());
+ }
+
+ private Pair<Long, List<SendMessageResult>> sendUpdateGroupMessage(
+ GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange
+ ) throws IOException {
+ group.setGroup(newDecryptedGroup);
+ final SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(group,
+ groupChange.toByteArray());
+ account.getGroupStore().updateGroup(group);
+ return sendMessage(messageBuilder, group.getMembersIncludingPendingWithout(account.getSelfAddress()));
+ }
+
private void updateGroupV1(
final GroupInfoV1 g,
final String name,
private Collection<SignalServiceAddress> getSignalServiceAddresses(Collection<String> numbers) throws InvalidNumberException {
final Set<SignalServiceAddress> signalServiceAddresses = new HashSet<>(numbers.size());
+ final Set<SignalServiceAddress> missingUuids = new HashSet<>();
for (String number : numbers) {
- signalServiceAddresses.add(canonicalizeAndResolveSignalServiceAddress(number));
+ final SignalServiceAddress resolvedAddress = canonicalizeAndResolveSignalServiceAddress(number);
+ if (resolvedAddress.getUuid().isPresent()) {
+ signalServiceAddresses.add(resolvedAddress);
+ } else {
+ missingUuids.add(resolvedAddress);
+ }
+ }
+
+ Map<String, UUID> registeredUsers;
+ try {
+ registeredUsers = accountManager.getRegisteredUsers(getIasKeyStore(),
+ missingUuids.stream().map(a -> a.getNumber().get()).collect(Collectors.toSet()),
+ CDS_MRENCLAVE);
+ } catch (IOException | Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | SignatureException | UnauthenticatedResponseException e) {
+ System.err.println("Failed to resolve uuids from server: " + e.getMessage());
+ registeredUsers = new HashMap<>();
+ }
+
+ for (SignalServiceAddress address : missingUuids) {
+ final String number = address.getNumber().get();
+ if (registeredUsers.containsKey(number)) {
+ final SignalServiceAddress newAddress = resolveSignalServiceAddress(new SignalServiceAddress(
+ registeredUsers.get(number),
+ number));
+ signalServiceAddresses.add(newAddress);
+ } else {
+ signalServiceAddresses.add(address);
+ }
}
+
return signalServiceAddresses;
}
final SignalServiceGroupV2 groupContext = message.getGroupContext().get().getGroupV2().get();
final GroupMasterKey groupMasterKey = groupContext.getMasterKey();
- final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
-
- byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize();
- GroupInfo groupInfo = account.getGroupStore().getGroupByV2Id(groupId);
- if (groupInfo instanceof GroupInfoV1) {
- // Received a v2 group message for a v2 group, we need to locally migrate the group
- account.getGroupStore().deleteGroup(groupInfo.groupId);
- GroupInfoV2 groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey);
- groupInfoV2.setGroup(getDecryptedGroup(groupSecretParams));
- account.getGroupStore().updateGroup(groupInfoV2);
- System.err.println("Locally migrated group "
- + Base64.encodeBytes(groupInfo.groupId)
- + " to group v2, id: "
- + Base64.encodeBytes(groupInfoV2.groupId)
- + " !!!");
- } else if (groupInfo == null || groupInfo instanceof GroupInfoV2) {
- GroupInfoV2 groupInfoV2 = groupInfo == null
- ? new GroupInfoV2(groupId, groupMasterKey)
- : (GroupInfoV2) groupInfo;
-
- if (groupInfoV2.getGroup() == null
- || groupInfoV2.getGroup().getRevision() < groupContext.getRevision()) {
- DecryptedGroup group = null;
- if (groupContext.hasSignedGroupChange()
- && groupInfoV2.getGroup() != null
- && groupInfoV2.getGroup().getRevision() + 1 == groupContext.getRevision()) {
- group = groupHelper.getUpdatedDecryptedGroup(groupInfoV2.getGroup(),
- groupContext.getSignedGroupChange(),
- groupMasterKey);
- }
- if (group == null) {
- group = getDecryptedGroup(groupSecretParams);
- }
- groupInfoV2.setGroup(group);
- account.getGroupStore().updateGroup(groupInfoV2);
- }
- }
+ getOrMigrateGroup(groupMasterKey,
+ groupContext.getRevision(),
+ groupContext.hasSignedGroupChange() ? groupContext.getSignedGroupChange() : null);
}
}
+
final SignalServiceAddress conversationPartnerAddress = isSync ? destination : source;
if (message.isEndSession()) {
handleEndSession(conversationPartnerAddress);
return actions;
}
- private DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) {
- try {
- final GroupsV2AuthorizationString groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams);
- DecryptedGroup group = groupsV2Api.getGroup(groupSecretParams, groupsV2AuthorizationString);
- for (DecryptedMember member : group.getMembersList()) {
- final SignalServiceAddress address = resolveSignalServiceAddress(new SignalServiceAddress(UuidUtil.parseOrThrow(
- member.getUuid().toByteArray()), null));
- try {
- account.getProfileStore()
- .storeProfileKey(address, new ProfileKey(member.getProfileKey().toByteArray()));
- } catch (InvalidInputException ignored) {
- }
+ private GroupInfoV2 getOrMigrateGroup(
+ final GroupMasterKey groupMasterKey, final int revision, final byte[] signedGroupChange
+ ) {
+ final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
+
+ byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize();
+ GroupInfo groupInfo = account.getGroupStore().getGroupByV2Id(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);
+ groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey);
+ System.err.println("Locally migrated group "
+ + Base64.encodeBytes(groupInfo.groupId)
+ + " to group v2, id: "
+ + Base64.encodeBytes(groupInfoV2.groupId)
+ + " !!!");
+ } else if (groupInfo instanceof GroupInfoV2) {
+ groupInfoV2 = (GroupInfoV2) groupInfo;
+ } else {
+ groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey);
+ }
+
+ if (groupInfoV2.getGroup() == null || groupInfoV2.getGroup().getRevision() < revision) {
+ DecryptedGroup group = null;
+ if (signedGroupChange != null
+ && groupInfoV2.getGroup() != null
+ && groupInfoV2.getGroup().getRevision() + 1 == revision) {
+ group = groupHelper.getUpdatedDecryptedGroup(groupInfoV2.getGroup(), signedGroupChange, groupMasterKey);
+ }
+ if (group == null) {
+ group = groupHelper.getDecryptedGroup(groupSecretParams);
+ }
+ if (group != null) {
+ storeProfileKeysFromMembers(group);
+ }
+ groupInfoV2.setGroup(group);
+ account.getGroupStore().updateGroup(groupInfoV2);
+ }
+
+ return groupInfoV2;
+ }
+
+ private void storeProfileKeysFromMembers(final DecryptedGroup group) {
+ for (DecryptedMember member : group.getMembersList()) {
+ final SignalServiceAddress address = resolveSignalServiceAddress(new SignalServiceAddress(UuidUtil.parseOrThrow(
+ member.getUuid().toByteArray()), null));
+ try {
+ account.getProfileStore()
+ .storeProfileKey(address, new ProfileKey(member.getProfileKey().toByteArray()));
+ } catch (InvalidInputException ignored) {
}
- return group;
- } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
- System.err.println("Failed to retrieve Group V2 info, ignoring ...");
- return null;
}
}
if (content != null && content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get();
- if (message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()) {
- SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
- GroupInfo group = getGroup(groupInfo.getGroupId());
- return groupInfo.getType() == SignalServiceGroup.Type.DELIVER && group != null && group.isBlocked();
+ 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 (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);
+ }
+ if (group != null && group.isBlocked()) {
+ return true;
+ }
}
}
return false;