From 1f48ce1f39052745d46fd6b405559bf6df9c1ca3 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 20 Dec 2021 12:26:03 +0100 Subject: [PATCH] Implementing sending group messages with sender keys --- .../org/asamk/signal/manager/ManagerImpl.java | 5 +- .../org/asamk/signal/manager/TrustLevel.java | 7 + .../signal/manager/helper/GroupHelper.java | 27 ++- .../signal/manager/helper/IdentityHelper.java | 1 + .../signal/manager/helper/ProfileHelper.java | 1 + .../signal/manager/helper/SendHelper.java | 206 ++++++++++++++++-- .../signal/manager/storage/SignalAccount.java | 23 +- .../manager/storage/groups/GroupInfo.java | 3 + .../manager/storage/groups/GroupInfoV1.java | 6 + .../manager/storage/groups/GroupInfoV2.java | 19 +- .../manager/storage/groups/GroupStore.java | 12 +- .../storage/identities/IdentityKeyStore.java | 10 +- .../senderKeys/SenderKeyRecordStore.java | 35 ++- .../senderKeys/SenderKeySharedStore.java | 32 ++- .../storage/senderKeys/SenderKeyStore.java | 13 ++ .../asamk/signal/manager/util/IOUtils.java | 12 + 16 files changed, 359 insertions(+), 53 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index df757a8b..5c3f0e0b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -195,7 +195,7 @@ public class ManagerImpl implements Manager { unidentifiedAccessHelper::getAccessFor, this::resolveSignalServiceAddress); final GroupV2Helper groupV2Helper = new GroupV2Helper(profileHelper::getRecipientProfileKeyCredential, - this::getRecipientProfile, + profileHelper::getRecipientProfile, account::getSelfRecipientId, dependencies.getGroupsV2Operations(), dependencies.getGroupsV2Api(), @@ -207,6 +207,7 @@ public class ManagerImpl implements Manager { account.getRecipientStore(), this::handleIdentityFailure, this::getGroupInfo, + profileHelper::getRecipientProfile, this::refreshRegisteredUser); this.groupHelper = new GroupHelper(account, dependencies, @@ -245,7 +246,7 @@ public class ManagerImpl implements Manager { contactHelper, attachmentHelper, syncHelper, - this::getRecipientProfile, + profileHelper::getRecipientProfile, jobExecutor); this.identityHelper = new IdentityHelper(account, dependencies, diff --git a/lib/src/main/java/org/asamk/signal/manager/TrustLevel.java b/lib/src/main/java/org/asamk/signal/manager/TrustLevel.java index fead442c..9d58754c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/TrustLevel.java +++ b/lib/src/main/java/org/asamk/signal/manager/TrustLevel.java @@ -41,4 +41,11 @@ public enum TrustLevel { case TRUSTED_VERIFIED -> VerifiedMessage.VerifiedState.VERIFIED; }; } + + public boolean isTrusted() { + return switch (this) { + case TRUSTED_UNVERIFIED, TRUSTED_VERIFIED -> true; + case UNTRUSTED -> false; + }; + } } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index a0f6c89b..809788d5 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -44,6 +44,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2; import org.whispersystems.signalservice.api.push.ACI; +import org.whispersystems.signalservice.api.push.DistributionId; import org.whispersystems.signalservice.api.push.exceptions.ConflictException; import java.io.File; @@ -200,7 +201,9 @@ public class GroupHelper { final var messageBuilder = getGroupUpdateMessageBuilder(gv2, null); - final var result = sendGroupMessage(messageBuilder, gv2.getMembersIncludingPendingWithout(selfRecipientId)); + final var result = sendGroupMessage(messageBuilder, + gv2.getMembersIncludingPendingWithout(selfRecipientId), + gv2.getDistributionId()); return new Pair<>(gv2.getGroupId(), result); } @@ -333,7 +336,7 @@ public class GroupHelper { var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build()); // Send group info request message to the recipient who sent us a message with this groupId - return sendGroupMessage(messageBuilder, Set.of(recipientId)); + return sendGroupMessage(messageBuilder, Set.of(recipientId), null); } public SendGroupMessageResults sendGroupInfoMessage( @@ -353,7 +356,7 @@ public class GroupHelper { var messageBuilder = getGroupUpdateMessageBuilder(g); // Send group message only to the recipient who requested it - return sendGroupMessage(messageBuilder, Set.of(recipientId)); + return sendGroupMessage(messageBuilder, Set.of(recipientId), null); } private GroupInfo getGroup(GroupId groupId, boolean forceUpdate) { @@ -438,7 +441,9 @@ public class GroupHelper { account.getGroupStore().updateGroup(gv1); var messageBuilder = getGroupUpdateMessageBuilder(gv1); - return sendGroupMessage(messageBuilder, gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); + return sendGroupMessage(messageBuilder, + gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId()), + gv1.getDistributionId()); } private void updateGroupV1Details( @@ -600,7 +605,8 @@ public class GroupHelper { groupInfoV1.removeMember(account.getSelfRecipientId()); account.getGroupStore().updateGroup(groupInfoV1); return sendGroupMessage(messageBuilder, - groupInfoV1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); + groupInfoV1.getMembersIncludingPendingWithout(account.getSelfRecipientId()), + groupInfoV1.getDistributionId()); } private SendGroupMessageResults quitGroupV2( @@ -622,7 +628,8 @@ public class GroupHelper { var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray()); return sendGroupMessage(messageBuilder, - groupInfoV2.getMembersIncludingPendingWithout(account.getSelfRecipientId())); + groupInfoV2.getMembersIncludingPendingWithout(account.getSelfRecipientId()), + groupInfoV2.getDistributionId()); } private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException { @@ -664,15 +671,17 @@ public class GroupHelper { account.getGroupStore().updateGroup(group); final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray()); - return sendGroupMessage(messageBuilder, members); + return sendGroupMessage(messageBuilder, members, group.getDistributionId()); } private SendGroupMessageResults sendGroupMessage( - final SignalServiceDataMessage.Builder messageBuilder, final Set members + final SignalServiceDataMessage.Builder messageBuilder, + final Set members, + final DistributionId distributionId ) throws IOException { final var timestamp = System.currentTimeMillis(); messageBuilder.withTimestamp(timestamp); - final var results = sendHelper.sendGroupMessage(messageBuilder.build(), members); + final var results = sendHelper.sendGroupMessage(messageBuilder.build(), members, distributionId); return new SendGroupMessageResults(timestamp, results.stream() .map(sendMessageResult -> SendMessageResult.from(sendMessageResult, diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java index 531870d9..79afe863 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java @@ -126,6 +126,7 @@ public class IdentityHelper { final var newIdentity = account.getIdentityKeyStore().saveIdentity(recipientId, identityKey, new Date()); if (newIdentity) { account.getSessionStore().archiveSessions(recipientId); + account.getSenderKeyStore().deleteSharedWith(recipientId); } } else { // Retrieve profile to get the current identity key from the server diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java index bf788598..e903d807 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -247,6 +247,7 @@ public final class ProfileHelper { if (newIdentity) { account.getSessionStore().archiveSessions(recipientId); + account.getSenderKeyStore().deleteSharedWith(recipientId); } } catch (InvalidKeyException ignored) { logger.warn("Got invalid identity key in profile for {}", diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java index c8d8bbb7..41dccaa4 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -8,14 +8,19 @@ import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.groups.GroupInfo; +import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.InvalidRegistrationIdException; +import org.whispersystems.libsignal.NoSessionException; import org.whispersystems.libsignal.protocol.DecryptionErrorMessage; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.crypto.ContentHint; +import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; @@ -23,16 +28,22 @@ import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; +import org.whispersystems.signalservice.api.push.DistributionId; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.push.exceptions.NotFoundException; import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException; import org.whispersystems.signalservice.api.push.exceptions.RateLimitException; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; +import org.whispersystems.signalservice.internal.push.exceptions.InvalidUnidentifiedAccessHeaderException; import java.io.IOException; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; public class SendHelper { @@ -45,6 +56,7 @@ public class SendHelper { private final RecipientResolver recipientResolver; private final IdentityFailureHandler identityFailureHandler; private final GroupProvider groupProvider; + private final ProfileProvider profileProvider; private final RecipientRegistrationRefresher recipientRegistrationRefresher; public SendHelper( @@ -55,6 +67,7 @@ public class SendHelper { final RecipientResolver recipientResolver, final IdentityFailureHandler identityFailureHandler, final GroupProvider groupProvider, + final ProfileProvider profileProvider, final RecipientRegistrationRefresher recipientRegistrationRefresher ) { this.account = account; @@ -64,6 +77,7 @@ public class SendHelper { this.recipientResolver = recipientResolver; this.identityFailureHandler = identityFailureHandler; this.groupProvider = groupProvider; + this.profileProvider = profileProvider; this.recipientRegistrationRefresher = recipientRegistrationRefresher; } @@ -81,7 +95,7 @@ public class SendHelper { final var message = messageBuilder.build(); final var result = sendMessage(message, recipientId); - handlePossibleIdentityFailure(result); + handleSendMessageResult(result); return result; } @@ -116,7 +130,7 @@ public class SendHelper { } } - return sendGroupMessage(message, recipients); + return sendGroupMessage(message, recipients, g.getDistributionId()); } /** @@ -124,12 +138,14 @@ public class SendHelper { * This method should only be used for create/update/quit group messages. */ public List sendGroupMessage( - final SignalServiceDataMessage message, final Set recipientIds + final SignalServiceDataMessage message, + final Set recipientIds, + final DistributionId distributionId ) throws IOException { - List result = sendGroupMessageInternal(message, recipientIds); + List result = sendGroupMessageInternal(message, recipientIds, distributionId); for (var r : result) { - handlePossibleIdentityFailure(r); + handleSendMessageResult(r); } return result; @@ -245,27 +261,189 @@ public class SendHelper { } private List sendGroupMessageInternal( - final SignalServiceDataMessage message, final Set recipientIds + final SignalServiceDataMessage message, + final Set recipientIds, + final DistributionId distributionId ) throws IOException { + // isRecipientUpdate is true if we've already sent this message to some recipients in the past, otherwise false. + final var isRecipientUpdate = false; + Set senderKeyTargets = distributionId == null + ? Set.of() + : getSenderKeyCapableRecipientIds(recipientIds); + final var allResults = new ArrayList(recipientIds.size()); + + if (senderKeyTargets.size() > 0) { + final var results = sendGroupMessageInternalWithSenderKey(message, + senderKeyTargets, + distributionId, + isRecipientUpdate); + + if (results == null) { + senderKeyTargets = Set.of(); + } else { + results.stream().filter(SendMessageResult::isSuccess).forEach(allResults::add); + final var failedTargets = results.stream() + .filter(r -> !r.isSuccess()) + .map(r -> recipientResolver.resolveRecipient(r.getAddress())) + .toList(); + if (failedTargets.size() > 0) { + senderKeyTargets = new HashSet<>(senderKeyTargets); + failedTargets.forEach(senderKeyTargets::remove); + } + } + } + + final var legacyTargets = new HashSet<>(recipientIds); + legacyTargets.removeAll(senderKeyTargets); + final boolean onlyTargetIsSelfWithLinkedDevice = recipientIds.isEmpty() && account.isMultiDevice(); + + if (legacyTargets.size() > 0 || onlyTargetIsSelfWithLinkedDevice) { + if (legacyTargets.size() > 0) { + logger.debug("Need to do {} legacy sends.", legacyTargets.size()); + } else { + logger.debug("Need to do a legacy send to send a sync message for a group of only ourselves."); + } + + final List results = sendGroupMessageInternalWithLegacy(message, + legacyTargets, + isRecipientUpdate || allResults.size() > 0); + allResults.addAll(results); + } + + return allResults; + } + + private Set getSenderKeyCapableRecipientIds(final Set recipientIds) { + final var selfProfile = profileProvider.getProfile(account.getSelfRecipientId()); + if (selfProfile == null || !selfProfile.getCapabilities().contains(Profile.Capability.senderKey)) { + logger.debug("Not all of our devices support sender key. Using legacy."); + return Set.of(); + } + + final var senderKeyTargets = new HashSet(); + for (final var recipientId : recipientIds) { + // TODO filter out unregistered + final var profile = profileProvider.getProfile(recipientId); + if (profile == null || !profile.getCapabilities().contains(Profile.Capability.senderKey)) { + continue; + } + + final var access = unidentifiedAccessHelper.getAccessFor(recipientId); + if (!access.isPresent() || !access.get().getTargetUnidentifiedAccess().isPresent()) { + continue; + } + + final var identity = account.getIdentityKeyStore().getIdentity(recipientId); + if (identity == null || !identity.getTrustLevel().isTrusted()) { + continue; + } + + senderKeyTargets.add(recipientId); + } + + if (senderKeyTargets.size() < 2) { + logger.debug("Too few sender-key-capable users ({}). Doing all legacy sends.", senderKeyTargets.size()); + return Set.of(); + } + + logger.debug("Can use sender key for {}/{} recipients.", senderKeyTargets.size(), recipientIds.size()); + return senderKeyTargets; + } + + private List sendGroupMessageInternalWithLegacy( + final SignalServiceDataMessage message, final Set recipientIds, final boolean isRecipientUpdate + ) throws IOException { + final var recipientIdList = new ArrayList<>(recipientIds); + final var addresses = recipientIdList.stream().map(addressResolver::resolveSignalServiceAddress).toList(); + final var unidentifiedAccesses = unidentifiedAccessHelper.getAccessFor(recipientIdList); + final var messageSender = dependencies.getMessageSender(); try { - var messageSender = dependencies.getMessageSender(); - // isRecipientUpdate is true if we've already sent this message to some recipients in the past, otherwise false. - final var isRecipientUpdate = false; - final var recipientIdList = new ArrayList<>(recipientIds); - final var addresses = recipientIdList.stream().map(addressResolver::resolveSignalServiceAddress).toList(); - return messageSender.sendDataMessage(addresses, - unidentifiedAccessHelper.getAccessFor(recipientIdList), + final var results = messageSender.sendDataMessage(addresses, + unidentifiedAccesses, isRecipientUpdate, ContentHint.DEFAULT, message, SignalServiceMessageSender.LegacyGroupEvents.EMPTY, sendResult -> logger.trace("Partial message send result: {}", sendResult.isSuccess()), () -> false); + + final var successCount = results.stream().filter(SendMessageResult::isSuccess).count(); + logger.debug("Successfully sent using 1:1 to {}/{} legacy targets.", successCount, recipientIdList.size()); + return results; } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) { return List.of(); } } + private List sendGroupMessageInternalWithSenderKey( + final SignalServiceDataMessage message, + final Set recipientIds, + final DistributionId distributionId, + final boolean isRecipientUpdate + ) throws IOException { + final var recipientIdList = new ArrayList<>(recipientIds); + final var messageSender = dependencies.getMessageSender(); + + long keyCreateTime = account.getSenderKeyStore() + .getCreateTimeForOurKey(account.getSelfRecipientId(), account.getDeviceId(), distributionId); + long keyAge = System.currentTimeMillis() - keyCreateTime; + + if (keyCreateTime != -1 && keyAge > TimeUnit.DAYS.toMillis(14)) { + logger.debug("DistributionId {} was created at {} and is {} ms old (~{} days). Rotating.", + distributionId, + keyCreateTime, + keyAge, + TimeUnit.MILLISECONDS.toDays(keyAge)); + account.getSenderKeyStore().deleteOurKey(account.getSelfRecipientId(), distributionId); + } + + List addresses = recipientIdList.stream() + .map(addressResolver::resolveSignalServiceAddress) + .collect(Collectors.toList()); + List unidentifiedAccesses = recipientIdList.stream() + .map(unidentifiedAccessHelper::getAccessFor) + .map(Optional::get) + .map(UnidentifiedAccessPair::getTargetUnidentifiedAccess) + .map(Optional::get) + .collect(Collectors.toList()); + + try { + List results = messageSender.sendGroupDataMessage(distributionId, + addresses, + unidentifiedAccesses, + isRecipientUpdate, + ContentHint.DEFAULT, + message, + SignalServiceMessageSender.SenderKeyGroupEvents.EMPTY); + + final var successCount = results.stream().filter(SendMessageResult::isSuccess).count(); + logger.debug("Successfully sent using sender key to {}/{} sender key targets.", + successCount, + addresses.size()); + + return results; + } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) { + return null; + } catch (InvalidUnidentifiedAccessHeaderException e) { + logger.warn("Someone had a bad UD header. Falling back to legacy sends.", e); + return null; + } catch (NoSessionException e) { + logger.warn("No session. Falling back to legacy sends.", e); + account.getSenderKeyStore().deleteOurKey(account.getSelfRecipientId(), distributionId); + return null; + } catch (InvalidKeyException e) { + logger.warn("Invalid key. Falling back to legacy sends.", e); + account.getSenderKeyStore().deleteOurKey(account.getSelfRecipientId(), distributionId); + return null; + } catch (InvalidRegistrationIdException e) { + logger.warn("Invalid registrationId. Falling back to legacy sends.", e); + return null; + } catch (NotFoundException e) { + logger.warn("Someone was unregistered. Falling back to legacy sends.", e); + return null; + } + } + private SendMessageResult sendMessage( SignalServiceDataMessage message, RecipientId recipientId ) { @@ -317,7 +495,7 @@ public class SendHelper { return sendSyncMessage(syncMessage); } - private void handlePossibleIdentityFailure(final SendMessageResult r) { + private void handleSendMessageResult(final SendMessageResult r) { if (r.getIdentityFailure() != null) { final var recipientId = recipientResolver.resolveRecipient(r.getAddress()); identityFailureHandler.handleIdentityFailure(recipientId, r.getIdentityFailure()); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index 364c61e9..d823f641 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -10,6 +10,7 @@ import org.asamk.signal.manager.storage.configuration.ConfigurationStore; import org.asamk.signal.manager.storage.contacts.ContactsStore; import org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore; import org.asamk.signal.manager.storage.groups.GroupInfoV1; +import org.asamk.signal.manager.storage.groups.GroupInfoV2; import org.asamk.signal.manager.storage.groups.GroupStore; import org.asamk.signal.manager.storage.identities.IdentityKeyStore; import org.asamk.signal.manager.storage.identities.TrustNewIdentity; @@ -45,6 +46,7 @@ import org.whispersystems.libsignal.util.Medium; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.api.push.ACI; +import org.whispersystems.signalservice.api.push.DistributionId; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.storage.StorageKey; import org.whispersystems.signalservice.api.util.UuidUtil; @@ -69,7 +71,9 @@ public class SignalAccount implements Closeable { private final static Logger logger = LoggerFactory.getLogger(SignalAccount.class); private static final int MINIMUM_STORAGE_VERSION = 1; - private static final int CURRENT_STORAGE_VERSION = 2; + private static final int CURRENT_STORAGE_VERSION = 3; + + private int previousStorageVersion; private final ObjectMapper jsonProcessor = Utils.createStorageObjectMapper(); @@ -166,6 +170,7 @@ public class SignalAccount implements Closeable { signalAccount.registered = false; + signalAccount.previousStorageVersion = CURRENT_STORAGE_VERSION; signalAccount.migrateLegacyConfigs(); signalAccount.save(); @@ -274,6 +279,7 @@ public class SignalAccount implements Closeable { signalAccount.configurationStore = new ConfigurationStore(signalAccount::saveConfigurationStore); signalAccount.recipientStore.resolveRecipientTrusted(signalAccount.getSelfAddress()); + signalAccount.previousStorageVersion = CURRENT_STORAGE_VERSION; signalAccount.migrateLegacyConfigs(); signalAccount.save(); @@ -307,12 +313,20 @@ public class SignalAccount implements Closeable { setPassword(KeyUtils.createPassword()); } - if (getProfileKey() == null && isRegistered()) { + if (getProfileKey() == null) { // Old config file, creating new profile key setProfileKey(KeyUtils.createProfileKey()); } // Ensure our profile key is stored in profile store getProfileStore().storeProfileKey(getSelfRecipientId(), getProfileKey()); + if (previousStorageVersion < 3) { + for (final var group : groupStore.getGroups()) { + if (group instanceof GroupInfoV2 && group.getDistributionId() == null) { + ((GroupInfoV2) group).setDistributionId(DistributionId.create()); + groupStore.updateGroup(group); + } + } + } } private void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) { @@ -405,10 +419,13 @@ public class SignalAccount implements Closeable { } else if (accountVersion < MINIMUM_STORAGE_VERSION) { throw new IOException("Config file was created by a no longer supported older version!"); } + previousStorageVersion = accountVersion; } account = Utils.getNotNullNode(rootNode, "username").asText(); - password = Utils.getNotNullNode(rootNode, "password").asText(); + if (rootNode.hasNonNull("password")) { + password = rootNode.get("password").asText(); + } registered = Utils.getNotNullNode(rootNode, "registered").asBoolean(); if (rootNode.hasNonNull("uuid")) { try { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java index 2c210259..4d94eb01 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java @@ -4,6 +4,7 @@ import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.whispersystems.signalservice.api.push.DistributionId; import java.util.Set; import java.util.stream.Collectors; @@ -13,6 +14,8 @@ public sealed abstract class GroupInfo permits GroupInfoV1, GroupInfoV2 { public abstract GroupId getGroupId(); + public abstract DistributionId getDistributionId(); + public abstract String getTitle(); public String getDescription() { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java index 4e759e5f..8b103976 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java @@ -6,6 +6,7 @@ import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.whispersystems.signalservice.api.push.DistributionId; import java.util.Collection; import java.util.HashSet; @@ -54,6 +55,11 @@ public final class GroupInfoV1 extends GroupInfo { return groupId; } + @Override + public DistributionId getDistributionId() { + return null; + } + public GroupIdV2 getExpectedV2Id() { if (expectedV2Id == null) { expectedV2Id = GroupUtils.getGroupIdV2(groupId); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java index 752d42ba..eb6d5f29 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java @@ -11,6 +11,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.EnabledState; import org.signal.zkgroup.groups.GroupMasterKey; import org.whispersystems.signalservice.api.push.ACI; +import org.whispersystems.signalservice.api.push.DistributionId; import java.util.Set; import java.util.stream.Collectors; @@ -19,25 +20,29 @@ public final class GroupInfoV2 extends GroupInfo { private final GroupIdV2 groupId; private final GroupMasterKey masterKey; - + private DistributionId distributionId; private boolean blocked; - private DecryptedGroup group; // stored as a file with hexadecimal groupId as name - private RecipientResolver recipientResolver; + private DecryptedGroup group; // stored as a file with base64 groupId as name private boolean permissionDenied; + private RecipientResolver recipientResolver; + public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey) { this.groupId = groupId; this.masterKey = masterKey; + this.distributionId = DistributionId.create(); } public GroupInfoV2( final GroupIdV2 groupId, final GroupMasterKey masterKey, + final DistributionId distributionId, final boolean blocked, final boolean permissionDenied ) { this.groupId = groupId; this.masterKey = masterKey; + this.distributionId = distributionId; this.blocked = blocked; this.permissionDenied = permissionDenied; } @@ -51,6 +56,14 @@ public final class GroupInfoV2 extends GroupInfo { return masterKey; } + public DistributionId getDistributionId() { + return distributionId; + } + + public void setDistributionId(final DistributionId distributionId) { + this.distributionId = distributionId; + } + public void setGroup(final DecryptedGroup group, final RecipientResolver recipientResolver) { if (group != null) { this.permissionDenied = false; diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java index d77c9918..78c8b23e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java @@ -23,6 +23,7 @@ import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.groups.GroupMasterKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.push.DistributionId; import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.internal.util.Hex; @@ -105,7 +106,11 @@ public class GroupStore { throw new AssertionError("Invalid master key for group " + groupId.toBase64()); } - return new GroupInfoV2(groupId, masterKey, g2.blocked, g2.permissionDenied); + return new GroupInfoV2(groupId, + masterKey, + g2.distributionId == null ? null : DistributionId.from(g2.distributionId), + g2.blocked, + g2.permissionDenied); }).collect(Collectors.toMap(GroupInfo::getGroupId, g -> g)); return new GroupStore(groupCachePath, groups, recipientResolver, saver); @@ -268,6 +273,7 @@ public class GroupStore { final var g2 = (GroupInfoV2) g; return new Storage.GroupV2(g2.getGroupId().toBase64(), Base64.getEncoder().encodeToString(g2.getMasterKey().serialize()), + g2.getDistributionId() == null ? null : g2.getDistributionId().toString(), g2.isBlocked(), g2.isPermissionDenied()); }).toList()); @@ -334,7 +340,9 @@ public class GroupStore { } } - private record GroupV2(String groupId, String masterKey, boolean blocked, boolean permissionDenied) {} + private record GroupV2( + String groupId, String masterKey, String distributionId, boolean blocked, boolean permissionDenied + ) {} } private static class GroupsDeserializer extends JsonDeserializer> { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java index 0330b3b8..d7b25d23 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java @@ -120,16 +120,20 @@ public class IdentityKeyStore implements org.whispersystems.libsignal.state.Iden var recipientId = resolveRecipient(address.getName()); synchronized (cachedIdentities) { - final var identityInfo = loadIdentityLocked(recipientId); + // TODO implement possibility for different handling of incoming/outgoing trust decisions + var identityInfo = loadIdentityLocked(recipientId); if (identityInfo == null) { // Identity not found + saveIdentity(address, identityKey); return trustNewIdentity == TrustNewIdentity.ON_FIRST_USE; } - // TODO implement possibility for different handling of incoming/outgoing trust decisions if (!identityInfo.getIdentityKey().equals(identityKey)) { // Identity found, but different - return false; + if (direction == Direction.SENDING) { + saveIdentity(address, identityKey); + identityInfo = loadIdentityLocked(recipientId); + } } return identityInfo.isTrusted(); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java index 9b012e90..340a55ef 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java @@ -59,7 +59,28 @@ public class SenderKeyRecordStore implements org.whispersystems.libsignal.groups } } - public void deleteAll() { + long getCreateTimeForKey(final RecipientId selfRecipientId, final int selfDeviceId, final UUID distributionId) { + final var key = getKey(selfRecipientId, selfDeviceId, distributionId); + final var senderKeyFile = getSenderKeyFile(key); + + if (!senderKeyFile.exists()) { + return -1; + } + + return IOUtils.getFileCreateTime(senderKeyFile); + } + + void deleteSenderKey(final RecipientId recipientId, final UUID distributionId) { + synchronized (cachedSenderKeys) { + cachedSenderKeys.clear(); + final var keys = getKeysLocked(recipientId); + for (var key : keys) { + if (key.distributionId.equals(distributionId)) deleteSenderKeyLocked(key); + } + } + } + + void deleteAll() { synchronized (cachedSenderKeys) { cachedSenderKeys.clear(); final var files = senderKeysPath.listFiles((_file, s) -> senderKeyFileNamePattern.matcher(s).matches()); @@ -77,7 +98,7 @@ public class SenderKeyRecordStore implements org.whispersystems.libsignal.groups } } - public void deleteAllFor(final RecipientId recipientId) { + void deleteAllFor(final RecipientId recipientId) { synchronized (cachedSenderKeys) { cachedSenderKeys.clear(); final var keys = getKeysLocked(recipientId); @@ -87,7 +108,7 @@ public class SenderKeyRecordStore implements org.whispersystems.libsignal.groups } } - public void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) { + void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) { synchronized (cachedSenderKeys) { final var keys = getKeysLocked(toBeMergedRecipientId); final var otherHasSenderKeys = keys.size() > 0; @@ -120,6 +141,10 @@ public class SenderKeyRecordStore implements org.whispersystems.libsignal.groups return resolver.resolveRecipient(identifier); } + private Key getKey(final RecipientId recipientId, int deviceId, final UUID distributionId) { + return new Key(recipientId, deviceId, distributionId); + } + private Key getKey(final SignalProtocolAddress address, final UUID distributionId) { final var recipientId = resolveRecipient(address.getName()); return new Key(recipientId, address.getDeviceId(), distributionId); @@ -217,7 +242,5 @@ public class SenderKeyRecordStore implements org.whispersystems.libsignal.groups } } - private record Key(RecipientId recipientId, int deviceId, UUID distributionId) { - - } + private record Key(RecipientId recipientId, int deviceId, UUID distributionId) {} } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeySharedStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeySharedStore.java index c2447d48..43234165 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeySharedStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeySharedStore.java @@ -25,13 +25,14 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; public class SenderKeySharedStore { private final static Logger logger = LoggerFactory.getLogger(SenderKeySharedStore.class); - private final Map> sharedSenderKeys; + private final Map> sharedSenderKeys; private final ObjectMapper objectMapper; private final File file; @@ -45,19 +46,18 @@ public class SenderKeySharedStore { final var objectMapper = Utils.createStorageObjectMapper(); try (var inputStream = new FileInputStream(file)) { final var storage = objectMapper.readValue(inputStream, Storage.class); - final var sharedSenderKeys = new HashMap>(); + final var sharedSenderKeys = new HashMap>(); for (final var senderKey : storage.sharedSenderKeys) { final var recipientId = resolver.resolveRecipient(senderKey.recipientId); if (recipientId == null) { continue; } final var entry = new SenderKeySharedEntry(recipientId, senderKey.deviceId); - final var uuid = UuidUtil.parseOrNull(senderKey.distributionId); - if (uuid == null) { + final var distributionId = UuidUtil.parseOrNull(senderKey.distributionId); + if (distributionId == null) { logger.warn("Read invalid distribution id from storage {}, ignoring", senderKey.distributionId); continue; } - final var distributionId = DistributionId.from(uuid); var entries = sharedSenderKeys.get(distributionId); if (entries == null) { entries = new HashSet<>(); @@ -74,7 +74,7 @@ public class SenderKeySharedStore { } private SenderKeySharedStore( - final Map> sharedSenderKeys, + final Map> sharedSenderKeys, final ObjectMapper objectMapper, final File file, final RecipientAddressResolver addressResolver, @@ -89,8 +89,11 @@ public class SenderKeySharedStore { public Set getSenderKeySharedWith(final DistributionId distributionId) { synchronized (sharedSenderKeys) { - return sharedSenderKeys.get(distributionId) - .stream() + final var addresses = sharedSenderKeys.get(distributionId.asUuid()); + if (addresses == null) { + return Set.of(); + } + return addresses.stream() .map(k -> new SignalProtocolAddress(addressResolver.resolveRecipientAddress(k.recipientId()) .getIdentifier(), k.deviceId())) .collect(Collectors.toSet()); @@ -105,9 +108,9 @@ public class SenderKeySharedStore { .collect(Collectors.toSet()); synchronized (sharedSenderKeys) { - final var previousEntries = sharedSenderKeys.getOrDefault(distributionId, Set.of()); + final var previousEntries = sharedSenderKeys.getOrDefault(distributionId.asUuid(), Set.of()); - sharedSenderKeys.put(distributionId, new HashSet<>() { + sharedSenderKeys.put(distributionId.asUuid(), new HashSet<>() { { addAll(previousEntries); addAll(newEntries); @@ -158,6 +161,13 @@ public class SenderKeySharedStore { } } + public void deleteAllFor(final DistributionId distributionId) { + synchronized (sharedSenderKeys) { + sharedSenderKeys.remove(distributionId.asUuid()); + saveLocked(); + } + } + public void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) { synchronized (sharedSenderKeys) { for (final var distributionId : sharedSenderKeys.keySet()) { @@ -187,7 +197,7 @@ public class SenderKeySharedStore { return sharedWith.stream() .map(entry -> new Storage.SharedSenderKey(entry.recipientId().id(), entry.deviceId(), - pair.getKey().asUuid().toString())); + pair.getKey().toString())); }).toList()); // Write to memory first to prevent corrupting the file in case of serialization errors diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyStore.java index ba2a0322..3f08c389 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyStore.java @@ -68,6 +68,19 @@ public class SenderKeyStore implements SignalServiceSenderKeyStore { senderKeyRecordStore.deleteAllFor(recipientId); } + public void deleteSharedWith(RecipientId recipientId) { + senderKeySharedStore.deleteAllFor(recipientId); + } + + public void deleteOurKey(RecipientId selfRecipientId, DistributionId distributionId) { + senderKeySharedStore.deleteAllFor(distributionId); + senderKeyRecordStore.deleteSenderKey(selfRecipientId, distributionId.asUuid()); + } + + public long getCreateTimeForOurKey(RecipientId selfRecipientId, int deviceId, DistributionId distributionId) { + return senderKeyRecordStore.getCreateTimeForKey(selfRecipientId, deviceId, distributionId.asUuid()); + } + public void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) { senderKeySharedStore.mergeRecipients(recipientId, toBeMergedRecipientId); senderKeyRecordStore.mergeRecipients(recipientId, toBeMergedRecipientId); diff --git a/lib/src/main/java/org/asamk/signal/manager/util/IOUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/IOUtils.java index 3cc708d8..eebe6451 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/IOUtils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/IOUtils.java @@ -7,6 +7,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; import java.util.EnumSet; @@ -72,4 +74,14 @@ public class IOUtils { output.write(buffer, 0, read); } } + + public static long getFileCreateTime(final File file) { + try { + BasicFileAttributes attr = Files.readAttributes(file.toPath(), BasicFileAttributes.class); + FileTime fileTime = attr.creationTime(); + return fileTime.toMillis(); + } catch (IOException ex) { + return -1; + } + } } -- 2.50.1