mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 10:30:38 +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,
|
unidentifiedAccessHelper::getAccessFor,
|
||||||
this::resolveSignalServiceAddress);
|
this::resolveSignalServiceAddress);
|
||||||
final GroupV2Helper groupV2Helper = new GroupV2Helper(profileHelper::getRecipientProfileKeyCredential,
|
final GroupV2Helper groupV2Helper = new GroupV2Helper(profileHelper::getRecipientProfileKeyCredential,
|
||||||
this::getRecipientProfile,
|
profileHelper::getRecipientProfile,
|
||||||
account::getSelfRecipientId,
|
account::getSelfRecipientId,
|
||||||
dependencies.getGroupsV2Operations(),
|
dependencies.getGroupsV2Operations(),
|
||||||
dependencies.getGroupsV2Api(),
|
dependencies.getGroupsV2Api(),
|
||||||
|
@ -207,6 +207,7 @@ public class ManagerImpl implements Manager {
|
||||||
account.getRecipientStore(),
|
account.getRecipientStore(),
|
||||||
this::handleIdentityFailure,
|
this::handleIdentityFailure,
|
||||||
this::getGroupInfo,
|
this::getGroupInfo,
|
||||||
|
profileHelper::getRecipientProfile,
|
||||||
this::refreshRegisteredUser);
|
this::refreshRegisteredUser);
|
||||||
this.groupHelper = new GroupHelper(account,
|
this.groupHelper = new GroupHelper(account,
|
||||||
dependencies,
|
dependencies,
|
||||||
|
@ -245,7 +246,7 @@ public class ManagerImpl implements Manager {
|
||||||
contactHelper,
|
contactHelper,
|
||||||
attachmentHelper,
|
attachmentHelper,
|
||||||
syncHelper,
|
syncHelper,
|
||||||
this::getRecipientProfile,
|
profileHelper::getRecipientProfile,
|
||||||
jobExecutor);
|
jobExecutor);
|
||||||
this.identityHelper = new IdentityHelper(account,
|
this.identityHelper = new IdentityHelper(account,
|
||||||
dependencies,
|
dependencies,
|
||||||
|
|
|
@ -41,4 +41,11 @@ public enum TrustLevel {
|
||||||
case TRUSTED_VERIFIED -> VerifiedMessage.VerifiedState.VERIFIED;
|
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.SignalServiceGroup;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
|
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
|
||||||
import org.whispersystems.signalservice.api.push.ACI;
|
import org.whispersystems.signalservice.api.push.ACI;
|
||||||
|
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.ConflictException;
|
import org.whispersystems.signalservice.api.push.exceptions.ConflictException;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
@ -200,7 +201,9 @@ public class GroupHelper {
|
||||||
|
|
||||||
final var messageBuilder = getGroupUpdateMessageBuilder(gv2, null);
|
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);
|
return new Pair<>(gv2.getGroupId(), result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -333,7 +336,7 @@ public class GroupHelper {
|
||||||
var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build());
|
var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build());
|
||||||
|
|
||||||
// Send group info request message to the recipient who sent us a message with this groupId
|
// 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(
|
public SendGroupMessageResults sendGroupInfoMessage(
|
||||||
|
@ -353,7 +356,7 @@ public class GroupHelper {
|
||||||
var messageBuilder = getGroupUpdateMessageBuilder(g);
|
var messageBuilder = getGroupUpdateMessageBuilder(g);
|
||||||
|
|
||||||
// Send group message only to the recipient who requested it
|
// 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) {
|
private GroupInfo getGroup(GroupId groupId, boolean forceUpdate) {
|
||||||
|
@ -438,7 +441,9 @@ public class GroupHelper {
|
||||||
account.getGroupStore().updateGroup(gv1);
|
account.getGroupStore().updateGroup(gv1);
|
||||||
|
|
||||||
var messageBuilder = getGroupUpdateMessageBuilder(gv1);
|
var messageBuilder = getGroupUpdateMessageBuilder(gv1);
|
||||||
return sendGroupMessage(messageBuilder, gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
|
return sendGroupMessage(messageBuilder,
|
||||||
|
gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId()),
|
||||||
|
gv1.getDistributionId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateGroupV1Details(
|
private void updateGroupV1Details(
|
||||||
|
@ -600,7 +605,8 @@ public class GroupHelper {
|
||||||
groupInfoV1.removeMember(account.getSelfRecipientId());
|
groupInfoV1.removeMember(account.getSelfRecipientId());
|
||||||
account.getGroupStore().updateGroup(groupInfoV1);
|
account.getGroupStore().updateGroup(groupInfoV1);
|
||||||
return sendGroupMessage(messageBuilder,
|
return sendGroupMessage(messageBuilder,
|
||||||
groupInfoV1.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
|
groupInfoV1.getMembersIncludingPendingWithout(account.getSelfRecipientId()),
|
||||||
|
groupInfoV1.getDistributionId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private SendGroupMessageResults quitGroupV2(
|
private SendGroupMessageResults quitGroupV2(
|
||||||
|
@ -622,7 +628,8 @@ public class GroupHelper {
|
||||||
|
|
||||||
var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray());
|
var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray());
|
||||||
return sendGroupMessage(messageBuilder,
|
return sendGroupMessage(messageBuilder,
|
||||||
groupInfoV2.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
|
groupInfoV2.getMembersIncludingPendingWithout(account.getSelfRecipientId()),
|
||||||
|
groupInfoV2.getDistributionId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException {
|
private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException {
|
||||||
|
@ -664,15 +671,17 @@ public class GroupHelper {
|
||||||
account.getGroupStore().updateGroup(group);
|
account.getGroupStore().updateGroup(group);
|
||||||
|
|
||||||
final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray());
|
final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray());
|
||||||
return sendGroupMessage(messageBuilder, members);
|
return sendGroupMessage(messageBuilder, members, group.getDistributionId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private SendGroupMessageResults sendGroupMessage(
|
private SendGroupMessageResults sendGroupMessage(
|
||||||
final SignalServiceDataMessage.Builder messageBuilder, final Set<RecipientId> members
|
final SignalServiceDataMessage.Builder messageBuilder,
|
||||||
|
final Set<RecipientId> members,
|
||||||
|
final DistributionId distributionId
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
final var timestamp = System.currentTimeMillis();
|
final var timestamp = System.currentTimeMillis();
|
||||||
messageBuilder.withTimestamp(timestamp);
|
messageBuilder.withTimestamp(timestamp);
|
||||||
final var results = sendHelper.sendGroupMessage(messageBuilder.build(), members);
|
final var results = sendHelper.sendGroupMessage(messageBuilder.build(), members, distributionId);
|
||||||
return new SendGroupMessageResults(timestamp,
|
return new SendGroupMessageResults(timestamp,
|
||||||
results.stream()
|
results.stream()
|
||||||
.map(sendMessageResult -> SendMessageResult.from(sendMessageResult,
|
.map(sendMessageResult -> SendMessageResult.from(sendMessageResult,
|
||||||
|
|
|
@ -126,6 +126,7 @@ public class IdentityHelper {
|
||||||
final var newIdentity = account.getIdentityKeyStore().saveIdentity(recipientId, identityKey, new Date());
|
final var newIdentity = account.getIdentityKeyStore().saveIdentity(recipientId, identityKey, new Date());
|
||||||
if (newIdentity) {
|
if (newIdentity) {
|
||||||
account.getSessionStore().archiveSessions(recipientId);
|
account.getSessionStore().archiveSessions(recipientId);
|
||||||
|
account.getSenderKeyStore().deleteSharedWith(recipientId);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Retrieve profile to get the current identity key from the server
|
// Retrieve profile to get the current identity key from the server
|
||||||
|
|
|
@ -247,6 +247,7 @@ public final class ProfileHelper {
|
||||||
|
|
||||||
if (newIdentity) {
|
if (newIdentity) {
|
||||||
account.getSessionStore().archiveSessions(recipientId);
|
account.getSessionStore().archiveSessions(recipientId);
|
||||||
|
account.getSenderKeyStore().deleteSharedWith(recipientId);
|
||||||
}
|
}
|
||||||
} catch (InvalidKeyException ignored) {
|
} catch (InvalidKeyException ignored) {
|
||||||
logger.warn("Got invalid identity key in profile for {}",
|
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.groups.NotAGroupMemberException;
|
||||||
import org.asamk.signal.manager.storage.SignalAccount;
|
import org.asamk.signal.manager.storage.SignalAccount;
|
||||||
import org.asamk.signal.manager.storage.groups.GroupInfo;
|
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.RecipientId;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
|
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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.protocol.DecryptionErrorMessage;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||||
import org.whispersystems.signalservice.api.crypto.ContentHint;
|
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.crypto.UnidentifiedAccessPair;
|
||||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
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.SignalServiceTypingMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
|
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
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.SignalServiceAddress;
|
||||||
|
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
|
import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
|
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
|
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
|
||||||
|
import org.whispersystems.signalservice.internal.push.exceptions.InvalidUnidentifiedAccessHeaderException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class SendHelper {
|
public class SendHelper {
|
||||||
|
|
||||||
|
@ -45,6 +56,7 @@ public class SendHelper {
|
||||||
private final RecipientResolver recipientResolver;
|
private final RecipientResolver recipientResolver;
|
||||||
private final IdentityFailureHandler identityFailureHandler;
|
private final IdentityFailureHandler identityFailureHandler;
|
||||||
private final GroupProvider groupProvider;
|
private final GroupProvider groupProvider;
|
||||||
|
private final ProfileProvider profileProvider;
|
||||||
private final RecipientRegistrationRefresher recipientRegistrationRefresher;
|
private final RecipientRegistrationRefresher recipientRegistrationRefresher;
|
||||||
|
|
||||||
public SendHelper(
|
public SendHelper(
|
||||||
|
@ -55,6 +67,7 @@ public class SendHelper {
|
||||||
final RecipientResolver recipientResolver,
|
final RecipientResolver recipientResolver,
|
||||||
final IdentityFailureHandler identityFailureHandler,
|
final IdentityFailureHandler identityFailureHandler,
|
||||||
final GroupProvider groupProvider,
|
final GroupProvider groupProvider,
|
||||||
|
final ProfileProvider profileProvider,
|
||||||
final RecipientRegistrationRefresher recipientRegistrationRefresher
|
final RecipientRegistrationRefresher recipientRegistrationRefresher
|
||||||
) {
|
) {
|
||||||
this.account = account;
|
this.account = account;
|
||||||
|
@ -64,6 +77,7 @@ public class SendHelper {
|
||||||
this.recipientResolver = recipientResolver;
|
this.recipientResolver = recipientResolver;
|
||||||
this.identityFailureHandler = identityFailureHandler;
|
this.identityFailureHandler = identityFailureHandler;
|
||||||
this.groupProvider = groupProvider;
|
this.groupProvider = groupProvider;
|
||||||
|
this.profileProvider = profileProvider;
|
||||||
this.recipientRegistrationRefresher = recipientRegistrationRefresher;
|
this.recipientRegistrationRefresher = recipientRegistrationRefresher;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,7 +95,7 @@ public class SendHelper {
|
||||||
|
|
||||||
final var message = messageBuilder.build();
|
final var message = messageBuilder.build();
|
||||||
final var result = sendMessage(message, recipientId);
|
final var result = sendMessage(message, recipientId);
|
||||||
handlePossibleIdentityFailure(result);
|
handleSendMessageResult(result);
|
||||||
return 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.
|
* This method should only be used for create/update/quit group messages.
|
||||||
*/
|
*/
|
||||||
public List<SendMessageResult> sendGroupMessage(
|
public List<SendMessageResult> sendGroupMessage(
|
||||||
final SignalServiceDataMessage message, final Set<RecipientId> recipientIds
|
final SignalServiceDataMessage message,
|
||||||
|
final Set<RecipientId> recipientIds,
|
||||||
|
final DistributionId distributionId
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
List<SendMessageResult> result = sendGroupMessageInternal(message, recipientIds);
|
List<SendMessageResult> result = sendGroupMessageInternal(message, recipientIds, distributionId);
|
||||||
|
|
||||||
for (var r : result) {
|
for (var r : result) {
|
||||||
handlePossibleIdentityFailure(r);
|
handleSendMessageResult(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -245,27 +261,189 @@ public class SendHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<SendMessageResult> sendGroupMessageInternal(
|
private List<SendMessageResult> sendGroupMessageInternal(
|
||||||
final SignalServiceDataMessage message, final Set<RecipientId> recipientIds
|
final SignalServiceDataMessage message,
|
||||||
|
final Set<RecipientId> recipientIds,
|
||||||
|
final DistributionId distributionId
|
||||||
) throws IOException {
|
) 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 {
|
try {
|
||||||
var messageSender = dependencies.getMessageSender();
|
final var results = messageSender.sendDataMessage(addresses,
|
||||||
// isRecipientUpdate is true if we've already sent this message to some recipients in the past, otherwise false.
|
unidentifiedAccesses,
|
||||||
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),
|
|
||||||
isRecipientUpdate,
|
isRecipientUpdate,
|
||||||
ContentHint.DEFAULT,
|
ContentHint.DEFAULT,
|
||||||
message,
|
message,
|
||||||
SignalServiceMessageSender.LegacyGroupEvents.EMPTY,
|
SignalServiceMessageSender.LegacyGroupEvents.EMPTY,
|
||||||
sendResult -> logger.trace("Partial message send result: {}", sendResult.isSuccess()),
|
sendResult -> logger.trace("Partial message send result: {}", sendResult.isSuccess()),
|
||||||
() -> false);
|
() -> 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) {
|
} catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) {
|
||||||
return List.of();
|
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(
|
private SendMessageResult sendMessage(
|
||||||
SignalServiceDataMessage message, RecipientId recipientId
|
SignalServiceDataMessage message, RecipientId recipientId
|
||||||
) {
|
) {
|
||||||
|
@ -317,7 +495,7 @@ public class SendHelper {
|
||||||
return sendSyncMessage(syncMessage);
|
return sendSyncMessage(syncMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handlePossibleIdentityFailure(final SendMessageResult r) {
|
private void handleSendMessageResult(final SendMessageResult r) {
|
||||||
if (r.getIdentityFailure() != null) {
|
if (r.getIdentityFailure() != null) {
|
||||||
final var recipientId = recipientResolver.resolveRecipient(r.getAddress());
|
final var recipientId = recipientResolver.resolveRecipient(r.getAddress());
|
||||||
identityFailureHandler.handleIdentityFailure(recipientId, r.getIdentityFailure());
|
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.ContactsStore;
|
||||||
import org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore;
|
import org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore;
|
||||||
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
|
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.groups.GroupStore;
|
||||||
import org.asamk.signal.manager.storage.identities.IdentityKeyStore;
|
import org.asamk.signal.manager.storage.identities.IdentityKeyStore;
|
||||||
import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
|
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.crypto.UnidentifiedAccess;
|
||||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||||
import org.whispersystems.signalservice.api.push.ACI;
|
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.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
import org.whispersystems.signalservice.api.storage.StorageKey;
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
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 final static Logger logger = LoggerFactory.getLogger(SignalAccount.class);
|
||||||
|
|
||||||
private static final int MINIMUM_STORAGE_VERSION = 1;
|
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();
|
private final ObjectMapper jsonProcessor = Utils.createStorageObjectMapper();
|
||||||
|
|
||||||
|
@ -166,6 +170,7 @@ public class SignalAccount implements Closeable {
|
||||||
|
|
||||||
signalAccount.registered = false;
|
signalAccount.registered = false;
|
||||||
|
|
||||||
|
signalAccount.previousStorageVersion = CURRENT_STORAGE_VERSION;
|
||||||
signalAccount.migrateLegacyConfigs();
|
signalAccount.migrateLegacyConfigs();
|
||||||
signalAccount.save();
|
signalAccount.save();
|
||||||
|
|
||||||
|
@ -274,6 +279,7 @@ public class SignalAccount implements Closeable {
|
||||||
signalAccount.configurationStore = new ConfigurationStore(signalAccount::saveConfigurationStore);
|
signalAccount.configurationStore = new ConfigurationStore(signalAccount::saveConfigurationStore);
|
||||||
|
|
||||||
signalAccount.recipientStore.resolveRecipientTrusted(signalAccount.getSelfAddress());
|
signalAccount.recipientStore.resolveRecipientTrusted(signalAccount.getSelfAddress());
|
||||||
|
signalAccount.previousStorageVersion = CURRENT_STORAGE_VERSION;
|
||||||
signalAccount.migrateLegacyConfigs();
|
signalAccount.migrateLegacyConfigs();
|
||||||
signalAccount.save();
|
signalAccount.save();
|
||||||
|
|
||||||
|
@ -307,12 +313,20 @@ public class SignalAccount implements Closeable {
|
||||||
setPassword(KeyUtils.createPassword());
|
setPassword(KeyUtils.createPassword());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getProfileKey() == null && isRegistered()) {
|
if (getProfileKey() == null) {
|
||||||
// Old config file, creating new profile key
|
// Old config file, creating new profile key
|
||||||
setProfileKey(KeyUtils.createProfileKey());
|
setProfileKey(KeyUtils.createProfileKey());
|
||||||
}
|
}
|
||||||
// Ensure our profile key is stored in profile store
|
// Ensure our profile key is stored in profile store
|
||||||
getProfileStore().storeProfileKey(getSelfRecipientId(), getProfileKey());
|
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) {
|
private void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
|
||||||
|
@ -405,10 +419,13 @@ public class SignalAccount implements Closeable {
|
||||||
} else if (accountVersion < MINIMUM_STORAGE_VERSION) {
|
} else if (accountVersion < MINIMUM_STORAGE_VERSION) {
|
||||||
throw new IOException("Config file was created by a no longer supported older version!");
|
throw new IOException("Config file was created by a no longer supported older version!");
|
||||||
}
|
}
|
||||||
|
previousStorageVersion = accountVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
account = Utils.getNotNullNode(rootNode, "username").asText();
|
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();
|
registered = Utils.getNotNullNode(rootNode, "registered").asBoolean();
|
||||||
if (rootNode.hasNonNull("uuid")) {
|
if (rootNode.hasNonNull("uuid")) {
|
||||||
try {
|
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.GroupInviteLinkUrl;
|
||||||
import org.asamk.signal.manager.groups.GroupPermission;
|
import org.asamk.signal.manager.groups.GroupPermission;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
|
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
@ -13,6 +14,8 @@ public sealed abstract class GroupInfo permits GroupInfoV1, GroupInfoV2 {
|
||||||
|
|
||||||
public abstract GroupId getGroupId();
|
public abstract GroupId getGroupId();
|
||||||
|
|
||||||
|
public abstract DistributionId getDistributionId();
|
||||||
|
|
||||||
public abstract String getTitle();
|
public abstract String getTitle();
|
||||||
|
|
||||||
public String getDescription() {
|
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.GroupPermission;
|
||||||
import org.asamk.signal.manager.groups.GroupUtils;
|
import org.asamk.signal.manager.groups.GroupUtils;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
|
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
@ -54,6 +55,11 @@ public final class GroupInfoV1 extends GroupInfo {
|
||||||
return groupId;
|
return groupId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DistributionId getDistributionId() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public GroupIdV2 getExpectedV2Id() {
|
public GroupIdV2 getExpectedV2Id() {
|
||||||
if (expectedV2Id == null) {
|
if (expectedV2Id == null) {
|
||||||
expectedV2Id = GroupUtils.getGroupIdV2(groupId);
|
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.storageservice.protos.groups.local.EnabledState;
|
||||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||||
import org.whispersystems.signalservice.api.push.ACI;
|
import org.whispersystems.signalservice.api.push.ACI;
|
||||||
|
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
@ -19,25 +20,29 @@ public final class GroupInfoV2 extends GroupInfo {
|
||||||
|
|
||||||
private final GroupIdV2 groupId;
|
private final GroupIdV2 groupId;
|
||||||
private final GroupMasterKey masterKey;
|
private final GroupMasterKey masterKey;
|
||||||
|
private DistributionId distributionId;
|
||||||
private boolean blocked;
|
private boolean blocked;
|
||||||
private DecryptedGroup group; // stored as a file with hexadecimal groupId as name
|
private DecryptedGroup group; // stored as a file with base64 groupId as name
|
||||||
private RecipientResolver recipientResolver;
|
|
||||||
private boolean permissionDenied;
|
private boolean permissionDenied;
|
||||||
|
|
||||||
|
private RecipientResolver recipientResolver;
|
||||||
|
|
||||||
public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey) {
|
public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey) {
|
||||||
this.groupId = groupId;
|
this.groupId = groupId;
|
||||||
this.masterKey = masterKey;
|
this.masterKey = masterKey;
|
||||||
|
this.distributionId = DistributionId.create();
|
||||||
}
|
}
|
||||||
|
|
||||||
public GroupInfoV2(
|
public GroupInfoV2(
|
||||||
final GroupIdV2 groupId,
|
final GroupIdV2 groupId,
|
||||||
final GroupMasterKey masterKey,
|
final GroupMasterKey masterKey,
|
||||||
|
final DistributionId distributionId,
|
||||||
final boolean blocked,
|
final boolean blocked,
|
||||||
final boolean permissionDenied
|
final boolean permissionDenied
|
||||||
) {
|
) {
|
||||||
this.groupId = groupId;
|
this.groupId = groupId;
|
||||||
this.masterKey = masterKey;
|
this.masterKey = masterKey;
|
||||||
|
this.distributionId = distributionId;
|
||||||
this.blocked = blocked;
|
this.blocked = blocked;
|
||||||
this.permissionDenied = permissionDenied;
|
this.permissionDenied = permissionDenied;
|
||||||
}
|
}
|
||||||
|
@ -51,6 +56,14 @@ public final class GroupInfoV2 extends GroupInfo {
|
||||||
return masterKey;
|
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) {
|
public void setGroup(final DecryptedGroup group, final RecipientResolver recipientResolver) {
|
||||||
if (group != null) {
|
if (group != null) {
|
||||||
this.permissionDenied = false;
|
this.permissionDenied = false;
|
||||||
|
|
|
@ -23,6 +23,7 @@ import org.signal.zkgroup.InvalidInputException;
|
||||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||||
import org.whispersystems.signalservice.internal.util.Hex;
|
import org.whispersystems.signalservice.internal.util.Hex;
|
||||||
|
|
||||||
|
@ -105,7 +106,11 @@ public class GroupStore {
|
||||||
throw new AssertionError("Invalid master key for group " + groupId.toBase64());
|
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));
|
}).collect(Collectors.toMap(GroupInfo::getGroupId, g -> g));
|
||||||
|
|
||||||
return new GroupStore(groupCachePath, groups, recipientResolver, saver);
|
return new GroupStore(groupCachePath, groups, recipientResolver, saver);
|
||||||
|
@ -268,6 +273,7 @@ public class GroupStore {
|
||||||
final var g2 = (GroupInfoV2) g;
|
final var g2 = (GroupInfoV2) g;
|
||||||
return new Storage.GroupV2(g2.getGroupId().toBase64(),
|
return new Storage.GroupV2(g2.getGroupId().toBase64(),
|
||||||
Base64.getEncoder().encodeToString(g2.getMasterKey().serialize()),
|
Base64.getEncoder().encodeToString(g2.getMasterKey().serialize()),
|
||||||
|
g2.getDistributionId() == null ? null : g2.getDistributionId().toString(),
|
||||||
g2.isBlocked(),
|
g2.isBlocked(),
|
||||||
g2.isPermissionDenied());
|
g2.isPermissionDenied());
|
||||||
}).toList());
|
}).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>> {
|
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());
|
var recipientId = resolveRecipient(address.getName());
|
||||||
|
|
||||||
synchronized (cachedIdentities) {
|
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) {
|
if (identityInfo == null) {
|
||||||
// Identity not found
|
// Identity not found
|
||||||
|
saveIdentity(address, identityKey);
|
||||||
return trustNewIdentity == TrustNewIdentity.ON_FIRST_USE;
|
return trustNewIdentity == TrustNewIdentity.ON_FIRST_USE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO implement possibility for different handling of incoming/outgoing trust decisions
|
|
||||||
if (!identityInfo.getIdentityKey().equals(identityKey)) {
|
if (!identityInfo.getIdentityKey().equals(identityKey)) {
|
||||||
// Identity found, but different
|
// Identity found, but different
|
||||||
return false;
|
if (direction == Direction.SENDING) {
|
||||||
|
saveIdentity(address, identityKey);
|
||||||
|
identityInfo = loadIdentityLocked(recipientId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return identityInfo.isTrusted();
|
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) {
|
synchronized (cachedSenderKeys) {
|
||||||
cachedSenderKeys.clear();
|
cachedSenderKeys.clear();
|
||||||
final var files = senderKeysPath.listFiles((_file, s) -> senderKeyFileNamePattern.matcher(s).matches());
|
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) {
|
synchronized (cachedSenderKeys) {
|
||||||
cachedSenderKeys.clear();
|
cachedSenderKeys.clear();
|
||||||
final var keys = getKeysLocked(recipientId);
|
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) {
|
synchronized (cachedSenderKeys) {
|
||||||
final var keys = getKeysLocked(toBeMergedRecipientId);
|
final var keys = getKeysLocked(toBeMergedRecipientId);
|
||||||
final var otherHasSenderKeys = keys.size() > 0;
|
final var otherHasSenderKeys = keys.size() > 0;
|
||||||
|
@ -120,6 +141,10 @@ public class SenderKeyRecordStore implements org.whispersystems.libsignal.groups
|
||||||
return resolver.resolveRecipient(identifier);
|
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) {
|
private Key getKey(final SignalProtocolAddress address, final UUID distributionId) {
|
||||||
final var recipientId = resolveRecipient(address.getName());
|
final var recipientId = resolveRecipient(address.getName());
|
||||||
return new Key(recipientId, address.getDeviceId(), distributionId);
|
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.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class SenderKeySharedStore {
|
public class SenderKeySharedStore {
|
||||||
|
|
||||||
private final static Logger logger = LoggerFactory.getLogger(SenderKeySharedStore.class);
|
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 ObjectMapper objectMapper;
|
||||||
private final File file;
|
private final File file;
|
||||||
|
@ -45,19 +46,18 @@ public class SenderKeySharedStore {
|
||||||
final var objectMapper = Utils.createStorageObjectMapper();
|
final var objectMapper = Utils.createStorageObjectMapper();
|
||||||
try (var inputStream = new FileInputStream(file)) {
|
try (var inputStream = new FileInputStream(file)) {
|
||||||
final var storage = objectMapper.readValue(inputStream, Storage.class);
|
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) {
|
for (final var senderKey : storage.sharedSenderKeys) {
|
||||||
final var recipientId = resolver.resolveRecipient(senderKey.recipientId);
|
final var recipientId = resolver.resolveRecipient(senderKey.recipientId);
|
||||||
if (recipientId == null) {
|
if (recipientId == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
final var entry = new SenderKeySharedEntry(recipientId, senderKey.deviceId);
|
final var entry = new SenderKeySharedEntry(recipientId, senderKey.deviceId);
|
||||||
final var uuid = UuidUtil.parseOrNull(senderKey.distributionId);
|
final var distributionId = UuidUtil.parseOrNull(senderKey.distributionId);
|
||||||
if (uuid == null) {
|
if (distributionId == null) {
|
||||||
logger.warn("Read invalid distribution id from storage {}, ignoring", senderKey.distributionId);
|
logger.warn("Read invalid distribution id from storage {}, ignoring", senderKey.distributionId);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
final var distributionId = DistributionId.from(uuid);
|
|
||||||
var entries = sharedSenderKeys.get(distributionId);
|
var entries = sharedSenderKeys.get(distributionId);
|
||||||
if (entries == null) {
|
if (entries == null) {
|
||||||
entries = new HashSet<>();
|
entries = new HashSet<>();
|
||||||
|
@ -74,7 +74,7 @@ public class SenderKeySharedStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
private SenderKeySharedStore(
|
private SenderKeySharedStore(
|
||||||
final Map<DistributionId, Set<SenderKeySharedEntry>> sharedSenderKeys,
|
final Map<UUID, Set<SenderKeySharedEntry>> sharedSenderKeys,
|
||||||
final ObjectMapper objectMapper,
|
final ObjectMapper objectMapper,
|
||||||
final File file,
|
final File file,
|
||||||
final RecipientAddressResolver addressResolver,
|
final RecipientAddressResolver addressResolver,
|
||||||
|
@ -89,8 +89,11 @@ public class SenderKeySharedStore {
|
||||||
|
|
||||||
public Set<SignalProtocolAddress> getSenderKeySharedWith(final DistributionId distributionId) {
|
public Set<SignalProtocolAddress> getSenderKeySharedWith(final DistributionId distributionId) {
|
||||||
synchronized (sharedSenderKeys) {
|
synchronized (sharedSenderKeys) {
|
||||||
return sharedSenderKeys.get(distributionId)
|
final var addresses = sharedSenderKeys.get(distributionId.asUuid());
|
||||||
.stream()
|
if (addresses == null) {
|
||||||
|
return Set.of();
|
||||||
|
}
|
||||||
|
return addresses.stream()
|
||||||
.map(k -> new SignalProtocolAddress(addressResolver.resolveRecipientAddress(k.recipientId())
|
.map(k -> new SignalProtocolAddress(addressResolver.resolveRecipientAddress(k.recipientId())
|
||||||
.getIdentifier(), k.deviceId()))
|
.getIdentifier(), k.deviceId()))
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
@ -105,9 +108,9 @@ public class SenderKeySharedStore {
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
synchronized (sharedSenderKeys) {
|
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(previousEntries);
|
||||||
addAll(newEntries);
|
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) {
|
public void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
|
||||||
synchronized (sharedSenderKeys) {
|
synchronized (sharedSenderKeys) {
|
||||||
for (final var distributionId : sharedSenderKeys.keySet()) {
|
for (final var distributionId : sharedSenderKeys.keySet()) {
|
||||||
|
@ -187,7 +197,7 @@ public class SenderKeySharedStore {
|
||||||
return sharedWith.stream()
|
return sharedWith.stream()
|
||||||
.map(entry -> new Storage.SharedSenderKey(entry.recipientId().id(),
|
.map(entry -> new Storage.SharedSenderKey(entry.recipientId().id(),
|
||||||
entry.deviceId(),
|
entry.deviceId(),
|
||||||
pair.getKey().asUuid().toString()));
|
pair.getKey().toString()));
|
||||||
}).toList());
|
}).toList());
|
||||||
|
|
||||||
// Write to memory first to prevent corrupting the file in case of serialization errors
|
// 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);
|
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) {
|
public void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
|
||||||
senderKeySharedStore.mergeRecipients(recipientId, toBeMergedRecipientId);
|
senderKeySharedStore.mergeRecipients(recipientId, toBeMergedRecipientId);
|
||||||
senderKeyRecordStore.mergeRecipients(recipientId, toBeMergedRecipientId);
|
senderKeyRecordStore.mergeRecipients(recipientId, toBeMergedRecipientId);
|
||||||
|
|
|
@ -7,6 +7,8 @@ import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.nio.file.Files;
|
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.PosixFilePermission;
|
||||||
import java.nio.file.attribute.PosixFilePermissions;
|
import java.nio.file.attribute.PosixFilePermissions;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
|
@ -72,4 +74,14 @@ public class IOUtils {
|
||||||
output.write(buffer, 0, read);
|
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