X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/98dee97cc69b49c0c3b75bb8691ae7ccc266e4d2..17608ce5220142b979f777cbd1512416043f52bb:/src/main/java/org/asamk/signal/manager/Manager.java diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 000455ac..24bc0ebd 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -43,6 +43,7 @@ import org.signal.libsignal.metadata.ProtocolLegacyMessageException; import org.signal.libsignal.metadata.ProtocolNoSessionException; import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; 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.DecryptedMember; import org.signal.zkgroup.InvalidInputException; @@ -119,6 +120,9 @@ import org.whispersystems.signalservice.api.util.StreamDetails; 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; @@ -141,6 +145,7 @@ import java.nio.charset.StandardCharsets; 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; @@ -150,6 +155,7 @@ import java.util.HashMap; 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; @@ -160,7 +166,9 @@ import java.util.stream.Collectors; 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 { @@ -213,7 +221,9 @@ public class Manager implements Closeable { this.groupHelper = new GroupHelper(this::getRecipientProfileKeyCredential, this::getRecipientProfile, account::getSelfAddress, - groupsV2Operations); + groupsV2Operations, + groupsV2Api, + this::getGroupAuthForToday); } public String getUsername() { @@ -312,6 +322,8 @@ 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 { @@ -695,6 +707,17 @@ public class Manager implements Closeable { 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 getGroups() { return account.getGroupStore().getGroups(); } @@ -736,52 +759,29 @@ public class Manager implements Closeable { } public Pair> sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, NotAGroupMemberException { - SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT).withId(groupId).build(); - SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group); + 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) + .withId(groupId) + .build(); + messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group); groupInfoV1.removeMember(account.getSelfAddress()); account.getGroupStore().updateGroup(groupInfoV1); } else { - throw new RuntimeException("TODO Not implemented!"); + final GroupInfoV2 groupInfoV2 = (GroupInfoV2) g; + final Pair groupGroupChangePair = groupHelper.leaveGroup(groupInfoV2); + groupInfoV2.setGroup(groupGroupChangePair.first()); + messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray()); + account.getGroupStore().updateGroup(groupInfoV2); } return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress())); } - private GroupInfoV2 createGroupV2( - String name, Collection members, InputStream avatar - ) throws IOException { - byte[] avatarBytes = avatar == null ? null : IOUtils.readFully(avatar); - final GroupsV2Operations.NewGroup newGroup = groupHelper.createGroupV2(name, members, avatarBytes); - final GroupSecretParams groupSecretParams = newGroup.getGroupSecretParams(); - - final GroupsV2AuthorizationString groupAuthForToday; - final DecryptedGroup decryptedGroup; - try { - groupAuthForToday = getGroupAuthForToday(groupSecretParams); - groupsV2Api.putNewGroup(newGroup, groupAuthForToday); - decryptedGroup = groupsV2Api.getGroup(groupSecretParams, groupAuthForToday); - } catch (IOException | VerificationFailedException | InvalidGroupStateException e) { - System.err.println("Failed to create V2 group: " + e.getMessage()); - return null; - } - if (decryptedGroup == null) { - System.err.println("Failed to create V2 group!"); - return null; - } - - final byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize(); - final GroupMasterKey masterKey = groupSecretParams.getMasterKey(); - GroupInfoV2 g = new GroupInfoV2(groupId, masterKey); - g.setGroup(decryptedGroup); - - return g; - } - private Pair> sendUpdateGroupMessage( byte[] groupId, String name, Collection members, String avatarFile ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { @@ -789,8 +789,7 @@ public class Manager implements Closeable { SignalServiceDataMessage.Builder messageBuilder; if (groupId == null) { // Create new group - InputStream avatar = avatarFile == null ? null : new FileInputStream(avatarFile); - GroupInfoV2 gv2 = createGroupV2(name, members, avatar); + GroupInfoV2 gv2 = groupHelper.createGroupV2(name, members, avatarFile); if (gv2 == null) { GroupInfoV1 gv1 = new GroupInfoV1(KeyUtils.createGroupId()); gv1.addMembers(Collections.singleton(account.getSelfAddress())); @@ -798,27 +797,68 @@ public class Manager implements Closeable { messageBuilder = getGroupUpdateMessageBuilder(gv1); g = gv1; } else { - messageBuilder = getGroupUpdateMessageBuilder(gv2); + messageBuilder = getGroupUpdateMessageBuilder(gv2, null); g = gv2; } } else { - GroupInfo group = getGroupForSending(groupId); - if (!(group instanceof GroupInfoV1)) { - throw new RuntimeException("TODO Not implemented!"); + GroupInfo group = getGroupForUpdating(groupId); + if (group instanceof GroupInfoV2) { + final GroupInfoV2 groupInfoV2 = (GroupInfoV2) group; + + Pair> result = null; + if (groupInfoV2.isPendingMember(getSelfAddress())) { + Pair groupGroupChangePair = groupHelper.acceptInvite(groupInfoV2); + result = sendUpdateGroupMessage(groupInfoV2, + groupGroupChangePair.first(), + groupGroupChangePair.second()); + } + + if (members != null) { + final Set newMembers = new HashSet<>(members); + newMembers.removeAll(group.getMembers()); + if (newMembers.size() > 0) { + Pair groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2, + newMembers); + result = sendUpdateGroupMessage(groupInfoV2, + groupGroupChangePair.first(), + groupGroupChangePair.second()); + } + } + if (result == null || name != null || avatarFile != null) { + Pair groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2, + name, + avatarFile); + result = sendUpdateGroupMessage(groupInfoV2, + groupGroupChangePair.first(), + groupGroupChangePair.second()); + } + + return new Pair<>(group.groupId, result.second()); + } else { + GroupInfoV1 gv1 = (GroupInfoV1) group; + updateGroupV1(gv1, name, members, avatarFile); + messageBuilder = getGroupUpdateMessageBuilder(gv1); + g = gv1; } - GroupInfoV1 gv1 = (GroupInfoV1) group; - updateGroupV1(gv1, name, members, avatarFile); - messageBuilder = getGroupUpdateMessageBuilder(gv1); - g = gv1; } account.getGroupStore().updateGroup(g); final Pair> result = sendMessage(messageBuilder, - g.getMembersWithout(account.getSelfAddress())); + g.getMembersIncludingPendingWithout(account.getSelfAddress())); return new Pair<>(g.groupId, result.second()); } + private Pair> 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, @@ -899,11 +939,10 @@ public class Manager implements Closeable { .withExpiration(g.getMessageExpirationTime()); } - private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g) { + private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) { SignalServiceGroupV2.Builder group = SignalServiceGroupV2.newBuilder(g.getMasterKey()) .withRevision(g.getGroup().getRevision()) -// .withSignedGroupChange() // TODO - ; + .withSignedGroupChange(signedGroupChange); return SignalServiceDataMessage.newBuilder() .asGroupMessage(group.build()) .withExpiration(g.getMessageExpirationTime()); @@ -1278,10 +1317,39 @@ public class Manager implements Closeable { private Collection getSignalServiceAddresses(Collection numbers) throws InvalidNumberException { final Set signalServiceAddresses = new HashSet<>(numbers.size()); + final Set 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 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; } @@ -1427,16 +1495,20 @@ public class Manager implements Closeable { private GroupsV2AuthorizationString getGroupAuthForToday( final GroupSecretParams groupSecretParams - ) throws IOException, VerificationFailedException { + ) throws IOException { final int today = currentTimeDays(); // Returns credentials for the next 7 days final HashMap credentials = groupsV2Api.getCredentials(today); // TODO cache credentials until they expire AuthCredentialResponse authCredentialResponse = credentials.get(today); - return groupsV2Api.getGroupsV2AuthorizationString(account.getUuid(), - today, - groupSecretParams, - authCredentialResponse); + try { + return groupsV2Api.getGroupsV2AuthorizationString(account.getUuid(), + today, + groupSecretParams, + authCredentialResponse); + } catch (VerificationFailedException e) { + throw new IOException(e); + } } private List handleSignalServiceDataMessage( @@ -1541,6 +1613,9 @@ public class Manager implements Closeable { group = groupHelper.getUpdatedDecryptedGroup(groupInfoV2.getGroup(), groupContext.getSignedGroupChange(), groupMasterKey); + if (group != null) { + storeProfileKeysFromMembers(group); + } } if (group == null) { group = getDecryptedGroup(groupSecretParams); @@ -1637,15 +1712,7 @@ public class Manager implements Closeable { 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) { - } - } + storeProfileKeysFromMembers(group); return group; } catch (IOException | VerificationFailedException | InvalidGroupStateException e) { System.err.println("Failed to retrieve Group V2 info, ignoring ..."); @@ -1653,6 +1720,18 @@ public class Manager implements Closeable { } } + 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) { + } + } + } + private void retryFailedReceivedMessages( ReceiveMessageHandler handler, boolean ignoreAttachments ) { @@ -1863,7 +1942,7 @@ public class Manager implements Closeable { ) { List actions = new ArrayList<>(); if (content != null) { - SignalServiceAddress sender; + final SignalServiceAddress sender; if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { sender = envelope.getSourceAddress(); } else { @@ -1890,11 +1969,14 @@ public class Manager implements Closeable { SignalServiceSyncMessage syncMessage = content.getSyncMessage().get(); if (syncMessage.getSent().isPresent()) { SentTranscriptMessage message = syncMessage.getSent().get(); - actions.addAll(handleSignalServiceDataMessage(message.getMessage(), - true, - sender, - message.getDestination().orNull(), - ignoreAttachments)); + final SignalServiceAddress destination = message.getDestination().orNull(); + if (destination != null) { + actions.addAll(handleSignalServiceDataMessage(message.getMessage(), + true, + sender, + destination, + ignoreAttachments)); + } } if (syncMessage.getRequest().isPresent()) { RequestMessage rm = syncMessage.getRequest().get();