mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 18:40:39 +00:00
Implementing sending group messages with sender keys
This commit is contained in:
parent
c134f1b78e
commit
1f48ce1f39
16 changed files with 359 additions and 53 deletions
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<RecipientId> members
|
||||
final SignalServiceDataMessage.Builder messageBuilder,
|
||||
final Set<RecipientId> 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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {}",
|
||||
|
|
|
@ -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<SendMessageResult> sendGroupMessage(
|
||||
final SignalServiceDataMessage message, final Set<RecipientId> recipientIds
|
||||
final SignalServiceDataMessage message,
|
||||
final Set<RecipientId> recipientIds,
|
||||
final DistributionId distributionId
|
||||
) throws IOException {
|
||||
List<SendMessageResult> result = sendGroupMessageInternal(message, recipientIds);
|
||||
List<SendMessageResult> result = sendGroupMessageInternal(message, recipientIds, distributionId);
|
||||
|
||||
for (var r : result) {
|
||||
handlePossibleIdentityFailure(r);
|
||||
handleSendMessageResult(r);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
@ -245,27 +261,189 @@ public class SendHelper {
|
|||
}
|
||||
|
||||
private List<SendMessageResult> sendGroupMessageInternal(
|
||||
final SignalServiceDataMessage message, final Set<RecipientId> recipientIds
|
||||
final SignalServiceDataMessage message,
|
||||
final Set<RecipientId> 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<RecipientId> senderKeyTargets = distributionId == null
|
||||
? Set.of()
|
||||
: getSenderKeyCapableRecipientIds(recipientIds);
|
||||
final var allResults = new ArrayList<SendMessageResult>(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<SendMessageResult> results = sendGroupMessageInternalWithLegacy(message,
|
||||
legacyTargets,
|
||||
isRecipientUpdate || allResults.size() > 0);
|
||||
allResults.addAll(results);
|
||||
}
|
||||
|
||||
return allResults;
|
||||
}
|
||||
|
||||
private Set<RecipientId> getSenderKeyCapableRecipientIds(final Set<RecipientId> 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<RecipientId>();
|
||||
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<SendMessageResult> sendGroupMessageInternalWithLegacy(
|
||||
final SignalServiceDataMessage message, final Set<RecipientId> 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<SendMessageResult> sendGroupMessageInternalWithSenderKey(
|
||||
final SignalServiceDataMessage message,
|
||||
final Set<RecipientId> 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<SignalServiceAddress> addresses = recipientIdList.stream()
|
||||
.map(addressResolver::resolveSignalServiceAddress)
|
||||
.collect(Collectors.toList());
|
||||
List<UnidentifiedAccess> unidentifiedAccesses = recipientIdList.stream()
|
||||
.map(unidentifiedAccessHelper::getAccessFor)
|
||||
.map(Optional::get)
|
||||
.map(UnidentifiedAccessPair::getTargetUnidentifiedAccess)
|
||||
.map(Optional::get)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
try {
|
||||
List<SendMessageResult> 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());
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<List<Object>> {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {}
|
||||
}
|
||||
|
|
|
@ -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<DistributionId, Set<SenderKeySharedEntry>> sharedSenderKeys;
|
||||
private final Map<UUID, Set<SenderKeySharedEntry>> 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<DistributionId, Set<SenderKeySharedEntry>>();
|
||||
final var sharedSenderKeys = new HashMap<UUID, Set<SenderKeySharedEntry>>();
|
||||
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<DistributionId, Set<SenderKeySharedEntry>> sharedSenderKeys,
|
||||
final Map<UUID, Set<SenderKeySharedEntry>> sharedSenderKeys,
|
||||
final ObjectMapper objectMapper,
|
||||
final File file,
|
||||
final RecipientAddressResolver addressResolver,
|
||||
|
@ -89,8 +89,11 @@ public class SenderKeySharedStore {
|
|||
|
||||
public Set<SignalProtocolAddress> 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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue