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

View file

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

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

View file

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

View file

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

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.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 {
try {
var messageSender = dependencies.getMessageSender();
// isRecipientUpdate is true if we've already sent this message to some recipients in the past, otherwise false. // isRecipientUpdate is true if we've already sent this message to some recipients in the past, otherwise false.
final var isRecipientUpdate = 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 recipientIdList = new ArrayList<>(recipientIds);
final var addresses = recipientIdList.stream().map(addressResolver::resolveSignalServiceAddress).toList(); final var addresses = recipientIdList.stream().map(addressResolver::resolveSignalServiceAddress).toList();
return messageSender.sendDataMessage(addresses, final var unidentifiedAccesses = unidentifiedAccessHelper.getAccessFor(recipientIdList);
unidentifiedAccessHelper.getAccessFor(recipientIdList), final var messageSender = dependencies.getMessageSender();
try {
final var results = messageSender.sendDataMessage(addresses,
unidentifiedAccesses,
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());

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

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

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

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

View file

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

View file

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

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

View file

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

View file

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

View file

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