From: AsamK Date: Tue, 23 Aug 2022 12:17:14 +0000 (+0200) Subject: Key tables on serviceId instead of recipientId X-Git-Tag: v0.11.0~11 X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/280d8d7f10e7d6e6068545c4d5fce0bbbb96a2ea?ds=sidebyside Key tables on serviceId instead of recipientId --- diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index 6c52d59e..386e6fbe 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -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 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)); } diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/RenewSessionAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/RenewSessionAction.java index b2de2cdd..2718bc26 100644 --- a/lib/src/main/java/org/asamk/signal/manager/actions/RenewSessionAction.java +++ b/lib/src/main/java/org/asamk/signal/manager/actions/RenewSessionAction.java @@ -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); } diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/SendRetryMessageRequestAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/SendRetryMessageRequestAction.java index 491dbb8f..7c0562cc 100644 --- a/lib/src/main/java/org/asamk/signal/manager/actions/SendRetryMessageRequestAction.java +++ b/lib/src/main/java/org/asamk/signal/manager/actions/SendRetryMessageRequestAction.java @@ -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 = protocolException.getGroupId().isPresent() ? Optional.of(GroupId.unknownVersion( diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java index ce397353..0adeaf60 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java @@ -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 verifier, TrustLevel trustLevel + ServiceId serviceId, Function 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); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index 89ff3e70..fdf8bcd2 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -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(); - 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 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 handleSyncMessage( - final SignalServiceSyncMessage syncMessage, final RecipientId sender, final boolean ignoreAttachments + final SignalServiceSyncMessage syncMessage, final DeviceAddress sender, final boolean ignoreAttachments ) { var actions = new ArrayList(); 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 handleSignalServiceDataMessage( SignalServiceDataMessage message, boolean isSync, - RecipientId source, - RecipientId destination, + DeviceAddress source, + DeviceAddress destination, boolean ignoreAttachments ) { var actions = new ArrayList(); @@ -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 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) {} } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java index 45ff107a..3379c905 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -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()); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java index 717db812..95e84f08 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -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 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()); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java index bc785099..3037d7c3 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java @@ -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"); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java index 73662d22..60887619 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java @@ -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())); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/AccountDatabase.java b/lib/src/main/java/org/asamk/signal/manager/storage/AccountDatabase.java index c077708b..5093b9be 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/AccountDatabase.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/AccountDatabase.java @@ -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; + """); + } + } } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index 78601e07..72f6f3d5 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -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 { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityInfo.java b/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityInfo.java index 571f564d..84f7b06f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityInfo.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityInfo.java @@ -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() { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java index 3971532f..9ae8dda8 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java @@ -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 identityChanges = PublishSubject.create(); + private final PublishSubject 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 getIdentityChanges() { + public Observable 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; diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/identities/LegacyIdentityKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/identities/LegacyIdentityKeyStore.java index 669602e5..aa800a04 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/identities/LegacyIdentityKeyStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/identities/LegacyIdentityKeyStore.java @@ -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 getIdentities(final File identitiesPath, final RecipientResolver resolver) { + private static List 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; diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/identities/SignalIdentityKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/identities/SignalIdentityKeyStore.java index c15b858e..66dfec4e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/identities/SignalIdentityKeyStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/identities/SignalIdentityKeyStore.java @@ -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 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); - } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java index 0ab8c8f5..c5f0ccbc 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java @@ -35,6 +35,10 @@ public record RecipientAddress(Optional uuid, Optional 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, Optional number) { } public SignalServiceAddress toSignalServiceAddress() { - return new SignalServiceAddress(ServiceId.from(uuid.orElse(UNKNOWN_UUID)), number); + return new SignalServiceAddress(getServiceId(), number); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/sendLog/MessageSendLogStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/sendLog/MessageSendLogStore.java index 54632395..c7e6cdb1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/sendLog/MessageSendLogStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/sendLog/MessageSendLogStore.java @@ -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 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 sentTimestamps, RecipientId recipientId, int deviceId) { + public void deleteEntriesForRecipient(List 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, 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 deviceIds) {} + private record RecipientDevices(ServiceId serviceId, List deviceIds) {} } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/LegacySenderKeyRecordStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/LegacySenderKeyRecordStore.java index ea823cb2..f5a1fc53 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/LegacySenderKeyRecordStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/LegacySenderKeyRecordStore.java @@ -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) {} } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/LegacySenderKeySharedStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/LegacySenderKeySharedStore.java index 87c298f4..47465b3e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/LegacySenderKeySharedStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/LegacySenderKeySharedStore.java @@ -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) { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java index 9b612aac..5e42a924 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java @@ -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) {} } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeySharedStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeySharedStore.java index 2eb7c4bc..112c358c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeySharedStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeySharedStore.java @@ -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 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 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 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> 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) {} } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyStore.java index 6a3da47e..d60249b6 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyStore.java @@ -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()); - } - - public long getCreateTimeForOurKey(RecipientId selfRecipientId, int deviceId, DistributionId distributionId) { - return senderKeyRecordStore.getCreateTimeForKey(selfRecipientId, deviceId, distributionId.asUuid()); + senderKeyRecordStore.deleteSenderKey(selfServiceId, 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> senderKeys) { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/sessions/LegacySessionStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/sessions/LegacySessionStore.java index d0646ec1..1682b714 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/sessions/LegacySessionStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/sessions/LegacySessionStore.java @@ -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) {} } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java index ad0e198e..4bac3bd5 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java @@ -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 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 getAllAddressesWithActiveSessions(final List 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> 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> 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) {} } diff --git a/lib/src/main/java/org/asamk/signal/manager/util/Utils.java b/lib/src/main/java/org/asamk/signal/manager/util/Utils.java index 2226c92e..97adb69d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/Utils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/Utils.java @@ -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,