Implementing sending group messages with sender keys

This commit is contained in:
AsamK 2021-12-20 12:26:03 +01:00
parent c134f1b78e
commit 1f48ce1f39
16 changed files with 359 additions and 53 deletions

View file

@ -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,

View file

@ -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;
};
}
}

View file

@ -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,

View file

@ -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

View file

@ -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 {}",

View file

@ -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());

View file

@ -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 {

View file

@ -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() {

View file

@ -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);

View file

@ -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;

View file

@ -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>> {

View file

@ -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();

View file

@ -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) {}
}

View file

@ -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

View file

@ -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);

View file

@ -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;
}
}
}