unidentifiedAccessHelper::getAccessFor,
this::resolveSignalServiceAddress);
final GroupV2Helper groupV2Helper = new GroupV2Helper(profileHelper::getRecipientProfileKeyCredential,
- this::getRecipientProfile,
+ profileHelper::getRecipientProfile,
account::getSelfRecipientId,
dependencies.getGroupsV2Operations(),
dependencies.getGroupsV2Api(),
account.getRecipientStore(),
this::handleIdentityFailure,
this::getGroupInfo,
+ profileHelper::getRecipientProfile,
this::refreshRegisteredUser);
this.groupHelper = new GroupHelper(account,
dependencies,
contactHelper,
attachmentHelper,
syncHelper,
- this::getRecipientProfile,
+ profileHelper::getRecipientProfile,
jobExecutor);
this.identityHelper = new IdentityHelper(account,
dependencies,
case TRUSTED_VERIFIED -> VerifiedMessage.VerifiedState.VERIFIED;
};
}
+
+ public boolean isTrusted() {
+ return switch (this) {
+ case TRUSTED_UNVERIFIED, TRUSTED_VERIFIED -> true;
+ case UNTRUSTED -> false;
+ };
+ }
}
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;
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);
}
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(
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) {
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(
groupInfoV1.removeMember(account.getSelfRecipientId());
account.getGroupStore().updateGroup(groupInfoV1);
return sendGroupMessage(messageBuilder,
- groupInfoV1.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
+ groupInfoV1.getMembersIncludingPendingWithout(account.getSelfRecipientId()),
+ groupInfoV1.getDistributionId());
}
private SendGroupMessageResults quitGroupV2(
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 {
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,
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
if (newIdentity) {
account.getSessionStore().archiveSessions(recipientId);
+ account.getSenderKeyStore().deleteSharedWith(recipientId);
}
} catch (InvalidKeyException ignored) {
logger.warn("Got invalid identity key in profile for {}",
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;
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 {
private final RecipientResolver recipientResolver;
private final IdentityFailureHandler identityFailureHandler;
private final GroupProvider groupProvider;
+ private final ProfileProvider profileProvider;
private final RecipientRegistrationRefresher recipientRegistrationRefresher;
public SendHelper(
final RecipientResolver recipientResolver,
final IdentityFailureHandler identityFailureHandler,
final GroupProvider groupProvider,
+ final ProfileProvider profileProvider,
final RecipientRegistrationRefresher recipientRegistrationRefresher
) {
this.account = account;
this.recipientResolver = recipientResolver;
this.identityFailureHandler = identityFailureHandler;
this.groupProvider = groupProvider;
+ this.profileProvider = profileProvider;
this.recipientRegistrationRefresher = recipientRegistrationRefresher;
}
final var message = messageBuilder.build();
final var result = sendMessage(message, recipientId);
- handlePossibleIdentityFailure(result);
+ handleSendMessageResult(result);
return result;
}
}
}
- return sendGroupMessage(message, recipients);
+ return sendGroupMessage(message, recipients, g.getDistributionId());
}
/**
* 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;
}
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
) {
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());
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;
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;
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();
signalAccount.registered = false;
+ signalAccount.previousStorageVersion = CURRENT_STORAGE_VERSION;
signalAccount.migrateLegacyConfigs();
signalAccount.save();
signalAccount.configurationStore = new ConfigurationStore(signalAccount::saveConfigurationStore);
signalAccount.recipientStore.resolveRecipientTrusted(signalAccount.getSelfAddress());
+ signalAccount.previousStorageVersion = CURRENT_STORAGE_VERSION;
signalAccount.migrateLegacyConfigs();
signalAccount.save();
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) {
} 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 {
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;
public abstract GroupId getGroupId();
+ public abstract DistributionId getDistributionId();
+
public abstract String getTitle();
public String getDescription() {
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;
return groupId;
}
+ @Override
+ public DistributionId getDistributionId() {
+ return null;
+ }
+
public GroupIdV2 getExpectedV2Id() {
if (expectedV2Id == null) {
expectedV2Id = GroupUtils.getGroupIdV2(groupId);
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;
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;
}
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;
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;
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);
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());
}
}
- 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>> {
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();
}
}
- 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());
}
}
- public void deleteAllFor(final RecipientId recipientId) {
+ void deleteAllFor(final RecipientId recipientId) {
synchronized (cachedSenderKeys) {
cachedSenderKeys.clear();
final var keys = getKeysLocked(recipientId);
}
}
- 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;
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);
}
}
- private record Key(RecipientId recipientId, int deviceId, UUID distributionId) {
-
- }
+ private record Key(RecipientId recipientId, int deviceId, UUID distributionId) {}
}
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;
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<>();
}
private SenderKeySharedStore(
- final Map<DistributionId, Set<SenderKeySharedEntry>> sharedSenderKeys,
+ final Map<UUID, Set<SenderKeySharedEntry>> sharedSenderKeys,
final ObjectMapper objectMapper,
final File file,
final RecipientAddressResolver addressResolver,
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());
.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);
}
}
+ 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()) {
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
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);
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;
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;
+ }
+ }
}