Key tables on serviceId instead of recipientId

This commit is contained in:
AsamK 2022-08-23 14:17:14 +02:00
parent 04fa046815
commit 280d8d7f10
25 changed files with 528 additions and 514 deletions

View file

@ -69,6 +69,7 @@ import org.whispersystems.signalservice.api.messages.SignalServicePreview;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import org.whispersystems.signalservice.api.push.ACI;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
@ -165,11 +166,12 @@ class ManagerImpl implements Manager {
this.notifyAll();
}
});
disposable.add(account.getIdentityKeyStore().getIdentityChanges().subscribe(recipientId -> {
logger.trace("Archiving old sessions for {}", recipientId);
account.getAciSessionStore().archiveSessions(recipientId);
account.getPniSessionStore().archiveSessions(recipientId);
account.getSenderKeyStore().deleteSharedWith(recipientId);
disposable.add(account.getIdentityKeyStore().getIdentityChanges().subscribe(serviceId -> {
logger.trace("Archiving old sessions for {}", serviceId);
account.getAciSessionStore().archiveSessions(serviceId);
account.getPniSessionStore().archiveSessions(serviceId);
account.getSenderKeyStore().deleteSharedWith(serviceId);
final var recipientId = account.getRecipientResolver().resolveRecipient(serviceId);
final var profile = account.getProfileStore().getProfile(recipientId);
if (profile != null) {
account.getProfileStore()
@ -631,7 +633,11 @@ class ManagerImpl implements Manager {
if (recipient instanceof RecipientIdentifier.Single r) {
try {
final var recipientId = context.getRecipientHelper().resolveRecipient(r);
account.getMessageSendLogStore().deleteEntryForRecipientNonGroup(targetSentTimestamp, recipientId);
account.getMessageSendLogStore()
.deleteEntryForRecipientNonGroup(targetSentTimestamp,
account.getRecipientAddressResolver()
.resolveRecipientAddress(recipientId)
.getServiceId());
} catch (UnregisteredRecipientException ignored) {
}
} else if (recipient instanceof RecipientIdentifier.Group r) {
@ -689,7 +695,11 @@ class ManagerImpl implements Manager {
} catch (UnregisteredRecipientException e) {
continue;
}
account.getAciSessionStore().deleteAllSessions(recipientId);
final var serviceId = context.getAccount()
.getRecipientAddressResolver()
.resolveRecipientAddress(recipientId)
.getServiceId();
account.getAciSessionStore().deleteAllSessions(serviceId);
}
}
}
@ -1035,13 +1045,13 @@ class ManagerImpl implements Manager {
}
final var address = account.getRecipientAddressResolver()
.resolveRecipientAddress(identityInfo.getRecipientId());
.resolveRecipientAddress(account.getRecipientResolver().resolveRecipient(identityInfo.getServiceId()));
final var scannableFingerprint = context.getIdentityHelper()
.computeSafetyNumberForScanning(identityInfo.getRecipientId(), identityInfo.getIdentityKey());
.computeSafetyNumberForScanning(identityInfo.getServiceId(), identityInfo.getIdentityKey());
return new Identity(address,
identityInfo.getIdentityKey(),
context.getIdentityHelper()
.computeSafetyNumber(identityInfo.getRecipientId(), identityInfo.getIdentityKey()),
.computeSafetyNumber(identityInfo.getServiceId(), identityInfo.getIdentityKey()),
scannableFingerprint == null ? null : scannableFingerprint.getSerialized(),
identityInfo.getTrustLevel(),
identityInfo.getDateAddedTimestamp());
@ -1049,13 +1059,15 @@ class ManagerImpl implements Manager {
@Override
public List<Identity> getIdentities(RecipientIdentifier.Single recipient) {
IdentityInfo identity;
ServiceId serviceId;
try {
identity = account.getIdentityKeyStore()
.getIdentityInfo(context.getRecipientHelper().resolveRecipient(recipient));
serviceId = account.getRecipientAddressResolver()
.resolveRecipientAddress(context.getRecipientHelper().resolveRecipient(recipient))
.getServiceId();
} catch (UnregisteredRecipientException e) {
identity = null;
return List.of();
}
final var identity = account.getIdentityKeyStore().getIdentityInfo(serviceId);
return identity == null ? List.of() : List.of(toIdentity(identity));
}

View file

@ -2,18 +2,21 @@ package org.asamk.signal.manager.actions;
import org.asamk.signal.manager.helper.Context;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.whispersystems.signalservice.api.push.ServiceId;
public class RenewSessionAction implements HandleAction {
private final RecipientId recipientId;
private final ServiceId serviceId;
public RenewSessionAction(final RecipientId recipientId) {
public RenewSessionAction(final RecipientId recipientId, final ServiceId serviceId) {
this.recipientId = recipientId;
this.serviceId = serviceId;
}
@Override
public void execute(Context context) throws Throwable {
context.getAccount().getAciSessionStore().archiveSessions(recipientId);
context.getAccount().getAciSessionStore().archiveSessions(serviceId);
if (!recipientId.equals(context.getAccount().getSelfRecipientId())) {
context.getSendHelper().sendNullMessage(recipientId);
}

View file

@ -7,6 +7,7 @@ import org.signal.libsignal.metadata.ProtocolException;
import org.signal.libsignal.protocol.message.CiphertextMessage;
import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import java.util.Optional;
@ -14,22 +15,25 @@ import java.util.Optional;
public class SendRetryMessageRequestAction implements HandleAction {
private final RecipientId recipientId;
private final ServiceId serviceId;
private final ProtocolException protocolException;
private final SignalServiceEnvelope envelope;
public SendRetryMessageRequestAction(
final RecipientId recipientId,
final ServiceId serviceId,
final ProtocolException protocolException,
final SignalServiceEnvelope envelope
) {
this.recipientId = recipientId;
this.serviceId = serviceId;
this.protocolException = protocolException;
this.envelope = envelope;
}
@Override
public void execute(Context context) throws Throwable {
context.getAccount().getAciSessionStore().archiveSessions(recipientId);
context.getAccount().getAciSessionStore().archiveSessions(serviceId);
int senderDevice = protocolException.getSenderDevice();
Optional<GroupId> groupId = protocolException.getGroupId().isPresent() ? Optional.of(GroupId.unknownVersion(

View file

@ -2,6 +2,7 @@ package org.asamk.signal.manager.helper;
import org.asamk.signal.manager.api.TrustLevel;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.util.Utils;
import org.signal.libsignal.protocol.IdentityKey;
@ -12,7 +13,7 @@ import org.signal.libsignal.protocol.fingerprint.ScannableFingerprint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.ServiceId;
import java.io.IOException;
import java.util.Arrays;
@ -33,20 +34,23 @@ public class IdentityHelper {
}
public boolean trustIdentityVerified(RecipientId recipientId, byte[] fingerprint) {
return trustIdentity(recipientId,
final var serviceId = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId).getServiceId();
return trustIdentity(serviceId,
identityKey -> Arrays.equals(identityKey.serialize(), fingerprint),
TrustLevel.TRUSTED_VERIFIED);
}
public boolean trustIdentityVerifiedSafetyNumber(RecipientId recipientId, String safetyNumber) {
return trustIdentity(recipientId,
identityKey -> safetyNumber.equals(computeSafetyNumber(recipientId, identityKey)),
final var serviceId = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId).getServiceId();
return trustIdentity(serviceId,
identityKey -> safetyNumber.equals(computeSafetyNumber(serviceId, identityKey)),
TrustLevel.TRUSTED_VERIFIED);
}
public boolean trustIdentityVerifiedSafetyNumber(RecipientId recipientId, byte[] safetyNumber) {
return trustIdentity(recipientId, identityKey -> {
final var fingerprint = computeSafetyNumberForScanning(recipientId, identityKey);
final var serviceId = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId).getServiceId();
return trustIdentity(serviceId, identityKey -> {
final var fingerprint = computeSafetyNumberForScanning(serviceId, identityKey);
try {
return fingerprint != null && fingerprint.compareTo(safetyNumber);
} catch (FingerprintVersionMismatchException | FingerprintParsingException e) {
@ -56,35 +60,39 @@ public class IdentityHelper {
}
public boolean trustIdentityAllKeys(RecipientId recipientId) {
return trustIdentity(recipientId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED);
final var serviceId = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId).getServiceId();
return trustIdentity(serviceId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED);
}
public String computeSafetyNumber(RecipientId recipientId, IdentityKey theirIdentityKey) {
var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
final Fingerprint fingerprint = computeSafetyNumberFingerprint(address, theirIdentityKey);
public String computeSafetyNumber(ServiceId serviceId, IdentityKey theirIdentityKey) {
final Fingerprint fingerprint = computeSafetyNumberFingerprint(serviceId, theirIdentityKey);
return fingerprint == null ? null : fingerprint.getDisplayableFingerprint().getDisplayText();
}
public ScannableFingerprint computeSafetyNumberForScanning(RecipientId recipientId, IdentityKey theirIdentityKey) {
var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
final Fingerprint fingerprint = computeSafetyNumberFingerprint(address, theirIdentityKey);
public ScannableFingerprint computeSafetyNumberForScanning(ServiceId serviceId, IdentityKey theirIdentityKey) {
final Fingerprint fingerprint = computeSafetyNumberFingerprint(serviceId, theirIdentityKey);
return fingerprint == null ? null : fingerprint.getScannableFingerprint();
}
private Fingerprint computeSafetyNumberFingerprint(
final SignalServiceAddress theirAddress, final IdentityKey theirIdentityKey
final ServiceId serviceId, final IdentityKey theirIdentityKey
) {
final var address = account.getRecipientAddressResolver()
.resolveRecipientAddress(account.getRecipientResolver().resolveRecipient(serviceId));
return Utils.computeSafetyNumber(capabilities.isUuid(),
account.getSelfAddress(),
account.getSelfRecipientAddress(),
account.getAciIdentityKeyPair().getPublicKey(),
theirAddress,
address.getServiceId().equals(serviceId)
? address
: new RecipientAddress(serviceId.uuid(), address.number().orElse(null)),
theirIdentityKey);
}
private boolean trustIdentity(
RecipientId recipientId, Function<IdentityKey, Boolean> verifier, TrustLevel trustLevel
ServiceId serviceId, Function<IdentityKey, Boolean> verifier, TrustLevel trustLevel
) {
var identity = account.getIdentityKeyStore().getIdentityInfo(recipientId);
var identity = account.getIdentityKeyStore().getIdentityInfo(serviceId);
if (identity == null) {
return false;
}
@ -93,9 +101,11 @@ public class IdentityHelper {
return false;
}
account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identity.getIdentityKey(), trustLevel);
account.getIdentityKeyStore().setIdentityTrustLevel(serviceId, identity.getIdentityKey(), trustLevel);
try {
var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
final var address = account.getRecipientAddressResolver()
.resolveRecipientAddress(account.getRecipientResolver().resolveRecipient(serviceId))
.toSignalServiceAddress();
context.getSyncHelper().sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel);
} catch (IOException e) {
logger.warn("Failed to send verification sync message: {}", e.getMessage());
@ -105,11 +115,13 @@ public class IdentityHelper {
}
public void handleIdentityFailure(
final RecipientId recipientId, final SendMessageResult.IdentityFailure identityFailure
final RecipientId recipientId,
final ServiceId serviceId,
final SendMessageResult.IdentityFailure identityFailure
) {
final var identityKey = identityFailure.getIdentityKey();
if (identityKey != null) {
account.getIdentityKeyStore().saveIdentity(recipientId, identityKey);
account.getIdentityKeyStore().saveIdentity(serviceId, identityKey);
} else {
// Retrieve profile to get the current identity key from the server
context.getProfileHelper().refreshRecipientProfile(recipientId);

View file

@ -42,7 +42,6 @@ import org.signal.libsignal.metadata.ProtocolInvalidMessageException;
import org.signal.libsignal.metadata.ProtocolNoSessionException;
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
import org.signal.libsignal.metadata.SelfSendException;
import org.signal.libsignal.protocol.SignalProtocolAddress;
import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
@ -57,6 +56,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.ArrayList;
@ -137,16 +137,17 @@ public final class IncomingMessageHandler {
} else {
final var senderProfile = context.getProfileHelper().getRecipientProfile(sender);
final var selfProfile = context.getProfileHelper().getSelfProfile();
final var serviceId = ServiceId.parseOrThrow(e.getSender());
if ((!sender.equals(account.getSelfRecipientId()) || e.getSenderDevice() != account.getDeviceId())
&& senderProfile != null
&& senderProfile.getCapabilities().contains(Profile.Capability.senderKey)
&& selfProfile != null
&& selfProfile.getCapabilities().contains(Profile.Capability.senderKey)) {
logger.debug("Received invalid message, requesting message resend.");
actions.add(new SendRetryMessageRequestAction(sender, e, envelope));
actions.add(new SendRetryMessageRequestAction(sender, serviceId, e, envelope));
} else {
logger.debug("Received invalid message, queuing renew session action.");
actions.add(new RenewSessionAction(sender));
actions.add(new RenewSessionAction(sender, serviceId));
}
}
exception = e;
@ -176,9 +177,9 @@ public final class IncomingMessageHandler {
account.getRecipientTrustedResolver().resolveRecipientTrusted(content.getSender());
}
if (envelope.isReceipt()) {
final var senderPair = getSender(envelope, content);
final var sender = senderPair.first();
final var senderDeviceId = senderPair.second();
final var senderDeviceAddress = getSender(envelope, content);
final var sender = senderDeviceAddress.serviceId();
final var senderDeviceId = senderDeviceAddress.deviceId();
account.getMessageSendLogStore().deleteEntryForRecipient(envelope.getTimestamp(), sender, senderDeviceId);
}
@ -211,24 +212,23 @@ public final class IncomingMessageHandler {
SignalServiceEnvelope envelope, SignalServiceContent content, ReceiveConfig receiveConfig
) {
var actions = new ArrayList<HandleAction>();
final var senderPair = getSender(envelope, content);
final var sender = senderPair.first();
final var senderDeviceId = senderPair.second();
final var senderDeviceAddress = getSender(envelope, content);
final var sender = senderDeviceAddress.recipientId();
final var senderServiceId = senderDeviceAddress.serviceId();
final var senderDeviceId = senderDeviceAddress.deviceId();
final var destination = getDestination(envelope);
if (content.getReceiptMessage().isPresent()) {
final var message = content.getReceiptMessage().get();
if (message.isDeliveryReceipt()) {
account.getMessageSendLogStore()
.deleteEntriesForRecipient(message.getTimestamps(), sender, senderDeviceId);
.deleteEntriesForRecipient(message.getTimestamps(), senderServiceId, senderDeviceId);
}
}
if (content.getSenderKeyDistributionMessage().isPresent()) {
final var message = content.getSenderKeyDistributionMessage().get();
final var protocolAddress = new SignalProtocolAddress(context.getRecipientHelper()
.resolveSignalServiceAddress(sender)
.getIdentifier(), senderDeviceId);
final var protocolAddress = senderServiceId.toProtocolAddress(senderDeviceId);
logger.debug("Received a sender key distribution message for distributionId {} from {}",
message.getDistributionId(),
protocolAddress);
@ -242,7 +242,7 @@ public final class IncomingMessageHandler {
senderDeviceId,
message.getTimestamp());
if (message.getDeviceId() == account.getDeviceId()) {
handleDecryptionErrorMessage(actions, sender, senderDeviceId, message);
handleDecryptionErrorMessage(actions, sender, senderServiceId, senderDeviceId, message);
} else {
logger.debug("Request is for another one of our devices");
}
@ -274,7 +274,7 @@ public final class IncomingMessageHandler {
actions.addAll(handleSignalServiceDataMessage(message,
false,
sender,
senderDeviceAddress,
destination,
receiveConfig.ignoreAttachments()));
}
@ -286,7 +286,7 @@ public final class IncomingMessageHandler {
if (content.getSyncMessage().isPresent()) {
var syncMessage = content.getSyncMessage().get();
actions.addAll(handleSyncMessage(syncMessage, sender, receiveConfig.ignoreAttachments()));
actions.addAll(handleSyncMessage(syncMessage, senderDeviceAddress, receiveConfig.ignoreAttachments()));
}
return actions;
@ -295,11 +295,15 @@ public final class IncomingMessageHandler {
private void handleDecryptionErrorMessage(
final List<HandleAction> actions,
final RecipientId sender,
final ServiceId senderServiceId,
final int senderDeviceId,
final DecryptionErrorMessage message
) {
final var logEntries = account.getMessageSendLogStore()
.findMessages(sender, senderDeviceId, message.getTimestamp(), message.getRatchetKey().isEmpty());
.findMessages(senderServiceId,
senderDeviceId,
message.getTimestamp(),
message.getRatchetKey().isEmpty());
for (final var logEntry : logEntries) {
actions.add(new ResendMessageAction(sender, message.getTimestamp(), logEntry));
@ -307,13 +311,13 @@ public final class IncomingMessageHandler {
if (message.getRatchetKey().isPresent()) {
if (account.getAciSessionStore()
.isCurrentRatchetKey(sender, senderDeviceId, message.getRatchetKey().get())) {
.isCurrentRatchetKey(senderServiceId, senderDeviceId, message.getRatchetKey().get())) {
if (logEntries.isEmpty()) {
logger.debug("Renewing the session with sender");
actions.add(new RenewSessionAction(sender));
actions.add(new RenewSessionAction(sender, senderServiceId));
} else {
logger.trace("Archiving the session with sender, a resend message has already been queued");
context.getAccount().getAciSessionStore().archiveSessions(sender);
context.getAccount().getAciSessionStore().archiveSessions(senderServiceId);
}
}
return;
@ -333,16 +337,16 @@ public final class IncomingMessageHandler {
sender,
senderDeviceId,
group.getDistributionId());
account.getSenderKeyStore().deleteSharedWith(sender, senderDeviceId, group.getDistributionId());
account.getSenderKeyStore().deleteSharedWith(senderServiceId, senderDeviceId, group.getDistributionId());
}
if (!found) {
logger.debug("Reset all shared sender keys with this recipient, no related message found in send log");
account.getSenderKeyStore().deleteSharedWith(sender);
account.getSenderKeyStore().deleteSharedWith(senderServiceId);
}
}
private List<HandleAction> handleSyncMessage(
final SignalServiceSyncMessage syncMessage, final RecipientId sender, final boolean ignoreAttachments
final SignalServiceSyncMessage syncMessage, final DeviceAddress sender, final boolean ignoreAttachments
) {
var actions = new ArrayList<HandleAction>();
account.setMultiDevice(true);
@ -353,12 +357,16 @@ public final class IncomingMessageHandler {
actions.addAll(handleSignalServiceDataMessage(message.getDataMessage().get(),
true,
sender,
destination == null ? null : context.getRecipientHelper().resolveRecipient(destination),
destination == null
? null
: new DeviceAddress(context.getRecipientHelper().resolveRecipient(destination),
destination.getServiceId(),
0),
ignoreAttachments));
}
if (message.getStoryMessage().isPresent()) {
actions.addAll(handleSignalServiceStoryMessage(message.getStoryMessage().get(),
sender,
sender.recipientId(),
ignoreAttachments));
}
}
@ -423,8 +431,7 @@ public final class IncomingMessageHandler {
if (syncMessage.getVerified().isPresent()) {
final var verifiedMessage = syncMessage.getVerified().get();
account.getIdentityKeyStore()
.setIdentityTrustLevel(account.getRecipientTrustedResolver()
.resolveRecipientTrusted(verifiedMessage.getDestination()),
.setIdentityTrustLevel(verifiedMessage.getDestination().getServiceId(),
verifiedMessage.getIdentityKey(),
TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
}
@ -575,8 +582,8 @@ public final class IncomingMessageHandler {
private List<HandleAction> handleSignalServiceDataMessage(
SignalServiceDataMessage message,
boolean isSync,
RecipientId source,
RecipientId destination,
DeviceAddress source,
DeviceAddress destination,
boolean ignoreAttachments
) {
var actions = new ArrayList<HandleAction>();
@ -616,19 +623,19 @@ public final class IncomingMessageHandler {
}
case DELIVER:
if (groupV1 == null && !isSync) {
actions.add(new SendGroupInfoRequestAction(source, groupId));
actions.add(new SendGroupInfoRequestAction(source.recipientId(), groupId));
}
break;
case QUIT: {
if (groupV1 != null) {
groupV1.removeMember(source);
groupV1.removeMember(source.recipientId());
account.getGroupStore().updateGroup(groupV1);
}
break;
}
case REQUEST_INFO:
if (groupV1 != null && !isSync) {
actions.add(new SendGroupInfoAction(source, groupV1.getGroupId()));
actions.add(new SendGroupInfoAction(source.recipientId(), groupV1.getGroupId()));
}
break;
}
@ -643,7 +650,7 @@ public final class IncomingMessageHandler {
final var conversationPartnerAddress = isSync ? destination : source;
if (conversationPartnerAddress != null && message.isEndSession()) {
account.getAciSessionStore().deleteAllSessions(conversationPartnerAddress);
account.getAciSessionStore().deleteAllSessions(conversationPartnerAddress.serviceId());
}
if (message.isExpirationUpdate() || message.getBody().isPresent()) {
if (message.getGroupContext().isPresent()) {
@ -662,7 +669,7 @@ public final class IncomingMessageHandler {
}
} else if (conversationPartnerAddress != null) {
context.getContactHelper()
.setExpirationTimer(conversationPartnerAddress, message.getExpiresInSeconds());
.setExpirationTimer(conversationPartnerAddress.recipientId(), message.getExpiresInSeconds());
}
}
if (!ignoreAttachments) {
@ -698,7 +705,7 @@ public final class IncomingMessageHandler {
}
}
if (message.getProfileKey().isPresent()) {
handleIncomingProfileKey(message.getProfileKey().get(), source);
handleIncomingProfileKey(message.getProfileKey().get(), source.recipientId());
}
if (message.getSticker().isPresent()) {
final var messageSticker = message.getSticker().get();
@ -769,24 +776,29 @@ public final class IncomingMessageHandler {
this.account.getProfileStore().storeProfileKey(source, profileKey);
}
private Pair<RecipientId, Integer> getSender(SignalServiceEnvelope envelope, SignalServiceContent content) {
private DeviceAddress getSender(SignalServiceEnvelope envelope, SignalServiceContent content) {
if (!envelope.isUnidentifiedSender() && envelope.hasSourceUuid()) {
return new Pair<>(context.getRecipientHelper().resolveRecipient(envelope.getSourceAddress()),
return new DeviceAddress(context.getRecipientHelper().resolveRecipient(envelope.getSourceAddress()),
envelope.getSourceAddress().getServiceId(),
envelope.getSourceDevice());
} else {
return new Pair<>(context.getRecipientHelper().resolveRecipient(content.getSender()),
return new DeviceAddress(context.getRecipientHelper().resolveRecipient(content.getSender()),
content.getSender().getServiceId(),
content.getSenderDevice());
}
}
private RecipientId getDestination(SignalServiceEnvelope envelope) {
private DeviceAddress getDestination(SignalServiceEnvelope envelope) {
if (!envelope.hasDestinationUuid()) {
return account.getSelfRecipientId();
return new DeviceAddress(account.getSelfRecipientId(), account.getAci(), account.getDeviceId());
}
final var addressOptional = SignalServiceAddress.fromRaw(envelope.getDestinationUuid(), null);
if (addressOptional.isEmpty()) {
return account.getSelfRecipientId();
return new DeviceAddress(account.getSelfRecipientId(), account.getAci(), account.getDeviceId());
}
return context.getRecipientHelper().resolveRecipient(addressOptional.get());
final var address = addressOptional.get();
return new DeviceAddress(context.getRecipientHelper().resolveRecipient(address), address.getServiceId(), 0);
}
private record DeviceAddress(RecipientId recipientId, ServiceId serviceId, int deviceId) {}
}

View file

@ -346,7 +346,7 @@ public final class ProfileHelper {
try {
logger.trace("Storing identity");
final var identityKey = new IdentityKey(Base64.getDecoder().decode(encryptedProfile.getIdentityKey()));
account.getIdentityKeyStore().saveIdentity(recipientId, identityKey);
account.getIdentityKeyStore().saveIdentity(p.getProfile().getServiceId(), identityKey);
} catch (InvalidKeyException ignored) {
logger.warn("Got invalid identity key in profile for {}",
context.getRecipientHelper().resolveSignalServiceAddress(recipientId).getIdentifier());

View file

@ -486,7 +486,10 @@ public class SendHelper {
continue;
}
final var identity = account.getIdentityKeyStore().getIdentityInfo(recipientId);
final var serviceId = account.getRecipientAddressResolver()
.resolveRecipientAddress(recipientId)
.getServiceId();
final var identity = account.getIdentityKeyStore().getIdentityInfo(serviceId);
if (identity == null || !identity.getTrustLevel().isTrusted()) {
continue;
}
@ -531,7 +534,7 @@ public class SendHelper {
final var recipientIdList = new ArrayList<>(recipientIds);
long keyCreateTime = account.getSenderKeyStore()
.getCreateTimeForOurKey(account.getSelfRecipientId(), account.getDeviceId(), distributionId);
.getCreateTimeForOurKey(account.getAci(), account.getDeviceId(), distributionId);
long keyAge = System.currentTimeMillis() - keyCreateTime;
if (keyCreateTime != -1 && keyAge > TimeUnit.DAYS.toMillis(14)) {
@ -540,7 +543,7 @@ public class SendHelper {
keyCreateTime,
keyAge,
TimeUnit.MILLISECONDS.toDays(keyAge));
account.getSenderKeyStore().deleteOurKey(account.getSelfRecipientId(), distributionId);
account.getSenderKeyStore().deleteOurKey(account.getAci(), distributionId);
}
List<SignalServiceAddress> addresses = recipientIdList.stream()
@ -573,11 +576,11 @@ public class SendHelper {
return null;
} catch (NoSessionException e) {
logger.warn("No session. Falling back to legacy sends.", e);
account.getSenderKeyStore().deleteOurKey(account.getSelfRecipientId(), distributionId);
account.getSenderKeyStore().deleteOurKey(account.getAci(), distributionId);
return null;
} catch (InvalidKeyException e) {
logger.warn("Invalid key. Falling back to legacy sends.", e);
account.getSenderKeyStore().deleteOurKey(account.getSelfRecipientId(), distributionId);
account.getSenderKeyStore().deleteOurKey(account.getAci(), distributionId);
return null;
} catch (InvalidRegistrationIdException e) {
logger.warn("Invalid registrationId. Falling back to legacy sends.", e);
@ -685,7 +688,8 @@ public class SendHelper {
}
if (r.getIdentityFailure() != null) {
final var recipientId = context.getRecipientHelper().resolveRecipient(r.getAddress());
context.getIdentityHelper().handleIdentityFailure(recipientId, r.getIdentityFailure());
context.getIdentityHelper()
.handleIdentityFailure(recipientId, r.getAddress().getServiceId(), r.getIdentityFailure());
}
}

View file

@ -144,11 +144,12 @@ public class StorageHelper {
try {
logger.trace("Storing identity key {}", recipientId);
final var identityKey = new IdentityKey(contactRecord.getIdentityKey().get());
account.getIdentityKeyStore().saveIdentity(recipientId, identityKey);
account.getIdentityKeyStore().saveIdentity(address.getServiceId(), identityKey);
final var trustLevel = TrustLevel.fromIdentityState(contactRecord.getIdentityState());
if (trustLevel != null) {
account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identityKey, trustLevel);
account.getIdentityKeyStore()
.setIdentityTrustLevel(address.getServiceId(), identityKey, trustLevel);
}
} catch (InvalidKeyException e) {
logger.warn("Received invalid contact identity key from storage");

View file

@ -132,7 +132,7 @@ public class SyncHelper {
final var contact = contactPair.second();
final var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
var currentIdentity = account.getIdentityKeyStore().getIdentityInfo(recipientId);
var currentIdentity = account.getIdentityKeyStore().getIdentityInfo(address.getServiceId());
VerifiedMessage verifiedMessage = null;
if (currentIdentity != null) {
verifiedMessage = new VerifiedMessage(address,
@ -319,8 +319,7 @@ public class SyncHelper {
if (c.getVerified().isPresent()) {
final var verifiedMessage = c.getVerified().get();
account.getIdentityKeyStore()
.setIdentityTrustLevel(account.getRecipientTrustedResolver()
.resolveRecipientTrusted(verifiedMessage.getDestination()),
.setIdentityTrustLevel(verifiedMessage.getDestination().getServiceId(),
verifiedMessage.getIdentityKey(),
TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
}

View file

@ -22,7 +22,7 @@ import java.sql.SQLException;
public class AccountDatabase extends Database {
private final static Logger logger = LoggerFactory.getLogger(AccountDatabase.class);
private static final long DATABASE_VERSION = 9;
private static final long DATABASE_VERSION = 10;
private AccountDatabase(final HikariDataSource dataSource) {
super(logger, DATABASE_VERSION, dataSource);
@ -212,5 +212,81 @@ public class AccountDatabase extends Database {
""");
}
}
if (oldVersion < 10) {
logger.debug("Updating database: Key tables on serviceId instead of recipientId");
try (final var statement = connection.createStatement()) {
statement.executeUpdate("""
CREATE TABLE identity2 (
_id INTEGER PRIMARY KEY,
uuid BLOB UNIQUE NOT NULL,
identity_key BLOB NOT NULL,
added_timestamp INTEGER NOT NULL,
trust_level INTEGER NOT NULL
) STRICT;
INSERT INTO identity2 (_id, uuid, identity_key, added_timestamp, trust_level)
SELECT i._id, r.uuid, i.identity_key, i.added_timestamp, i.trust_level
FROM identity i LEFT JOIN recipient r ON i.recipient_id = r._id
WHERE uuid IS NOT NULL;
DROP TABLE identity;
ALTER TABLE identity2 RENAME TO identity;
DROP INDEX msl_recipient_index;
ALTER TABLE message_send_log ADD COLUMN uuid BLOB;
UPDATE message_send_log
SET uuid = r.uuid
FROM message_send_log i, (SELECT _id, uuid FROM recipient) AS r
WHERE i.recipient_id = r._id;
DELETE FROM message_send_log WHERE uuid IS NULL;
ALTER TABLE message_send_log DROP COLUMN recipient_id;
CREATE INDEX msl_recipient_index ON message_send_log (uuid, device_id, content_id);
CREATE TABLE sender_key2 (
_id INTEGER PRIMARY KEY,
uuid BLOB NOT NULL,
device_id INTEGER NOT NULL,
distribution_id BLOB NOT NULL,
record BLOB NOT NULL,
created_timestamp INTEGER NOT NULL,
UNIQUE(uuid, device_id, distribution_id)
) STRICT;
INSERT INTO sender_key2 (_id, uuid, device_id, distribution_id, record, created_timestamp)
SELECT s._id, r.uuid, s.device_id, s.distribution_id, s.record, s.created_timestamp
FROM sender_key s LEFT JOIN recipient r ON s.recipient_id = r._id
WHERE uuid IS NOT NULL;
DROP TABLE sender_key;
ALTER TABLE sender_key2 RENAME TO sender_key;
CREATE TABLE sender_key_shared2 (
_id INTEGER PRIMARY KEY,
uuid BLOB NOT NULL,
device_id INTEGER NOT NULL,
distribution_id BLOB NOT NULL,
timestamp INTEGER NOT NULL,
UNIQUE(uuid, device_id, distribution_id)
) STRICT;
INSERT INTO sender_key_shared2 (_id, uuid, device_id, distribution_id, timestamp)
SELECT s._id, r.uuid, s.device_id, s.distribution_id, s.timestamp
FROM sender_key_shared s LEFT JOIN recipient r ON s.recipient_id = r._id
WHERE uuid IS NOT NULL;
DROP TABLE sender_key_shared;
ALTER TABLE sender_key_shared2 RENAME TO sender_key_shared;
CREATE TABLE session2 (
_id INTEGER PRIMARY KEY,
account_id_type INTEGER NOT NULL,
uuid BLOB NOT NULL,
device_id INTEGER NOT NULL,
record BLOB NOT NULL,
UNIQUE(account_id_type, uuid, device_id)
) STRICT;
INSERT INTO session2 (_id, account_id_type, uuid, device_id, record)
SELECT s._id, s.account_id_type, r.uuid, s.device_id, s.record
FROM session s LEFT JOIN recipient r ON s.recipient_id = r._id
WHERE uuid IS NOT NULL;
DROP TABLE session;
ALTER TABLE session2 RENAME TO session;
""");
}
}
}
}

View file

@ -383,6 +383,14 @@ public class SignalAccount implements Closeable {
this.storageManifestVersion = -1;
this.setStorageManifest(null);
this.storageKey = null;
final var aciPublicKey = getAciIdentityKeyPair().getPublicKey();
getIdentityKeyStore().saveIdentity(getAci(), aciPublicKey);
getIdentityKeyStore().setIdentityTrustLevel(getAci(), aciPublicKey, TrustLevel.TRUSTED_VERIFIED);
if (getPniIdentityKeyPair() != null) {
final var pniPublicKey = getPniIdentityKeyPair().getPublicKey();
getIdentityKeyStore().saveIdentity(getPni(), pniPublicKey);
getIdentityKeyStore().setIdentityTrustLevel(getPni(), pniPublicKey, TrustLevel.TRUSTED_VERIFIED);
}
}
private void migrateLegacyConfigs() {
@ -400,21 +408,21 @@ public class SignalAccount implements Closeable {
}
private void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
getAciSessionStore().mergeRecipients(recipientId, toBeMergedRecipientId);
getPniSessionStore().mergeRecipients(recipientId, toBeMergedRecipientId);
getIdentityKeyStore().mergeRecipients(recipientId, toBeMergedRecipientId);
getMessageCache().mergeRecipients(recipientId, toBeMergedRecipientId);
getGroupStore().mergeRecipients(recipientId, toBeMergedRecipientId);
getSenderKeyStore().mergeRecipients(recipientId, toBeMergedRecipientId);
}
public void removeRecipient(final RecipientId recipientId) {
getAciSessionStore().deleteAllSessions(recipientId);
getPniSessionStore().deleteAllSessions(recipientId);
getIdentityKeyStore().deleteIdentity(recipientId);
getMessageCache().deleteMessages(recipientId);
getSenderKeyStore().deleteAll(recipientId);
getRecipientStore().deleteRecipientData(recipientId);
getMessageCache().deleteMessages(recipientId);
final var recipientAddress = getRecipientStore().resolveRecipientAddress(recipientId);
if (recipientAddress.uuid().isPresent()) {
final var serviceId = ServiceId.from(recipientAddress.uuid().get());
getAciSessionStore().deleteAllSessions(serviceId);
getPniSessionStore().deleteAllSessions(serviceId);
getIdentityKeyStore().deleteIdentity(serviceId);
getSenderKeyStore().deleteAll(serviceId);
}
}
public static File getFileName(File dataPath, String account) {
@ -646,12 +654,18 @@ public class SignalAccount implements Closeable {
}
final var legacySessionsPath = getSessionsPath(dataPath, accountPath);
if (legacySessionsPath.exists()) {
LegacySessionStore.migrate(legacySessionsPath, getRecipientResolver(), getAciSessionStore());
LegacySessionStore.migrate(legacySessionsPath,
getRecipientResolver(),
getRecipientAddressResolver(),
getAciSessionStore());
migratedLegacyConfig = true;
}
final var legacyIdentitiesPath = getIdentitiesPath(dataPath, accountPath);
if (legacyIdentitiesPath.exists()) {
LegacyIdentityKeyStore.migrate(legacyIdentitiesPath, getRecipientResolver(), getIdentityKeyStore());
LegacyIdentityKeyStore.migrate(legacyIdentitiesPath,
getRecipientResolver(),
getRecipientAddressResolver(),
getIdentityKeyStore());
migratedLegacyConfig = true;
}
final var legacySignalProtocolStore = rootNode.hasNonNull("axolotlStore")
@ -672,12 +686,18 @@ public class SignalAccount implements Closeable {
final var legacySenderKeysPath = getSenderKeysPath(dataPath, accountPath);
if (legacySenderKeysPath.exists()) {
LegacySenderKeyRecordStore.migrate(legacySenderKeysPath, getRecipientResolver(), getSenderKeyStore());
LegacySenderKeyRecordStore.migrate(legacySenderKeysPath,
getRecipientResolver(),
getRecipientAddressResolver(),
getSenderKeyStore());
migratedLegacyConfig = true;
}
final var legacySenderKeysSharedPath = getSharedSenderKeysFile(dataPath, accountPath);
if (legacySenderKeysSharedPath.exists()) {
LegacySenderKeySharedStore.migrate(legacySenderKeysSharedPath, getRecipientResolver(), getSenderKeyStore());
LegacySenderKeySharedStore.migrate(legacySenderKeysSharedPath,
getRecipientResolver(),
getRecipientAddressResolver(),
getSenderKeyStore());
migratedLegacyConfig = true;
}
if (rootNode.hasNonNull("groupStore")) {
@ -770,9 +790,12 @@ public class SignalAccount implements Closeable {
if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyIdentityKeyStore() != null) {
logger.debug("Migrating legacy identity session store.");
for (var identity : legacySignalProtocolStore.getLegacyIdentityKeyStore().getIdentities()) {
RecipientId recipientId = getRecipientStore().resolveRecipientTrusted(identity.getAddress());
getIdentityKeyStore().saveIdentity(recipientId, identity.getIdentityKey());
getIdentityKeyStore().setIdentityTrustLevel(recipientId,
if (identity.getAddress().uuid().isEmpty()) {
continue;
}
final var serviceId = identity.getAddress().getServiceId();
getIdentityKeyStore().saveIdentity(serviceId, identity.getIdentityKey());
getIdentityKeyStore().setIdentityTrustLevel(serviceId,
identity.getIdentityKey(),
identity.getTrustLevel());
}
@ -1107,25 +1130,17 @@ public class SignalAccount implements Closeable {
public SessionStore getAciSessionStore() {
return getOrCreate(() -> aciSessionStore,
() -> aciSessionStore = new SessionStore(getAccountDatabase(),
ServiceIdType.ACI,
getRecipientResolver(),
getRecipientIdCreator()));
() -> aciSessionStore = new SessionStore(getAccountDatabase(), ServiceIdType.ACI));
}
public SessionStore getPniSessionStore() {
return getOrCreate(() -> pniSessionStore,
() -> pniSessionStore = new SessionStore(getAccountDatabase(),
ServiceIdType.PNI,
getRecipientResolver(),
getRecipientIdCreator()));
() -> pniSessionStore = new SessionStore(getAccountDatabase(), ServiceIdType.PNI));
}
public IdentityKeyStore getIdentityKeyStore() {
return getOrCreate(() -> identityKeyStore,
() -> identityKeyStore = new IdentityKeyStore(getAccountDatabase(),
getRecipientIdCreator(),
trustNewIdentity));
() -> identityKeyStore = new IdentityKeyStore(getAccountDatabase(), trustNewIdentity));
}
public SignalIdentityKeyStore getAciIdentityKeyStore() {
@ -1207,11 +1222,7 @@ public class SignalAccount implements Closeable {
}
public SenderKeyStore getSenderKeyStore() {
return getOrCreate(() -> senderKeyStore,
() -> senderKeyStore = new SenderKeyStore(getAccountDatabase(),
getRecipientAddressResolver(),
getRecipientResolver(),
getRecipientIdCreator()));
return getOrCreate(() -> senderKeyStore, () -> senderKeyStore = new SenderKeyStore(getAccountDatabase()));
}
public ConfigurationStore getConfigurationStore() {
@ -1235,7 +1246,7 @@ public class SignalAccount implements Closeable {
public MessageSendLogStore getMessageSendLogStore() {
return getOrCreate(() -> messageSendLogStore,
() -> messageSendLogStore = new MessageSendLogStore(getRecipientResolver(), getAccountDatabase()));
() -> messageSendLogStore = new MessageSendLogStore(getAccountDatabase()));
}
public CredentialsProvider getCredentialsProvider() {
@ -1350,6 +1361,9 @@ public class SignalAccount implements Closeable {
public void setPniIdentityKeyPair(final IdentityKeyPair identityKeyPair) {
pniIdentityKeyPair = identityKeyPair;
final var pniPublicKey = getPniIdentityKeyPair().getPublicKey();
getIdentityKeyStore().saveIdentity(getPni(), pniPublicKey);
getIdentityKeyStore().setIdentityTrustLevel(getPni(), pniPublicKey, TrustLevel.TRUSTED_VERIFIED);
save();
}
@ -1553,10 +1567,13 @@ public class SignalAccount implements Closeable {
getAciSessionStore().archiveAllSessions();
getPniSessionStore().archiveAllSessions();
getSenderKeyStore().deleteAll();
final var recipientId = getRecipientTrustedResolver().resolveSelfRecipientTrusted(getSelfRecipientAddress());
final var publicKey = getAciIdentityKeyPair().getPublicKey();
getIdentityKeyStore().saveIdentity(recipientId, publicKey);
getIdentityKeyStore().setIdentityTrustLevel(recipientId, publicKey, TrustLevel.TRUSTED_VERIFIED);
getRecipientTrustedResolver().resolveSelfRecipientTrusted(getSelfRecipientAddress());
final var aciPublicKey = getAciIdentityKeyPair().getPublicKey();
getIdentityKeyStore().saveIdentity(getAci(), aciPublicKey);
getIdentityKeyStore().setIdentityTrustLevel(getAci(), aciPublicKey, TrustLevel.TRUSTED_VERIFIED);
final var pniPublicKey = getPniIdentityKeyPair().getPublicKey();
getIdentityKeyStore().saveIdentity(getPni(), pniPublicKey);
getIdentityKeyStore().setIdentityTrustLevel(getPni(), pniPublicKey, TrustLevel.TRUSTED_VERIFIED);
}
public void deleteAccountData() throws IOException {

View file

@ -1,27 +1,27 @@
package org.asamk.signal.manager.storage.identities;
import org.asamk.signal.manager.api.TrustLevel;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.signal.libsignal.protocol.IdentityKey;
import org.whispersystems.signalservice.api.push.ServiceId;
public class IdentityInfo {
private final RecipientId recipientId;
private final ServiceId serviceId;
private final IdentityKey identityKey;
private final TrustLevel trustLevel;
private final long addedTimestamp;
IdentityInfo(
final RecipientId recipientId, IdentityKey identityKey, TrustLevel trustLevel, long addedTimestamp
final ServiceId serviceId, IdentityKey identityKey, TrustLevel trustLevel, long addedTimestamp
) {
this.recipientId = recipientId;
this.serviceId = serviceId;
this.identityKey = identityKey;
this.trustLevel = trustLevel;
this.addedTimestamp = addedTimestamp;
}
public RecipientId getRecipientId() {
return recipientId;
public ServiceId getServiceId() {
return serviceId;
}
public IdentityKey getIdentityKey() {

View file

@ -3,13 +3,12 @@ package org.asamk.signal.manager.storage.identities;
import org.asamk.signal.manager.api.TrustLevel;
import org.asamk.signal.manager.storage.Database;
import org.asamk.signal.manager.storage.Utils;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.recipients.RecipientIdCreator;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.state.IdentityKeyStore.Direction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.ServiceId;
import java.sql.Connection;
import java.sql.ResultSet;
@ -25,9 +24,8 @@ public class IdentityKeyStore {
private final static Logger logger = LoggerFactory.getLogger(IdentityKeyStore.class);
private static final String TABLE_IDENTITY = "identity";
private final Database database;
private final RecipientIdCreator recipientIdCreator;
private final TrustNewIdentity trustNewIdentity;
private final PublishSubject<RecipientId> identityChanges = PublishSubject.create();
private final PublishSubject<ServiceId> identityChanges = PublishSubject.create();
private boolean isRetryingDecryption = false;
@ -37,42 +35,37 @@ public class IdentityKeyStore {
statement.executeUpdate("""
CREATE TABLE identity (
_id INTEGER PRIMARY KEY,
recipient_id INTEGER UNIQUE NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,
uuid BLOB UNIQUE NOT NULL,
identity_key BLOB NOT NULL,
added_timestamp INTEGER NOT NULL,
trust_level INTEGER NOT NULL
);
) STRICT;
""");
}
}
public IdentityKeyStore(
final Database database,
final RecipientIdCreator recipientIdCreator,
final TrustNewIdentity trustNewIdentity
) {
public IdentityKeyStore(final Database database, final TrustNewIdentity trustNewIdentity) {
this.database = database;
this.recipientIdCreator = recipientIdCreator;
this.trustNewIdentity = trustNewIdentity;
}
public Observable<RecipientId> getIdentityChanges() {
public Observable<ServiceId> getIdentityChanges() {
return identityChanges;
}
public boolean saveIdentity(final RecipientId recipientId, final IdentityKey identityKey) {
public boolean saveIdentity(final ServiceId serviceId, final IdentityKey identityKey) {
if (isRetryingDecryption) {
return false;
}
try (final var connection = database.getConnection()) {
final var identityInfo = loadIdentity(connection, recipientId);
final var identityInfo = loadIdentity(connection, serviceId);
if (identityInfo != null && identityInfo.getIdentityKey().equals(identityKey)) {
// Identity already exists, not updating the trust level
logger.trace("Not storing new identity for recipient {}, identity already stored", recipientId);
logger.trace("Not storing new identity for recipient {}, identity already stored", serviceId);
return false;
}
saveNewIdentity(connection, recipientId, identityKey, identityInfo == null);
saveNewIdentity(connection, serviceId, identityKey, identityInfo == null);
return true;
} catch (SQLException e) {
throw new RuntimeException("Failed update identity store", e);
@ -83,24 +76,24 @@ public class IdentityKeyStore {
isRetryingDecryption = retryingDecryption;
}
public boolean setIdentityTrustLevel(RecipientId recipientId, IdentityKey identityKey, TrustLevel trustLevel) {
public boolean setIdentityTrustLevel(ServiceId serviceId, IdentityKey identityKey, TrustLevel trustLevel) {
try (final var connection = database.getConnection()) {
final var identityInfo = loadIdentity(connection, recipientId);
final var identityInfo = loadIdentity(connection, serviceId);
if (identityInfo == null) {
logger.debug("Not updating trust level for recipient {}, identity not found", recipientId);
logger.debug("Not updating trust level for recipient {}, identity not found", serviceId);
return false;
}
if (!identityInfo.getIdentityKey().equals(identityKey)) {
logger.debug("Not updating trust level for recipient {}, different identity found", recipientId);
logger.debug("Not updating trust level for recipient {}, different identity found", serviceId);
return false;
}
if (identityInfo.getTrustLevel() == trustLevel) {
logger.trace("Not updating trust level for recipient {}, trust level already matches", recipientId);
logger.trace("Not updating trust level for recipient {}, trust level already matches", serviceId);
return false;
}
logger.debug("Updating trust level for recipient {} with trust {}", recipientId, trustLevel);
final var newIdentityInfo = new IdentityInfo(recipientId,
logger.debug("Updating trust level for recipient {} with trust {}", serviceId, trustLevel);
final var newIdentityInfo = new IdentityInfo(serviceId,
identityKey,
trustLevel,
identityInfo.getDateAddedTimestamp());
@ -111,41 +104,41 @@ public class IdentityKeyStore {
}
}
public boolean isTrustedIdentity(RecipientId recipientId, IdentityKey identityKey, Direction direction) {
public boolean isTrustedIdentity(ServiceId serviceId, IdentityKey identityKey, Direction direction) {
if (trustNewIdentity == TrustNewIdentity.ALWAYS) {
return true;
}
try (final var connection = database.getConnection()) {
// TODO implement possibility for different handling of incoming/outgoing trust decisions
var identityInfo = loadIdentity(connection, recipientId);
var identityInfo = loadIdentity(connection, serviceId);
if (identityInfo == null) {
logger.debug("Initial identity found for {}, saving.", recipientId);
saveNewIdentity(connection, recipientId, identityKey, true);
identityInfo = loadIdentity(connection, recipientId);
logger.debug("Initial identity found for {}, saving.", serviceId);
saveNewIdentity(connection, serviceId, identityKey, true);
identityInfo = loadIdentity(connection, serviceId);
} else if (!identityInfo.getIdentityKey().equals(identityKey)) {
// Identity found, but different
if (direction == Direction.SENDING) {
logger.debug("Changed identity found for {}, saving.", recipientId);
saveNewIdentity(connection, recipientId, identityKey, false);
identityInfo = loadIdentity(connection, recipientId);
logger.debug("Changed identity found for {}, saving.", serviceId);
saveNewIdentity(connection, serviceId, identityKey, false);
identityInfo = loadIdentity(connection, serviceId);
} else {
logger.trace("Trusting identity for {} for {}: {}", recipientId, direction, false);
logger.trace("Trusting identity for {} for {}: {}", serviceId, direction, false);
return false;
}
}
final var isTrusted = identityInfo != null && identityInfo.isTrusted();
logger.trace("Trusting identity for {} for {}: {}", recipientId, direction, isTrusted);
logger.trace("Trusting identity for {} for {}: {}", serviceId, direction, isTrusted);
return isTrusted;
} catch (SQLException e) {
throw new RuntimeException("Failed read from identity store", e);
}
}
public IdentityInfo getIdentityInfo(RecipientId recipientId) {
public IdentityInfo getIdentityInfo(ServiceId serviceId) {
try (final var connection = database.getConnection()) {
return loadIdentity(connection, recipientId);
return loadIdentity(connection, serviceId);
} catch (SQLException e) {
throw new RuntimeException("Failed read from identity store", e);
}
@ -155,7 +148,7 @@ public class IdentityKeyStore {
try (final var connection = database.getConnection()) {
final var sql = (
"""
SELECT i.recipient_id, i.identity_key, i.added_timestamp, i.trust_level
SELECT i.uuid, i.identity_key, i.added_timestamp, i.trust_level
FROM %s AS i
"""
).formatted(TABLE_IDENTITY);
@ -167,32 +160,9 @@ public class IdentityKeyStore {
}
}
public void mergeRecipients(final RecipientId recipientId, final RecipientId toBeMergedRecipientId) {
public void deleteIdentity(final ServiceId serviceId) {
try (final var connection = database.getConnection()) {
connection.setAutoCommit(false);
final var sql = (
"""
UPDATE OR IGNORE %s
SET recipient_id = ?
WHERE recipient_id = ?
"""
).formatted(TABLE_IDENTITY);
try (final var statement = connection.prepareStatement(sql)) {
statement.setLong(1, recipientId.id());
statement.setLong(2, toBeMergedRecipientId.id());
statement.executeUpdate();
}
deleteIdentity(connection, toBeMergedRecipientId);
connection.commit();
} catch (SQLException e) {
throw new RuntimeException("Failed update identity store", e);
}
}
public void deleteIdentity(final RecipientId recipientId) {
try (final var connection = database.getConnection()) {
deleteIdentity(connection, recipientId);
deleteIdentity(connection, serviceId);
} catch (SQLException e) {
throw new RuntimeException("Failed update identity store", e);
}
@ -214,49 +184,49 @@ public class IdentityKeyStore {
}
private IdentityInfo loadIdentity(
final Connection connection, final RecipientId recipientId
final Connection connection, final ServiceId serviceId
) throws SQLException {
final var sql = (
"""
SELECT i.recipient_id, i.identity_key, i.added_timestamp, i.trust_level
SELECT i.uuid, i.identity_key, i.added_timestamp, i.trust_level
FROM %s AS i
WHERE i.recipient_id = ?
WHERE i.uuid = ?
"""
).formatted(TABLE_IDENTITY);
try (final var statement = connection.prepareStatement(sql)) {
statement.setLong(1, recipientId.id());
statement.setBytes(1, serviceId.toByteArray());
return Utils.executeQueryForOptional(statement, this::getIdentityInfoFromResultSet).orElse(null);
}
}
private void saveNewIdentity(
final Connection connection,
final RecipientId recipientId,
final ServiceId serviceId,
final IdentityKey identityKey,
final boolean firstIdentity
) throws SQLException {
final var trustLevel = trustNewIdentity == TrustNewIdentity.ALWAYS || (
trustNewIdentity == TrustNewIdentity.ON_FIRST_USE && firstIdentity
) ? TrustLevel.TRUSTED_UNVERIFIED : TrustLevel.UNTRUSTED;
logger.debug("Storing new identity for recipient {} with trust {}", recipientId, trustLevel);
final var newIdentityInfo = new IdentityInfo(recipientId, identityKey, trustLevel, System.currentTimeMillis());
logger.debug("Storing new identity for recipient {} with trust {}", serviceId, trustLevel);
final var newIdentityInfo = new IdentityInfo(serviceId, identityKey, trustLevel, System.currentTimeMillis());
storeIdentity(connection, newIdentityInfo);
identityChanges.onNext(recipientId);
identityChanges.onNext(serviceId);
}
private void storeIdentity(final Connection connection, final IdentityInfo identityInfo) throws SQLException {
logger.trace("Storing identity info for {}, trust: {}, added: {}",
identityInfo.getRecipientId(),
identityInfo.getServiceId(),
identityInfo.getTrustLevel(),
identityInfo.getDateAddedTimestamp());
final var sql = (
"""
INSERT OR REPLACE INTO %s (recipient_id, identity_key, added_timestamp, trust_level)
INSERT OR REPLACE INTO %s (uuid, identity_key, added_timestamp, trust_level)
VALUES (?, ?, ?, ?)
"""
).formatted(TABLE_IDENTITY);
try (final var statement = connection.prepareStatement(sql)) {
statement.setLong(1, identityInfo.getRecipientId().id());
statement.setBytes(1, identityInfo.getServiceId().toByteArray());
statement.setBytes(2, identityInfo.getIdentityKey().serialize());
statement.setLong(3, identityInfo.getDateAddedTimestamp());
statement.setInt(4, identityInfo.getTrustLevel().ordinal());
@ -264,27 +234,27 @@ public class IdentityKeyStore {
}
}
private void deleteIdentity(final Connection connection, final RecipientId recipientId) throws SQLException {
private void deleteIdentity(final Connection connection, final ServiceId serviceId) throws SQLException {
final var sql = (
"""
DELETE FROM %s AS i
WHERE i.recipient_id = ?
WHERE i.uuid = ?
"""
).formatted(TABLE_IDENTITY);
try (final var statement = connection.prepareStatement(sql)) {
statement.setLong(1, recipientId.id());
statement.setBytes(1, serviceId.toByteArray());
statement.executeUpdate();
}
}
private IdentityInfo getIdentityInfoFromResultSet(ResultSet resultSet) throws SQLException {
try {
final var recipientId = recipientIdCreator.create(resultSet.getLong("recipient_id"));
final var serviceId = ServiceId.parseOrThrow(resultSet.getBytes("uuid"));
final var id = new IdentityKey(resultSet.getBytes("identity_key"));
final var trustLevel = TrustLevel.fromInt(resultSet.getInt("trust_level"));
final var added = resultSet.getLong("added_timestamp");
return new IdentityInfo(recipientId, id, trustLevel, added);
return new IdentityInfo(serviceId, id, trustLevel, added);
} catch (InvalidKeyException e) {
logger.warn("Failed to load identity key, resetting: {}", e.getMessage());
return null;

View file

@ -3,6 +3,7 @@ package org.asamk.signal.manager.storage.identities;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.asamk.signal.manager.api.TrustLevel;
import org.asamk.signal.manager.helper.RecipientAddressResolver;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
import org.asamk.signal.manager.util.IOUtils;
@ -27,16 +28,21 @@ public class LegacyIdentityKeyStore {
private static final ObjectMapper objectMapper = org.asamk.signal.manager.storage.Utils.createStorageObjectMapper();
public static void migrate(
final File identitiesPath, final RecipientResolver resolver, final IdentityKeyStore identityKeyStore
final File identitiesPath,
final RecipientResolver resolver,
final RecipientAddressResolver addressResolver,
final IdentityKeyStore identityKeyStore
) {
final var identities = getIdentities(identitiesPath, resolver);
final var identities = getIdentities(identitiesPath, resolver, addressResolver);
identityKeyStore.addLegacyIdentities(identities);
removeIdentityFiles(identitiesPath);
}
static final Pattern identityFileNamePattern = Pattern.compile("(\\d+)");
private static List<IdentityInfo> getIdentities(final File identitiesPath, final RecipientResolver resolver) {
private static List<IdentityInfo> getIdentities(
final File identitiesPath, final RecipientResolver resolver, final RecipientAddressResolver addressResolver
) {
final var files = identitiesPath.listFiles();
if (files == null) {
return List.of();
@ -45,7 +51,7 @@ public class LegacyIdentityKeyStore {
.filter(f -> identityFileNamePattern.matcher(f.getName()).matches())
.map(f -> resolver.resolveRecipient(Long.parseLong(f.getName())))
.filter(Objects::nonNull)
.map(recipientId -> loadIdentityLocked(recipientId, identitiesPath))
.map(recipientId -> loadIdentityLocked(recipientId, addressResolver, identitiesPath))
.filter(Objects::nonNull)
.toList();
}
@ -59,7 +65,9 @@ public class LegacyIdentityKeyStore {
return new File(identitiesPath, String.valueOf(recipientId.id()));
}
private static IdentityInfo loadIdentityLocked(final RecipientId recipientId, final File identitiesPath) {
private static IdentityInfo loadIdentityLocked(
final RecipientId recipientId, RecipientAddressResolver addressResolver, final File identitiesPath
) {
final var file = getIdentityFile(recipientId, identitiesPath);
if (!file.exists()) {
return null;
@ -71,7 +79,8 @@ public class LegacyIdentityKeyStore {
var trustLevel = TrustLevel.fromInt(storage.trustLevel());
var added = storage.addedTimestamp();
return new IdentityInfo(recipientId, id, trustLevel, added);
final var serviceId = addressResolver.resolveRecipientAddress(recipientId).getServiceId();
return new IdentityInfo(serviceId, id, trustLevel, added);
} catch (IOException | InvalidKeyException e) {
logger.warn("Failed to load identity key: {}", e.getMessage());
return null;

View file

@ -1,16 +1,15 @@
package org.asamk.signal.manager.storage.identities;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.IdentityKeyPair;
import org.signal.libsignal.protocol.SignalProtocolAddress;
import org.whispersystems.signalservice.api.push.ServiceId;
import java.util.function.Supplier;
public class SignalIdentityKeyStore implements org.signal.libsignal.protocol.state.IdentityKeyStore {
private final RecipientResolver resolver;
private final Supplier<IdentityKeyPair> identityKeyPairSupplier;
private final int localRegistrationId;
private final IdentityKeyStore identityKeyStore;
@ -21,7 +20,6 @@ public class SignalIdentityKeyStore implements org.signal.libsignal.protocol.sta
final int localRegistrationId,
final IdentityKeyStore identityKeyStore
) {
this.resolver = resolver;
this.identityKeyPairSupplier = identityKeyPairSupplier;
this.localRegistrationId = localRegistrationId;
this.identityKeyStore = identityKeyStore;
@ -39,29 +37,22 @@ public class SignalIdentityKeyStore implements org.signal.libsignal.protocol.sta
@Override
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
final var recipientId = resolveRecipient(address.getName());
final var serviceId = ServiceId.parseOrThrow(address.getName());
return identityKeyStore.saveIdentity(recipientId, identityKey);
return identityKeyStore.saveIdentity(serviceId, identityKey);
}
@Override
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
var recipientId = resolveRecipient(address.getName());
final var serviceId = ServiceId.parseOrThrow(address.getName());
return identityKeyStore.isTrustedIdentity(recipientId, identityKey, direction);
return identityKeyStore.isTrustedIdentity(serviceId, identityKey, direction);
}
@Override
public IdentityKey getIdentity(SignalProtocolAddress address) {
var recipientId = resolveRecipient(address.getName());
final var identityInfo = identityKeyStore.getIdentityInfo(recipientId);
final var serviceId = ServiceId.parseOrThrow(address.getName());
final var identityInfo = identityKeyStore.getIdentityInfo(serviceId);
return identityInfo == null ? null : identityInfo.getIdentityKey();
}
/**
* @param identifier can be either a serialized uuid or an e164 phone number
*/
private RecipientId resolveRecipient(String identifier) {
return resolver.resolveRecipient(identifier);
}
}

View file

@ -35,6 +35,10 @@ public record RecipientAddress(Optional<UUID> uuid, Optional<String> number) {
this(Optional.of(uuid), Optional.empty());
}
public ServiceId getServiceId() {
return ServiceId.from(uuid.orElse(UNKNOWN_UUID));
}
public String getIdentifier() {
if (uuid.isPresent()) {
return uuid.get().toString();
@ -62,6 +66,6 @@ public record RecipientAddress(Optional<UUID> uuid, Optional<String> number) {
}
public SignalServiceAddress toSignalServiceAddress() {
return new SignalServiceAddress(ServiceId.from(uuid.orElse(UNKNOWN_UUID)), number);
return new SignalServiceAddress(getServiceId(), number);
}
}

View file

@ -4,14 +4,13 @@ import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupUtils;
import org.asamk.signal.manager.storage.Database;
import org.asamk.signal.manager.storage.Utils;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.crypto.ContentHint;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import java.io.IOException;
@ -32,14 +31,10 @@ public class MessageSendLogStore implements AutoCloseable {
private static final Duration LOG_DURATION = Duration.ofDays(1);
private final RecipientResolver recipientResolver;
private final Database database;
private final Thread cleanupThread;
public MessageSendLogStore(
final RecipientResolver recipientResolver, final Database database
) {
this.recipientResolver = recipientResolver;
public MessageSendLogStore(final Database database) {
this.database = database;
this.cleanupThread = new Thread(() -> {
try {
@ -69,9 +64,9 @@ public class MessageSendLogStore implements AutoCloseable {
CREATE TABLE message_send_log (
_id INTEGER PRIMARY KEY,
content_id INTEGER NOT NULL REFERENCES message_send_log_content (_id) ON DELETE CASCADE,
recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,
uuid BLOB NOT NULL,
device_id INTEGER NOT NULL
);
) STRICT;
CREATE TABLE message_send_log_content (
_id INTEGER PRIMARY KEY,
group_id BLOB,
@ -81,26 +76,26 @@ public class MessageSendLogStore implements AutoCloseable {
urgent BOOLEAN NOT NULL
);
CREATE INDEX mslc_timestamp_index ON message_send_log_content (timestamp);
CREATE INDEX msl_recipient_index ON message_send_log (recipient_id, device_id, content_id);
CREATE INDEX msl_recipient_index ON message_send_log (uuid, device_id, content_id);
CREATE INDEX msl_content_index ON message_send_log (content_id);
""");
}
}
public List<MessageSendLogEntry> findMessages(
final RecipientId recipientId, final int deviceId, final long timestamp, final boolean isSenderKey
final ServiceId serviceId, final int deviceId, final long timestamp, final boolean isSenderKey
) {
final var sql = """
SELECT group_id, content, content_hint
FROM %s l
INNER JOIN %s lc ON l.content_id = lc._id
WHERE l.recipient_id = ? AND l.device_id = ? AND lc.timestamp = ?
WHERE l.uuid = ? AND l.device_id = ? AND lc.timestamp = ?
""".formatted(TABLE_MESSAGE_SEND_LOG, TABLE_MESSAGE_SEND_LOG_CONTENT);
try (final var connection = database.getConnection()) {
deleteOutdatedEntries(connection);
try (final var statement = connection.prepareStatement(sql)) {
statement.setLong(1, recipientId.id());
statement.setBytes(1, serviceId.toByteArray());
statement.setInt(2, deviceId);
statement.setLong(3, timestamp);
try (var result = Utils.executeQueryForStream(statement, this::getMessageSendLogEntryFromResultSet)) {
@ -189,16 +184,16 @@ public class MessageSendLogStore implements AutoCloseable {
}
}
public void deleteEntryForRecipientNonGroup(long sentTimestamp, RecipientId recipientId) {
public void deleteEntryForRecipientNonGroup(long sentTimestamp, ServiceId serviceId) {
final var sql = """
DELETE FROM %s AS lc
WHERE lc.timestamp = ? AND lc.group_id IS NULL AND lc._id IN (SELECT content_id FROM %s l WHERE l.recipient_id = ?)
WHERE lc.timestamp = ? AND lc.group_id IS NULL AND lc._id IN (SELECT content_id FROM %s l WHERE l.uuid = ?)
""".formatted(TABLE_MESSAGE_SEND_LOG_CONTENT, TABLE_MESSAGE_SEND_LOG);
try (final var connection = database.getConnection()) {
connection.setAutoCommit(false);
try (final var statement = connection.prepareStatement(sql)) {
statement.setLong(1, sentTimestamp);
statement.setLong(2, recipientId.id());
statement.setBytes(2, serviceId.toByteArray());
statement.executeUpdate();
}
@ -209,21 +204,21 @@ public class MessageSendLogStore implements AutoCloseable {
}
}
public void deleteEntryForRecipient(long sentTimestamp, RecipientId recipientId, int deviceId) {
deleteEntriesForRecipient(List.of(sentTimestamp), recipientId, deviceId);
public void deleteEntryForRecipient(long sentTimestamp, ServiceId serviceId, int deviceId) {
deleteEntriesForRecipient(List.of(sentTimestamp), serviceId, deviceId);
}
public void deleteEntriesForRecipient(List<Long> sentTimestamps, RecipientId recipientId, int deviceId) {
public void deleteEntriesForRecipient(List<Long> sentTimestamps, ServiceId serviceId, int deviceId) {
final var sql = """
DELETE FROM %s AS l
WHERE l.content_id IN (SELECT _id FROM %s lc WHERE lc.timestamp = ?) AND l.recipient_id = ? AND l.device_id = ?
WHERE l.content_id IN (SELECT _id FROM %s lc WHERE lc.timestamp = ?) AND l.uuid = ? AND l.device_id = ?
""".formatted(TABLE_MESSAGE_SEND_LOG, TABLE_MESSAGE_SEND_LOG_CONTENT);
try (final var connection = database.getConnection()) {
connection.setAutoCommit(false);
try (final var statement = connection.prepareStatement(sql)) {
for (final var sentTimestamp : sentTimestamps) {
statement.setLong(1, sentTimestamp);
statement.setLong(2, recipientId.id());
statement.setBytes(2, serviceId.toByteArray());
statement.setInt(3, deviceId);
statement.executeUpdate();
}
@ -247,8 +242,8 @@ public class MessageSendLogStore implements AutoCloseable {
private RecipientDevices getRecipientDevices(final SendMessageResult sendMessageResult) {
if (sendMessageResult.isSuccess() && sendMessageResult.getSuccess().getContent().isPresent()) {
final var recipientId = recipientResolver.resolveRecipient(sendMessageResult.getAddress());
return new RecipientDevices(recipientId, sendMessageResult.getSuccess().getDevices());
final var serviceId = sendMessageResult.getAddress().getServiceId();
return new RecipientDevices(serviceId, sendMessageResult.getSuccess().getDevices());
} else {
return null;
}
@ -332,13 +327,13 @@ public class MessageSendLogStore implements AutoCloseable {
final long contentId, final List<RecipientDevices> recipientDevices, final Connection connection
) throws SQLException {
final var sql = """
INSERT INTO %s (recipient_id, device_id, content_id)
INSERT INTO %s (uuid, device_id, content_id)
VALUES (?,?,?)
""".formatted(TABLE_MESSAGE_SEND_LOG);
try (final var statement = connection.prepareStatement(sql)) {
for (final var recipientDevice : recipientDevices) {
for (final var deviceId : recipientDevice.deviceIds()) {
statement.setLong(1, recipientDevice.recipientId().id());
statement.setBytes(1, recipientDevice.serviceId().toByteArray());
statement.setInt(2, deviceId);
statement.setLong(3, contentId);
statement.executeUpdate();
@ -387,5 +382,5 @@ public class MessageSendLogStore implements AutoCloseable {
return new MessageSendLogEntry(groupId, content, contentHint, urgent);
}
private record RecipientDevices(RecipientId recipientId, List<Integer> deviceIds) {}
private record RecipientDevices(ServiceId serviceId, List<Integer> deviceIds) {}
}

View file

@ -1,11 +1,14 @@
package org.asamk.signal.manager.storage.senderKeys;
import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.helper.RecipientAddressResolver;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
import org.signal.libsignal.protocol.InvalidMessageException;
import org.signal.libsignal.protocol.groups.state.SenderKeyRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.ServiceId;
import java.io.File;
import java.io.FileInputStream;
@ -18,14 +21,15 @@ import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.asamk.signal.manager.storage.senderKeys.SenderKeyRecordStore.Key;
public class LegacySenderKeyRecordStore {
private final static Logger logger = LoggerFactory.getLogger(LegacySenderKeyRecordStore.class);
public static void migrate(
final File senderKeysPath, final RecipientResolver resolver, SenderKeyStore senderKeyStore
final File senderKeysPath,
final RecipientResolver resolver,
final RecipientAddressResolver addressResolver,
final SenderKeyStore senderKeyStore
) {
final var files = senderKeysPath.listFiles();
if (files == null) {
@ -34,10 +38,13 @@ public class LegacySenderKeyRecordStore {
final var senderKeys = parseFileNames(files, resolver).stream().map(key -> {
final var record = loadSenderKeyLocked(key, senderKeysPath);
if (record == null) {
final var uuid = addressResolver.resolveRecipientAddress(key.recipientId).uuid();
if (record == null || uuid.isEmpty()) {
return null;
}
return new Pair<>(key, record);
return new Pair<>(new SenderKeyRecordStore.Key(ServiceId.from(uuid.get()),
key.deviceId,
key.distributionId), record);
}).filter(Objects::nonNull).toList();
senderKeyStore.addLegacySenderKeys(senderKeys);
@ -98,4 +105,6 @@ public class LegacySenderKeyRecordStore {
return null;
}
}
record Key(RecipientId recipientId, int deviceId, UUID distributionId) {}
}

View file

@ -1,11 +1,13 @@
package org.asamk.signal.manager.storage.senderKeys;
import org.asamk.signal.manager.helper.RecipientAddressResolver;
import org.asamk.signal.manager.storage.Utils;
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
import org.asamk.signal.manager.storage.senderKeys.SenderKeySharedStore.SenderKeySharedEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.DistributionId;
import org.whispersystems.signalservice.api.push.ServiceId;
import java.io.File;
import java.io.FileInputStream;
@ -21,7 +23,10 @@ public class LegacySenderKeySharedStore {
private final static Logger logger = LoggerFactory.getLogger(LegacySenderKeySharedStore.class);
public static void migrate(
final File file, final RecipientResolver resolver, SenderKeyStore senderKeyStore
final File file,
final RecipientResolver resolver,
final RecipientAddressResolver addressResolver,
final SenderKeyStore senderKeyStore
) {
final var objectMapper = Utils.createStorageObjectMapper();
try (var inputStream = new FileInputStream(file)) {
@ -32,7 +37,11 @@ public class LegacySenderKeySharedStore {
if (recipientId == null) {
continue;
}
final var entry = new SenderKeySharedEntry(recipientId, senderKey.deviceId);
final var uuid = addressResolver.resolveRecipientAddress(recipientId).uuid();
if (uuid.isEmpty()) {
continue;
}
final var entry = new SenderKeySharedEntry(ServiceId.from(uuid.get()), senderKey.deviceId);
final var distributionId = DistributionId.from(senderKey.distributionId);
var entries = sharedSenderKeys.get(distributionId);
if (entries == null) {

View file

@ -3,14 +3,13 @@ package org.asamk.signal.manager.storage.senderKeys;
import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.storage.Database;
import org.asamk.signal.manager.storage.Utils;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
import org.signal.libsignal.protocol.InvalidMessageException;
import org.signal.libsignal.protocol.SignalProtocolAddress;
import org.signal.libsignal.protocol.groups.state.SenderKeyRecord;
import org.signal.libsignal.protocol.groups.state.SenderKeyStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.sql.Connection;
@ -25,7 +24,6 @@ public class SenderKeyRecordStore implements SenderKeyStore {
private final static String TABLE_SENDER_KEY = "sender_key";
private final Database database;
private final RecipientResolver resolver;
public static void createSql(Connection connection) throws SQLException {
// When modifying the CREATE statement here, also add a migration in AccountDatabase.java
@ -33,22 +31,19 @@ public class SenderKeyRecordStore implements SenderKeyStore {
statement.executeUpdate("""
CREATE TABLE sender_key (
_id INTEGER PRIMARY KEY,
recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,
uuid BLOB NOT NULL,
device_id INTEGER NOT NULL,
distribution_id BLOB NOT NULL,
record BLOB NOT NULL,
created_timestamp INTEGER NOT NULL,
UNIQUE(recipient_id, device_id, distribution_id)
);
UNIQUE(uuid, device_id, distribution_id)
) STRICT;
""");
}
}
SenderKeyRecordStore(
final Database database, final RecipientResolver resolver
) {
SenderKeyRecordStore(final Database database) {
this.database = database;
this.resolver = resolver;
}
@Override
@ -75,17 +70,17 @@ public class SenderKeyRecordStore implements SenderKeyStore {
}
}
long getCreateTimeForKey(final RecipientId selfRecipientId, final int selfDeviceId, final UUID distributionId) {
long getCreateTimeForKey(final ServiceId selfServiceId, final int selfDeviceId, final UUID distributionId) {
final var sql = (
"""
SELECT s.created_timestamp
FROM %s AS s
WHERE s.recipient_id = ? AND s.device_id = ? AND s.distribution_id = ?
WHERE s.uuid = ? AND s.device_id = ? AND s.distribution_id = ?
"""
).formatted(TABLE_SENDER_KEY);
try (final var connection = database.getConnection()) {
try (final var statement = connection.prepareStatement(sql)) {
statement.setLong(1, selfRecipientId.id());
statement.setBytes(1, selfServiceId.toByteArray());
statement.setInt(2, selfDeviceId);
statement.setBytes(3, UuidUtil.toByteArray(distributionId));
return Utils.executeQueryForOptional(statement, res -> res.getLong("created_timestamp")).orElse(-1L);
@ -95,16 +90,16 @@ public class SenderKeyRecordStore implements SenderKeyStore {
}
}
void deleteSenderKey(final RecipientId recipientId, final UUID distributionId) {
void deleteSenderKey(final ServiceId serviceId, final UUID distributionId) {
final var sql = (
"""
DELETE FROM %s AS s
WHERE s.recipient_id = ? AND s.distribution_id = ?
WHERE s.uuid = ? AND s.distribution_id = ?
"""
).formatted(TABLE_SENDER_KEY);
try (final var connection = database.getConnection()) {
try (final var statement = connection.prepareStatement(sql)) {
statement.setLong(1, recipientId.id());
statement.setBytes(1, serviceId.toByteArray());
statement.setBytes(2, UuidUtil.toByteArray(distributionId));
statement.executeUpdate();
}
@ -126,33 +121,9 @@ public class SenderKeyRecordStore implements SenderKeyStore {
}
}
void deleteAllFor(final RecipientId recipientId) {
void deleteAllFor(final ServiceId serviceId) {
try (final var connection = database.getConnection()) {
deleteAllFor(connection, recipientId);
} catch (SQLException e) {
throw new RuntimeException("Failed update sender key store", e);
}
}
void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
try (final var connection = database.getConnection()) {
connection.setAutoCommit(false);
final var sql = """
UPDATE OR IGNORE %s
SET recipient_id = ?
WHERE recipient_id = ?
""".formatted(TABLE_SENDER_KEY);
try (final var statement = connection.prepareStatement(sql)) {
statement.setLong(1, recipientId.id());
statement.setLong(2, toBeMergedRecipientId.id());
final var rows = statement.executeUpdate();
if (rows > 0) {
logger.debug("Reassigned {} sender keys of to be merged recipient.", rows);
}
}
// Delete all conflicting sender keys now
deleteAllFor(connection, toBeMergedRecipientId);
connection.commit();
deleteAllFor(connection, serviceId);
} catch (SQLException e) {
throw new RuntimeException("Failed update sender key store", e);
}
@ -173,16 +144,9 @@ public class SenderKeyRecordStore implements SenderKeyStore {
logger.debug("Complete sender keys migration took {}ms", (System.nanoTime() - start) / 1000000);
}
/**
* @param identifier can be either a serialized uuid or an e164 phone number
*/
private RecipientId resolveRecipient(String identifier) {
return resolver.resolveRecipient(identifier);
}
private Key getKey(final SignalProtocolAddress address, final UUID distributionId) {
final var recipientId = resolveRecipient(address.getName());
return new Key(recipientId, address.getDeviceId(), distributionId);
final var serviceId = ServiceId.parseOrThrow(address.getName());
return new Key(serviceId, address.getDeviceId(), distributionId);
}
private SenderKeyRecord loadSenderKey(final Connection connection, final Key key) throws SQLException {
@ -190,11 +154,11 @@ public class SenderKeyRecordStore implements SenderKeyStore {
"""
SELECT s.record
FROM %s AS s
WHERE s.recipient_id = ? AND s.device_id = ? AND s.distribution_id = ?
WHERE s.uuid = ? AND s.device_id = ? AND s.distribution_id = ?
"""
).formatted(TABLE_SENDER_KEY);
try (final var statement = connection.prepareStatement(sql)) {
statement.setLong(1, key.recipientId().id());
statement.setBytes(1, key.serviceId().toByteArray());
statement.setInt(2, key.deviceId());
statement.setBytes(3, UuidUtil.toByteArray(key.distributionId()));
return Utils.executeQueryForOptional(statement, this::getSenderKeyRecordFromResultSet).orElse(null);
@ -207,11 +171,11 @@ public class SenderKeyRecordStore implements SenderKeyStore {
final var sqlUpdate = """
UPDATE %s
SET record = ?
WHERE recipient_id = ? AND device_id = ? and distribution_id = ?
WHERE uuid = ? AND device_id = ? and distribution_id = ?
""".formatted(TABLE_SENDER_KEY);
try (final var statement = connection.prepareStatement(sqlUpdate)) {
statement.setBytes(1, senderKeyRecord.serialize());
statement.setLong(2, key.recipientId().id());
statement.setBytes(2, key.serviceId().toByteArray());
statement.setLong(3, key.deviceId());
statement.setBytes(4, UuidUtil.toByteArray(key.distributionId()));
final var rows = statement.executeUpdate();
@ -223,12 +187,12 @@ public class SenderKeyRecordStore implements SenderKeyStore {
// Record doesn't exist yet, creating a new one
final var sqlInsert = (
"""
INSERT OR REPLACE INTO %s (recipient_id, device_id, distribution_id, record, created_timestamp)
INSERT OR REPLACE INTO %s (uuid, device_id, distribution_id, record, created_timestamp)
VALUES (?, ?, ?, ?, ?)
"""
).formatted(TABLE_SENDER_KEY);
try (final var statement = connection.prepareStatement(sqlInsert)) {
statement.setLong(1, key.recipientId().id());
statement.setBytes(1, key.serviceId().toByteArray());
statement.setInt(2, key.deviceId());
statement.setBytes(3, UuidUtil.toByteArray(key.distributionId()));
statement.setBytes(4, senderKeyRecord.serialize());
@ -237,15 +201,15 @@ public class SenderKeyRecordStore implements SenderKeyStore {
}
}
private void deleteAllFor(final Connection connection, final RecipientId recipientId) throws SQLException {
private void deleteAllFor(final Connection connection, final ServiceId serviceId) throws SQLException {
final var sql = (
"""
DELETE FROM %s AS s
WHERE s.recipient_id = ?
WHERE s.uuid = ?
"""
).formatted(TABLE_SENDER_KEY);
try (final var statement = connection.prepareStatement(sql)) {
statement.setLong(1, recipientId.id());
statement.setBytes(1, serviceId.toByteArray());
statement.executeUpdate();
}
}
@ -261,5 +225,5 @@ public class SenderKeyRecordStore implements SenderKeyStore {
}
}
record Key(RecipientId recipientId, int deviceId, UUID distributionId) {}
record Key(ServiceId serviceId, int deviceId, UUID distributionId) {}
}

View file

@ -1,15 +1,12 @@
package org.asamk.signal.manager.storage.senderKeys;
import org.asamk.signal.manager.helper.RecipientAddressResolver;
import org.asamk.signal.manager.storage.Database;
import org.asamk.signal.manager.storage.Utils;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.recipients.RecipientIdCreator;
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
import org.signal.libsignal.protocol.SignalProtocolAddress;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.DistributionId;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.sql.Connection;
@ -26,9 +23,6 @@ public class SenderKeySharedStore {
private final static String TABLE_SENDER_KEY_SHARED = "sender_key_shared";
private final Database database;
private final RecipientIdCreator recipientIdCreator;
private final RecipientResolver resolver;
private final RecipientAddressResolver addressResolver;
public static void createSql(Connection connection) throws SQLException {
// When modifying the CREATE statement here, also add a migration in AccountDatabase.java
@ -36,33 +30,25 @@ public class SenderKeySharedStore {
statement.executeUpdate("""
CREATE TABLE sender_key_shared (
_id INTEGER PRIMARY KEY,
recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,
uuid BLOB NOT NULL,
device_id INTEGER NOT NULL,
distribution_id BLOB NOT NULL,
timestamp INTEGER NOT NULL,
UNIQUE(recipient_id, device_id, distribution_id)
);
UNIQUE(uuid, device_id, distribution_id)
) STRICT;
""");
}
}
SenderKeySharedStore(
final Database database,
final RecipientIdCreator recipientIdCreator,
final RecipientAddressResolver addressResolver,
final RecipientResolver resolver
) {
SenderKeySharedStore(final Database database) {
this.database = database;
this.recipientIdCreator = recipientIdCreator;
this.addressResolver = addressResolver;
this.resolver = resolver;
}
public Set<SignalProtocolAddress> getSenderKeySharedWith(final DistributionId distributionId) {
try (final var connection = database.getConnection()) {
final var sql = (
"""
SELECT s.recipient_id, s.device_id
SELECT s.uuid, s.device_id
FROM %s AS s
WHERE s.distribution_id = ?
"""
@ -70,8 +56,7 @@ public class SenderKeySharedStore {
try (final var statement = connection.prepareStatement(sql)) {
statement.setBytes(1, UuidUtil.toByteArray(distributionId.asUuid()));
return Utils.executeQueryForStream(statement, this::getSenderKeySharedEntryFromResultSet)
.map(k -> new SignalProtocolAddress(addressResolver.resolveRecipientAddress(k.recipientId())
.getIdentifier(), k.deviceId()))
.map(k -> k.serviceId.toProtocolAddress(k.deviceId()))
.collect(Collectors.toSet());
}
} catch (SQLException e) {
@ -83,7 +68,7 @@ public class SenderKeySharedStore {
final DistributionId distributionId, final Collection<SignalProtocolAddress> addresses
) {
final var newEntries = addresses.stream()
.map(a -> new SenderKeySharedEntry(resolver.resolveRecipient(a.getName()), a.getDeviceId()))
.map(a -> new SenderKeySharedEntry(ServiceId.parseOrThrow(a.getName()), a.getDeviceId()))
.collect(Collectors.toSet());
try (final var connection = database.getConnection()) {
@ -97,7 +82,8 @@ public class SenderKeySharedStore {
public void clearSenderKeySharedWith(final Collection<SignalProtocolAddress> addresses) {
final var entriesToDelete = addresses.stream()
.map(a -> new SenderKeySharedEntry(resolver.resolveRecipient(a.getName()), a.getDeviceId()))
.filter(a -> UuidUtil.isUuid(a.getName()))
.map(a -> new SenderKeySharedEntry(ServiceId.parseOrThrow(a.getName()), a.getDeviceId()))
.collect(Collectors.toSet());
try (final var connection = database.getConnection()) {
@ -105,12 +91,12 @@ public class SenderKeySharedStore {
final var sql = (
"""
DELETE FROM %s AS s
WHERE recipient_id = ? AND device_id = ?
WHERE uuid = ? AND device_id = ?
"""
).formatted(TABLE_SENDER_KEY_SHARED);
try (final var statement = connection.prepareStatement(sql)) {
for (final var entry : entriesToDelete) {
statement.setLong(1, entry.recipientId().id());
statement.setBytes(1, entry.serviceId().toByteArray());
statement.setInt(2, entry.deviceId());
statement.executeUpdate();
}
@ -136,16 +122,16 @@ public class SenderKeySharedStore {
}
}
public void deleteAllFor(final RecipientId recipientId) {
public void deleteAllFor(final ServiceId serviceId) {
try (final var connection = database.getConnection()) {
final var sql = (
"""
DELETE FROM %s AS s
WHERE recipient_id = ?
WHERE uuid = ?
"""
).formatted(TABLE_SENDER_KEY_SHARED);
try (final var statement = connection.prepareStatement(sql)) {
statement.setLong(1, recipientId.id());
statement.setBytes(1, serviceId.toByteArray());
statement.executeUpdate();
}
} catch (SQLException e) {
@ -154,17 +140,17 @@ public class SenderKeySharedStore {
}
public void deleteSharedWith(
final RecipientId recipientId, final int deviceId, final DistributionId distributionId
final ServiceId serviceId, final int deviceId, final DistributionId distributionId
) {
try (final var connection = database.getConnection()) {
final var sql = (
"""
DELETE FROM %s AS s
WHERE recipient_id = ? AND device_id = ? AND distribution_id = ?
WHERE uuid = ? AND device_id = ? AND distribution_id = ?
"""
).formatted(TABLE_SENDER_KEY_SHARED);
try (final var statement = connection.prepareStatement(sql)) {
statement.setLong(1, recipientId.id());
statement.setBytes(1, serviceId.toByteArray());
statement.setInt(2, deviceId);
statement.setBytes(3, UuidUtil.toByteArray(distributionId.asUuid()));
statement.executeUpdate();
@ -191,25 +177,6 @@ public class SenderKeySharedStore {
}
}
public void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
try (final var connection = database.getConnection()) {
final var sql = (
"""
UPDATE OR REPLACE %s
SET recipient_id = ?
WHERE recipient_id = ?
"""
).formatted(TABLE_SENDER_KEY_SHARED);
try (final var statement = connection.prepareStatement(sql)) {
statement.setLong(1, recipientId.id());
statement.setLong(2, toBeMergedRecipientId.id());
statement.executeUpdate();
}
} catch (SQLException e) {
throw new RuntimeException("Failed update shared sender key store", e);
}
}
void addLegacySenderKeysShared(final Map<DistributionId, Set<SenderKeySharedEntry>> sharedSenderKeys) {
logger.debug("Migrating legacy sender keys shared to database");
long start = System.nanoTime();
@ -230,13 +197,13 @@ public class SenderKeySharedStore {
) throws SQLException {
final var sql = (
"""
INSERT OR REPLACE INTO %s (recipient_id, device_id, distribution_id, timestamp)
INSERT OR REPLACE INTO %s (uuid, device_id, distribution_id, timestamp)
VALUES (?, ?, ?, ?)
"""
).formatted(TABLE_SENDER_KEY_SHARED);
try (final var statement = connection.prepareStatement(sql)) {
for (final var entry : newEntries) {
statement.setLong(1, entry.recipientId().id());
statement.setBytes(1, entry.serviceId().toByteArray());
statement.setInt(2, entry.deviceId());
statement.setBytes(3, UuidUtil.toByteArray(distributionId.asUuid()));
statement.setLong(4, System.currentTimeMillis());
@ -246,10 +213,10 @@ public class SenderKeySharedStore {
}
private SenderKeySharedEntry getSenderKeySharedEntryFromResultSet(ResultSet resultSet) throws SQLException {
final var recipientId = resultSet.getLong("recipient_id");
final var serviceId = ServiceId.parseOrThrow(resultSet.getBytes("uuid"));
final var deviceId = resultSet.getInt("device_id");
return new SenderKeySharedEntry(recipientIdCreator.create(recipientId), deviceId);
return new SenderKeySharedEntry(serviceId, deviceId);
}
record SenderKeySharedEntry(RecipientId recipientId, int deviceId) {}
record SenderKeySharedEntry(ServiceId serviceId, int deviceId) {}
}

View file

@ -1,15 +1,12 @@
package org.asamk.signal.manager.storage.senderKeys;
import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.helper.RecipientAddressResolver;
import org.asamk.signal.manager.storage.Database;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.recipients.RecipientIdCreator;
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
import org.signal.libsignal.protocol.SignalProtocolAddress;
import org.signal.libsignal.protocol.groups.state.SenderKeyRecord;
import org.whispersystems.signalservice.api.SignalServiceSenderKeyStore;
import org.whispersystems.signalservice.api.push.DistributionId;
import org.whispersystems.signalservice.api.push.ServiceId;
import java.util.Collection;
import java.util.Map;
@ -21,14 +18,9 @@ public class SenderKeyStore implements SignalServiceSenderKeyStore {
private final SenderKeyRecordStore senderKeyRecordStore;
private final SenderKeySharedStore senderKeySharedStore;
public SenderKeyStore(
final Database database,
final RecipientAddressResolver addressResolver,
final RecipientResolver resolver,
final RecipientIdCreator recipientIdCreator
) {
this.senderKeyRecordStore = new SenderKeyRecordStore(database, resolver);
this.senderKeySharedStore = new SenderKeySharedStore(database, recipientIdCreator, addressResolver, resolver);
public SenderKeyStore(final Database database) {
this.senderKeyRecordStore = new SenderKeyRecordStore(database);
this.senderKeySharedStore = new SenderKeySharedStore(database);
}
@Override
@ -65,31 +57,26 @@ public class SenderKeyStore implements SignalServiceSenderKeyStore {
senderKeyRecordStore.deleteAll();
}
public void deleteAll(RecipientId recipientId) {
senderKeySharedStore.deleteAllFor(recipientId);
senderKeyRecordStore.deleteAllFor(recipientId);
public void deleteAll(ServiceId serviceId) {
senderKeySharedStore.deleteAllFor(serviceId);
senderKeyRecordStore.deleteAllFor(serviceId);
}
public void deleteSharedWith(RecipientId recipientId) {
senderKeySharedStore.deleteAllFor(recipientId);
public void deleteSharedWith(ServiceId serviceId) {
senderKeySharedStore.deleteAllFor(serviceId);
}
public void deleteSharedWith(RecipientId recipientId, int deviceId, DistributionId distributionId) {
senderKeySharedStore.deleteSharedWith(recipientId, deviceId, distributionId);
public void deleteSharedWith(ServiceId serviceId, int deviceId, DistributionId distributionId) {
senderKeySharedStore.deleteSharedWith(serviceId, deviceId, distributionId);
}
public void deleteOurKey(RecipientId selfRecipientId, DistributionId distributionId) {
public void deleteOurKey(ServiceId selfServiceId, DistributionId distributionId) {
senderKeySharedStore.deleteAllFor(distributionId);
senderKeyRecordStore.deleteSenderKey(selfRecipientId, distributionId.asUuid());
senderKeyRecordStore.deleteSenderKey(selfServiceId, 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);
public long getCreateTimeForOurKey(ServiceId selfServiceId, int deviceId, DistributionId distributionId) {
return senderKeyRecordStore.getCreateTimeForKey(selfServiceId, deviceId, distributionId.asUuid());
}
void addLegacySenderKeys(final Collection<Pair<SenderKeyRecordStore.Key, SenderKeyRecord>> senderKeys) {

View file

@ -1,12 +1,14 @@
package org.asamk.signal.manager.storage.sessions;
import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.helper.RecipientAddressResolver;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
import org.asamk.signal.manager.storage.sessions.SessionStore.Key;
import org.asamk.signal.manager.util.IOUtils;
import org.signal.libsignal.protocol.state.SessionRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.ServiceId;
import java.io.File;
import java.io.FileInputStream;
@ -24,15 +26,19 @@ public class LegacySessionStore {
private final static Logger logger = LoggerFactory.getLogger(LegacySessionStore.class);
public static void migrate(
final File sessionsPath, final RecipientResolver resolver, final SessionStore sessionStore
final File sessionsPath,
final RecipientResolver resolver,
final RecipientAddressResolver addressResolver,
final SessionStore sessionStore
) {
final var keys = getKeysLocked(sessionsPath, resolver);
final var sessions = keys.stream().map(key -> {
final var record = loadSessionLocked(key, sessionsPath);
if (record == null) {
final var uuid = addressResolver.resolveRecipientAddress(key.recipientId).uuid();
if (record == null || uuid.isEmpty()) {
return null;
}
return new Pair<>(key, record);
return new Pair<>(new SessionStore.Key(ServiceId.from(uuid.get()), key.deviceId()), record);
}).filter(Objects::nonNull).toList();
sessionStore.addLegacySessions(sessions);
deleteAllSessions(sessionsPath);
@ -104,4 +110,6 @@ public class LegacySessionStore {
return null;
}
}
record Key(RecipientId recipientId, int deviceId) {}
}

View file

@ -3,9 +3,6 @@ package org.asamk.signal.manager.storage.sessions;
import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.storage.Database;
import org.asamk.signal.manager.storage.Utils;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.recipients.RecipientIdCreator;
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
import org.signal.libsignal.protocol.InvalidMessageException;
import org.signal.libsignal.protocol.NoSessionException;
import org.signal.libsignal.protocol.SignalProtocolAddress;
@ -15,7 +12,9 @@ import org.signal.libsignal.protocol.state.SessionRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalServiceSessionStore;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.ServiceIdType;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.sql.Connection;
import java.sql.ResultSet;
@ -38,8 +37,6 @@ public class SessionStore implements SignalServiceSessionStore {
private final Database database;
private final int accountIdType;
private final RecipientResolver resolver;
private final RecipientIdCreator recipientIdCreator;
public static void createSql(Connection connection) throws SQLException {
// When modifying the CREATE statement here, also add a migration in AccountDatabase.java
@ -48,25 +45,18 @@ public class SessionStore implements SignalServiceSessionStore {
CREATE TABLE session (
_id INTEGER PRIMARY KEY,
account_id_type INTEGER NOT NULL,
recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,
uuid BLOB NOT NULL,
device_id INTEGER NOT NULL,
record BLOB NOT NULL,
UNIQUE(account_id_type, recipient_id, device_id)
);
UNIQUE(account_id_type, uuid, device_id)
) STRICT;
""");
}
}
public SessionStore(
final Database database,
final ServiceIdType serviceIdType,
final RecipientResolver resolver,
final RecipientIdCreator recipientIdCreator
) {
public SessionStore(final Database database, final ServiceIdType serviceIdType) {
this.database = database;
this.accountIdType = Utils.getAccountIdType(serviceIdType);
this.resolver = resolver;
this.recipientIdCreator = recipientIdCreator;
}
@Override
@ -111,19 +101,19 @@ public class SessionStore implements SignalServiceSessionStore {
@Override
public List<Integer> getSubDeviceSessions(String name) {
final var recipientId = resolver.resolveRecipient(name);
final var serviceId = ServiceId.parseOrThrow(name);
// get all sessions for recipient except primary device session
final var sql = (
"""
SELECT s.device_id
FROM %s AS s
WHERE s.account_id_type = ? AND s.recipient_id = ? AND s.device_id != 1
WHERE s.account_id_type = ? AND s.uuid = ? AND s.device_id != 1
"""
).formatted(TABLE_SESSION);
try (final var connection = database.getConnection()) {
try (final var statement = connection.prepareStatement(sql)) {
statement.setInt(1, accountIdType);
statement.setLong(2, recipientId.id());
statement.setBytes(2, serviceId.toByteArray());
return Utils.executeQueryForStream(statement, res -> res.getInt("device_id")).toList();
}
} catch (SQLException e) {
@ -131,8 +121,8 @@ public class SessionStore implements SignalServiceSessionStore {
}
}
public boolean isCurrentRatchetKey(RecipientId recipientId, int deviceId, ECPublicKey ratchetKey) {
final var key = new Key(recipientId, deviceId);
public boolean isCurrentRatchetKey(ServiceId serviceId, int deviceId, ECPublicKey ratchetKey) {
final var key = new Key(serviceId, deviceId);
try (final var connection = database.getConnection()) {
final var session = loadSession(connection, key);
@ -181,13 +171,13 @@ public class SessionStore implements SignalServiceSessionStore {
@Override
public void deleteAllSessions(String name) {
final var recipientId = resolver.resolveRecipient(name);
deleteAllSessions(recipientId);
final var serviceId = ServiceId.parseOrThrow(name);
deleteAllSessions(serviceId);
}
public void deleteAllSessions(RecipientId recipientId) {
public void deleteAllSessions(ServiceId serviceId) {
try (final var connection = database.getConnection()) {
deleteAllSessions(connection, recipientId);
deleteAllSessions(connection, serviceId);
} catch (SQLException e) {
throw new RuntimeException("Failed update session store", e);
}
@ -195,6 +185,10 @@ public class SessionStore implements SignalServiceSessionStore {
@Override
public void archiveSession(final SignalProtocolAddress address) {
if (!UuidUtil.isUuid(address.getName())) {
return;
}
final var key = getKey(address);
try (final var connection = database.getConnection()) {
@ -212,19 +206,17 @@ public class SessionStore implements SignalServiceSessionStore {
@Override
public Set<SignalProtocolAddress> getAllAddressesWithActiveSessions(final List<String> addressNames) {
final var recipientIdToNameMap = addressNames.stream()
.collect(Collectors.toMap(resolver::resolveRecipient, name -> name));
final var recipientIdsCommaSeparated = recipientIdToNameMap.keySet()
.stream()
.map(recipientId -> String.valueOf(recipientId.id()))
final var serviceIdsCommaSeparated = addressNames.stream()
.map(ServiceId::parseOrThrow)
.map(ServiceId::toString)
.collect(Collectors.joining(","));
final var sql = (
"""
SELECT s.recipient_id, s.device_id, s.record
SELECT s.uuid, s.device_id, s.record
FROM %s AS s
WHERE s.account_id_type = ? AND s.recipient_id IN (%s)
WHERE s.account_id_type = ? AND s.uuid IN (%s)
"""
).formatted(TABLE_SESSION, recipientIdsCommaSeparated);
).formatted(TABLE_SESSION, serviceIdsCommaSeparated);
try (final var connection = database.getConnection()) {
try (final var statement = connection.prepareStatement(sql)) {
statement.setInt(1, accountIdType);
@ -232,8 +224,7 @@ public class SessionStore implements SignalServiceSessionStore {
res -> new Pair<>(getKeyFromResultSet(res), getSessionRecordFromResultSet(res)))
.filter(pair -> isActive(pair.second()))
.map(Pair::first)
.map(key -> new SignalProtocolAddress(recipientIdToNameMap.get(key.recipientId),
key.deviceId()))
.map(key -> key.serviceId().toProtocolAddress(key.deviceId()))
.collect(Collectors.toSet());
}
} catch (SQLException e) {
@ -244,7 +235,7 @@ public class SessionStore implements SignalServiceSessionStore {
public void archiveAllSessions() {
final var sql = (
"""
SELECT s.recipient_id, s.device_id, s.record
SELECT s.uuid, s.device_id, s.record
FROM %s AS s
WHERE s.account_id_type = ?
"""
@ -267,12 +258,12 @@ public class SessionStore implements SignalServiceSessionStore {
}
}
public void archiveSessions(final RecipientId recipientId) {
public void archiveSessions(final ServiceId serviceId) {
final var sql = (
"""
SELECT s.recipient_id, s.device_id, s.record
SELECT s.uuid, s.device_id, s.record
FROM %s AS s
WHERE s.account_id_type = ? AND s.recipient_id = ?
WHERE s.account_id_type = ? AND s.uuid = ?
"""
).formatted(TABLE_SESSION);
try (final var connection = database.getConnection()) {
@ -280,7 +271,7 @@ public class SessionStore implements SignalServiceSessionStore {
final List<Pair<Key, SessionRecord>> records;
try (final var statement = connection.prepareStatement(sql)) {
statement.setInt(1, accountIdType);
statement.setLong(2, recipientId.id());
statement.setBytes(2, serviceId.toByteArray());
records = Utils.executeQueryForStream(statement,
res -> new Pair<>(getKeyFromResultSet(res), getSessionRecordFromResultSet(res))).toList();
}
@ -294,35 +285,6 @@ public class SessionStore implements SignalServiceSessionStore {
}
}
public void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
try (final var connection = database.getConnection()) {
connection.setAutoCommit(false);
synchronized (cachedSessions) {
cachedSessions.clear();
}
final var sql = """
UPDATE OR IGNORE %s
SET recipient_id = ?
WHERE account_id_type = ? AND recipient_id = ?
""".formatted(TABLE_SESSION);
try (final var statement = connection.prepareStatement(sql)) {
statement.setLong(1, recipientId.id());
statement.setInt(2, accountIdType);
statement.setLong(3, toBeMergedRecipientId.id());
final var rows = statement.executeUpdate();
if (rows > 0) {
logger.debug("Reassigned {} sessions of to be merged recipient.", rows);
}
}
// Delete all conflicting sessions now
deleteAllSessions(connection, toBeMergedRecipientId);
connection.commit();
} catch (SQLException e) {
throw new RuntimeException("Failed update session store", e);
}
}
void addLegacySessions(final Collection<Pair<Key, SessionRecord>> sessions) {
logger.debug("Migrating legacy sessions to database");
long start = System.nanoTime();
@ -339,8 +301,8 @@ public class SessionStore implements SignalServiceSessionStore {
}
private Key getKey(final SignalProtocolAddress address) {
final var recipientId = resolver.resolveRecipient(address.getName());
return new Key(recipientId, address.getDeviceId());
final var serviceId = ServiceId.parseOrThrow(address.getName());
return new Key(serviceId, address.getDeviceId());
}
private SessionRecord loadSession(Connection connection, final Key key) throws SQLException {
@ -354,21 +316,21 @@ public class SessionStore implements SignalServiceSessionStore {
"""
SELECT s.record
FROM %s AS s
WHERE s.account_id_type = ? AND s.recipient_id = ? AND s.device_id = ?
WHERE s.account_id_type = ? AND s.uuid = ? AND s.device_id = ?
"""
).formatted(TABLE_SESSION);
try (final var statement = connection.prepareStatement(sql)) {
statement.setInt(1, accountIdType);
statement.setLong(2, key.recipientId().id());
statement.setBytes(2, key.serviceId().toByteArray());
statement.setInt(3, key.deviceId());
return Utils.executeQueryForOptional(statement, this::getSessionRecordFromResultSet).orElse(null);
}
}
private Key getKeyFromResultSet(ResultSet resultSet) throws SQLException {
final var recipientId = resultSet.getLong("recipient_id");
final var serviceId = ServiceId.parseOrThrow(resultSet.getBytes("uuid"));
final var deviceId = resultSet.getInt("device_id");
return new Key(recipientIdCreator.create(recipientId), deviceId);
return new Key(serviceId, deviceId);
}
private SessionRecord getSessionRecordFromResultSet(ResultSet resultSet) throws SQLException {
@ -389,19 +351,19 @@ public class SessionStore implements SignalServiceSessionStore {
}
final var sql = """
INSERT OR REPLACE INTO %s (account_id_type, recipient_id, device_id, record)
INSERT OR REPLACE INTO %s (account_id_type, uuid, device_id, record)
VALUES (?, ?, ?, ?)
""".formatted(TABLE_SESSION);
try (final var statement = connection.prepareStatement(sql)) {
statement.setInt(1, accountIdType);
statement.setLong(2, key.recipientId().id());
statement.setBytes(2, key.serviceId().toByteArray());
statement.setInt(3, key.deviceId());
statement.setBytes(4, session.serialize());
statement.executeUpdate();
}
}
private void deleteAllSessions(final Connection connection, final RecipientId recipientId) throws SQLException {
private void deleteAllSessions(final Connection connection, final ServiceId serviceId) throws SQLException {
synchronized (cachedSessions) {
cachedSessions.clear();
}
@ -409,12 +371,12 @@ public class SessionStore implements SignalServiceSessionStore {
final var sql = (
"""
DELETE FROM %s AS s
WHERE s.account_id_type = ? AND s.recipient_id = ?
WHERE s.account_id_type = ? AND s.uuid = ?
"""
).formatted(TABLE_SESSION);
try (final var statement = connection.prepareStatement(sql)) {
statement.setInt(1, accountIdType);
statement.setLong(2, recipientId.id());
statement.setBytes(2, serviceId.toByteArray());
statement.executeUpdate();
}
}
@ -427,12 +389,12 @@ public class SessionStore implements SignalServiceSessionStore {
final var sql = (
"""
DELETE FROM %s AS s
WHERE s.account_id_type = ? AND s.recipient_id = ? AND s.device_id = ?
WHERE s.account_id_type = ? AND s.uuid = ? AND s.device_id = ?
"""
).formatted(TABLE_SESSION);
try (final var statement = connection.prepareStatement(sql)) {
statement.setInt(1, accountIdType);
statement.setLong(2, key.recipientId().id());
statement.setBytes(2, key.serviceId().toByteArray());
statement.setInt(3, key.deviceId());
statement.executeUpdate();
}
@ -444,5 +406,5 @@ public class SessionStore implements SignalServiceSessionStore {
&& record.getSessionVersion() == CiphertextMessage.CURRENT_VERSION;
}
record Key(RecipientId recipientId, int deviceId) {}
record Key(ServiceId serviceId, int deviceId) {}
}

View file

@ -1,12 +1,12 @@
package org.asamk.signal.manager.util;
import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.fingerprint.Fingerprint;
import org.signal.libsignal.protocol.fingerprint.NumericFingerprintGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.StreamDetails;
import java.io.BufferedInputStream;
@ -73,28 +73,27 @@ public class Utils {
public static Fingerprint computeSafetyNumber(
boolean isUuidCapable,
SignalServiceAddress ownAddress,
RecipientAddress ownAddress,
IdentityKey ownIdentityKey,
SignalServiceAddress theirAddress,
RecipientAddress theirAddress,
IdentityKey theirIdentityKey
) {
int version;
byte[] ownId;
byte[] theirId;
if (isUuidCapable) {
if (!isUuidCapable && ownAddress.number().isPresent() && theirAddress.number().isPresent()) {
// Version 1: E164 user
version = 1;
ownId = ownAddress.number().get().getBytes();
theirId = theirAddress.number().get().getBytes();
} else if (isUuidCapable && theirAddress.uuid().isPresent()) {
// Version 2: UUID user
version = 2;
ownId = ownAddress.getServiceId().toByteArray();
theirId = theirAddress.getServiceId().toByteArray();
} else {
// Version 1: E164 user
version = 1;
if (ownAddress.getNumber().isEmpty() || theirAddress.getNumber().isEmpty()) {
return null;
}
ownId = ownAddress.getNumber().get().getBytes();
theirId = theirAddress.getNumber().get().getBytes();
return null;
}
return new NumericFingerprintGenerator(5200).createFor(version,