From: AsamK Date: Sat, 23 Nov 2024 21:29:01 +0000 (+0100) Subject: Update libsignal-service X-Git-Tag: v0.13.10~9 X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/ff6cb5262ad3d987bd113fac18afd8722fca38a4?ds=sidebyside Update libsignal-service Support for storage encryption v2 and account entropy pool Fixes #1632 --- diff --git a/graalvm-config-dir/reflect-config.json b/graalvm-config-dir/reflect-config.json index abe02ac4..230ede2f 100644 --- a/graalvm-config-dir/reflect-config.json +++ b/graalvm-config-dir/reflect-config.json @@ -124,6 +124,13 @@ "name":"com.fasterxml.jackson.databind.ext.Java7SupportImpl", "methods":[{"name":"","parameterTypes":[] }] }, +{ + "name":"com.squareup.wire.Message", + "methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }] +}, +{ + "name":"com.squareup.wire.ProtoAdapter" +}, { "name":"com.squareup.wire.internal.ImmutableList", "allDeclaredFields":true, @@ -209,9 +216,14 @@ { "name":"java.io.FilePermission" }, +{ + "name":"java.io.OutputStream" +}, { "name":"java.io.Serializable", - "allDeclaredMethods":true + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredClasses":true }, { "name":"java.lang.Boolean", @@ -577,6 +589,9 @@ { "name":"kotlin.String" }, +{ + "name":"kotlin.Unit" +}, { "name":"kotlin.collections.AbstractCollection", "allDeclaredFields":true, @@ -629,6 +644,9 @@ { "name":"long[]" }, +{ + "name":"okio.BufferedSink" +}, { "name":"okio.ByteString" }, @@ -1025,7 +1043,7 @@ "allDeclaredFields":true, "allDeclaredMethods":true, "allDeclaredConstructors":true, - "methods":[{"name":"groupId","parameterTypes":[] }, {"name":"type","parameterTypes":[] }] + "methods":[{"name":"groupId","parameterTypes":[] }, {"name":"groupName","parameterTypes":[] }, {"name":"revision","parameterTypes":[] }, {"name":"type","parameterTypes":[] }] }, { "name":"org.asamk.signal.json.JsonMention", @@ -1247,7 +1265,7 @@ "allDeclaredFields":true, "queryAllDeclaredMethods":true, "queryAllDeclaredConstructors":true, - "methods":[{"name":"","parameterTypes":["int","long","java.lang.String","boolean","java.lang.String","java.lang.String","java.lang.String","int","boolean","java.lang.String","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String"] }, {"name":"","parameterTypes":["int","java.lang.String","boolean","java.lang.String","java.lang.String","java.lang.String","int","boolean","java.lang.String","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","java.lang.String","java.lang.String","java.lang.String","java.lang.String"] }, {"name":"","parameterTypes":["int","java.lang.String","boolean","java.lang.String","java.lang.String","java.lang.String","int","boolean","java.lang.String","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String"] }, {"name":"aciAccountData","parameterTypes":[] }, {"name":"deviceId","parameterTypes":[] }, {"name":"encryptedDeviceName","parameterTypes":[] }, {"name":"isMultiDevice","parameterTypes":[] }, {"name":"number","parameterTypes":[] }, {"name":"password","parameterTypes":[] }, {"name":"pinMasterKey","parameterTypes":[] }, {"name":"pniAccountData","parameterTypes":[] }, {"name":"profileKey","parameterTypes":[] }, {"name":"registered","parameterTypes":[] }, {"name":"registrationLockPin","parameterTypes":[] }, {"name":"serviceEnvironment","parameterTypes":[] }, {"name":"storageKey","parameterTypes":[] }, {"name":"timestamp","parameterTypes":[] }, {"name":"username","parameterTypes":[] }, {"name":"usernameLinkEntropy","parameterTypes":[] }, {"name":"usernameLinkServerId","parameterTypes":[] }, {"name":"version","parameterTypes":[] }] + "methods":[{"name":"","parameterTypes":["int","long","java.lang.String","boolean","java.lang.String","java.lang.String","java.lang.String","int","boolean","java.lang.String","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String"] }, {"name":"","parameterTypes":["int","long","java.lang.String","boolean","java.lang.String","java.lang.String","java.lang.String","int","boolean","java.lang.String","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String"] }, {"name":"","parameterTypes":["int","java.lang.String","boolean","java.lang.String","java.lang.String","java.lang.String","int","boolean","java.lang.String","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","java.lang.String","java.lang.String","java.lang.String","java.lang.String"] }, {"name":"","parameterTypes":["int","java.lang.String","boolean","java.lang.String","java.lang.String","java.lang.String","int","boolean","java.lang.String","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String","java.lang.String"] }, {"name":"accountEntropyPool","parameterTypes":[] }, {"name":"aciAccountData","parameterTypes":[] }, {"name":"deviceId","parameterTypes":[] }, {"name":"encryptedDeviceName","parameterTypes":[] }, {"name":"isMultiDevice","parameterTypes":[] }, {"name":"mediaRootBackupKey","parameterTypes":[] }, {"name":"number","parameterTypes":[] }, {"name":"password","parameterTypes":[] }, {"name":"pinMasterKey","parameterTypes":[] }, {"name":"pniAccountData","parameterTypes":[] }, {"name":"profileKey","parameterTypes":[] }, {"name":"registered","parameterTypes":[] }, {"name":"registrationLockPin","parameterTypes":[] }, {"name":"serviceEnvironment","parameterTypes":[] }, {"name":"storageKey","parameterTypes":[] }, {"name":"timestamp","parameterTypes":[] }, {"name":"username","parameterTypes":[] }, {"name":"usernameLinkEntropy","parameterTypes":[] }, {"name":"usernameLinkServerId","parameterTypes":[] }, {"name":"version","parameterTypes":[] }] }, { "name":"org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData", @@ -2257,7 +2275,7 @@ "allDeclaredFields":true, "allDeclaredMethods":true, "allDeclaredConstructors":true, - "methods":[{"name":"getAnnouncementGroup","parameterTypes":[] }, {"name":"getChangeNumber","parameterTypes":[] }, {"name":"getDeleteSync","parameterTypes":[] }, {"name":"getGiftBadges","parameterTypes":[] }, {"name":"getPaymentActivation","parameterTypes":[] }, {"name":"getPni","parameterTypes":[] }, {"name":"getSenderKey","parameterTypes":[] }, {"name":"getStorage","parameterTypes":[] }, {"name":"getStories","parameterTypes":[] }, {"name":"getVersionedExpirationTimer","parameterTypes":[] }] + "methods":[{"name":"getAnnouncementGroup","parameterTypes":[] }, {"name":"getChangeNumber","parameterTypes":[] }, {"name":"getDeleteSync","parameterTypes":[] }, {"name":"getGiftBadges","parameterTypes":[] }, {"name":"getPaymentActivation","parameterTypes":[] }, {"name":"getPni","parameterTypes":[] }, {"name":"getSenderKey","parameterTypes":[] }, {"name":"getStorage","parameterTypes":[] }, {"name":"getStorageServiceEncryptionV2","parameterTypes":[] }, {"name":"getStories","parameterTypes":[] }, {"name":"getVersionedExpirationTimer","parameterTypes":[] }] }, { "name":"org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest", @@ -2284,6 +2302,13 @@ { "name":"org.whispersystems.signalservice.api.groupsv2.TemporalCredential[]" }, +{ + "name":"org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"","parameterTypes":["java.lang.String","java.lang.String","int","kotlin.jvm.internal.DefaultConstructorMarker"] }] +}, { "name":"org.whispersystems.signalservice.api.messages.calls.HangupMessage", "allDeclaredFields":true, @@ -2891,7 +2916,17 @@ }, { "name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord", - "allDeclaredFields":true + "allDeclaredFields":true, + "methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }] +}, +{ + "name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$Builder" +}, +{ + "name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$Companion" +}, +{ + "name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$PhoneNumberSharingMode" }, { "name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$PinnedConversation", @@ -2907,7 +2942,17 @@ }, { "name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord", - "allDeclaredFields":true + "allDeclaredFields":true, + "methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }] +}, +{ + "name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord$Builder" +}, +{ + "name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord$Companion" +}, +{ + "name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord$IdentityState" }, { "name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord$Name", @@ -2915,11 +2960,28 @@ }, { "name":"org.whispersystems.signalservice.internal.storage.protos.GroupV1Record", - "allDeclaredFields":true + "allDeclaredFields":true, + "methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }] +}, +{ + "name":"org.whispersystems.signalservice.internal.storage.protos.GroupV1Record$Builder" +}, +{ + "name":"org.whispersystems.signalservice.internal.storage.protos.GroupV1Record$Companion" }, { "name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record", - "allDeclaredFields":true + "allDeclaredFields":true, + "methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }] +}, +{ + "name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record$Builder" +}, +{ + "name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record$Companion" +}, +{ + "name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record$StorySendMode" }, { "name":"org.whispersystems.signalservice.internal.storage.protos.ManifestRecord", @@ -2929,6 +2991,9 @@ "name":"org.whispersystems.signalservice.internal.storage.protos.ManifestRecord$Identifier", "fields":[{"name":"raw_"}, {"name":"type_"}] }, +{ + "name":"org.whispersystems.signalservice.internal.storage.protos.OptionalBool" +}, { "name":"org.whispersystems.signalservice.internal.storage.protos.Payments", "allDeclaredFields":true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d7d56d71..e52a4ae7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" } logback = "ch.qos.logback:logback-classic:1.5.12" -signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_110" +signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_111" sqlite = "org.xerial:sqlite-jdbc:3.47.0.0" hikari = "com.zaxxer:HikariCP:6.2.1" junit-jupiter = "org.junit.jupiter:junit-jupiter:5.11.3" diff --git a/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java b/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java index 5598d438..d8438f5d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java @@ -611,11 +611,12 @@ public record MessageEnvelope( RecipientResolver recipientResolver, RecipientAddressResolver addressResolver ) { - return new Blocked(blockedListMessage.getAddresses() - .stream() - .map(d -> addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(d)) - .toApiRecipientAddress()) - .toList(), blockedListMessage.getGroupIds().stream().map(GroupId::unknownVersion).toList()); + return new Blocked(blockedListMessage.individuals.stream() + .map(d -> new RecipientAddress(d.getAci() == null ? null : d.getAci().toString(), + null, + d.getE164(), + null)) + .toList(), blockedListMessage.groupIds.stream().map(GroupId::unknownVersion).toList()); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/api/Profile.java b/lib/src/main/java/org/asamk/signal/manager/api/Profile.java index 5b1ecf3a..acbaac11 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/Profile.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/Profile.java @@ -161,7 +161,8 @@ public class Profile { } public enum Capability { - storage; + storage, + storageServiceEncryptionV2Capability; public static Capability valueOfOrNull(String value) { try { diff --git a/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java index f7859dae..8e844392 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java @@ -51,7 +51,7 @@ class LiveConfig { private static final byte[] backupServerPublicParams = Base64.getDecoder() .decode("AJwNSU55fsFCbgaxGRD11wO1juAs8Yr5GF8FPlGzzvdJJIKH5/4CC7ZJSOe3yL2vturVaRU2Cx0n751Vt8wkj1bozK3CBV1UokxV09GWf+hdVImLGjXGYLLhnI1J2TWEe7iWHyb553EEnRb5oxr9n3lUbNAJuRmFM7hrr0Al0F0wrDD4S8lo2mGaXe0MJCOM166F8oYRQqpFeEHfiLnxA1O8ZLh7vMdv4g9jI5phpRBTsJ5IjiJrWeP0zdIGHEssUeprDZ9OUJ14m0v61eYJMKsf59Bn+mAT2a7YfB+Don9O"); - private static Environment LIBSIGNAL_NET_ENV = Environment.PRODUCTION; + private static final Environment LIBSIGNAL_NET_ENV = Environment.PRODUCTION; static SignalServiceConfiguration createDefaultServiceConfiguration( final List interceptors @@ -71,7 +71,8 @@ class LiveConfig { proxy, zkGroupServerPublicParams, genericServerPublicParams, - backupServerPublicParams); + backupServerPublicParams, + false); } static ECPublicKey getUnidentifiedSenderTrustRoot() { diff --git a/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java index 482e0f60..24365cfe 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java @@ -29,7 +29,8 @@ public class ServiceConfig { public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) { final var deleteSync = !isPrimaryDevice; - return new AccountAttributes.Capabilities(true, deleteSync, true); + final var storageEncryptionV2 = !isPrimaryDevice; + return new AccountAttributes.Capabilities(true, deleteSync, true, storageEncryptionV2); } public static ServiceEnvironmentConfig getServiceEnvironmentConfig( diff --git a/lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java index a3212c07..741afa16 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java @@ -51,7 +51,7 @@ class StagingConfig { private static final byte[] backupServerPublicParams = Base64.getDecoder() .decode("AHYrGb9IfugAAJiPKp+mdXUx+OL9zBolPYHYQz6GI1gWjpEu5me3zVNSvmYY4zWboZHif+HG1sDHSuvwFd0QszSwuSF4X4kRP3fJREdTZ5MCR0n55zUppTwfHRW2S4sdQ0JGz7YDQIJCufYSKh0pGNEHL6hv79Agrdnr4momr3oXdnkpVBIp3HWAQ6IbXQVSG18X36GaicI1vdT0UFmTwU2KTneluC2eyL9c5ff8PcmiS+YcLzh0OKYQXB5ZfQ06d6DiINvDQLy75zcfUOniLAj0lGJiHxGczin/RXisKSR8"); - private static Network.Environment LIBSIGNAL_NET_ENV = Network.Environment.STAGING; + private static final Network.Environment LIBSIGNAL_NET_ENV = Network.Environment.STAGING; static SignalServiceConfiguration createDefaultServiceConfiguration( final List interceptors @@ -71,7 +71,8 @@ class StagingConfig { proxy, zkGroupServerPublicParams, genericServerPublicParams, - backupServerPublicParams); + backupServerPublicParams, + false); } static ECPublicKey getUnidentifiedSenderTrustRoot() { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java index d5e8581e..3eb2051d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java @@ -3,7 +3,6 @@ package org.asamk.signal.manager.helper; import org.asamk.signal.manager.api.CaptchaRequiredException; import org.asamk.signal.manager.api.DeviceLinkUrl; import org.asamk.signal.manager.api.IncorrectPinException; -import org.asamk.signal.manager.api.InvalidDeviceLinkException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.RateLimitException; @@ -27,6 +26,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; +import org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse; import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.ServiceId.PNI; import org.whispersystems.signalservice.api.push.ServiceIdType; @@ -56,6 +56,7 @@ import java.util.concurrent.TimeUnit; import okio.ByteString; import static org.asamk.signal.manager.config.ServiceConfig.PREKEY_MAXIMUM_ID; +import static org.asamk.signal.manager.util.Utils.handleResponseException; import static org.whispersystems.signalservice.internal.util.Util.isEmpty; public class AccountHelper { @@ -289,12 +290,11 @@ public class AccountHelper { (sessionId1, verificationCode1, registrationLock) -> { final var registrationApi = dependencies.getRegistrationApi(); try { - Utils.handleResponseException(registrationApi.verifyAccount(sessionId1, verificationCode1)); + handleResponseException(registrationApi.verifyAccount(sessionId1, verificationCode1)); } catch (AlreadyVerifiedException e) { // Already verified so can continue changing number } - return Utils.handleResponseException(registrationApi.changeNumber(new ChangePhoneNumberRequest( - sessionId1, + return handleResponseException(registrationApi.changeNumber(new ChangePhoneNumberRequest(sessionId1, null, newNumber, registrationLock, @@ -482,26 +482,28 @@ public class AccountHelper { dependencies.getAccountManager().setAccountAttributes(account.getAccountAttributes(null)); } - public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, InvalidDeviceLinkException, org.asamk.signal.manager.api.DeviceLimitExceededException { - String verificationCode; + public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, org.asamk.signal.manager.api.DeviceLimitExceededException { + final var linkDeviceApi = dependencies.getLinkDeviceApi(); + final LinkedDeviceVerificationCodeResponse verificationCode; try { - verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode(); + verificationCode = handleResponseException(linkDeviceApi.getDeviceVerificationCode()); } catch (DeviceLimitExceededException e) { throw new org.asamk.signal.manager.api.DeviceLimitExceededException("Too many linked devices", e); } - try { - dependencies.getAccountManager() - .addDevice(deviceLinkInfo.deviceIdentifier(), - deviceLinkInfo.deviceKey(), - account.getAciIdentityKeyPair(), - account.getPniIdentityKeyPair(), - account.getProfileKey(), - account.getOrCreatePinMasterKey(), - verificationCode); - } catch (InvalidKeyException e) { - throw new InvalidDeviceLinkException("Invalid device link", e); - } + handleResponseException(dependencies.getLinkDeviceApi() + .linkDevice(account.getNumber(), + account.getAci(), + account.getPni(), + deviceLinkInfo.deviceIdentifier(), + deviceLinkInfo.deviceKey(), + account.getAciIdentityKeyPair(), + account.getPniIdentityKeyPair(), + account.getProfileKey(), + account.getOrCreatePinMasterKey(), + account.getOrCreateMediaRootBackupKey(), + verificationCode.getVerificationCode(), + null)); account.setMultiDevice(true); context.getJobExecutor().enqueueJob(new SyncStorageJob()); } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java index 05bc876a..a3330d5c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java @@ -82,7 +82,7 @@ class GroupV2Helper { final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams); return dependencies.getGroupsV2Api().getGroup(groupSecretParams, groupsV2AuthorizationString); } catch (NonSuccessfulResponseCodeException e) { - if (e.getCode() == 403) { + if (e.code == 403) { throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null); } logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage()); @@ -119,7 +119,7 @@ class GroupV2Helper { false, sendEndorsementsExpirationMs); } catch (NonSuccessfulResponseCodeException e) { - if (e.getCode() == 403) { + if (e.code == 403) { throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null); } logger.warn("Failed to retrieve Group V2 history, ignoring: {}", e.getMessage()); 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 376a671b..5deffb3e 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 @@ -31,6 +31,7 @@ import org.asamk.signal.manager.internal.SignalDependencies; import org.asamk.signal.manager.jobs.RetrieveStickerPackJob; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.groups.GroupInfoV1; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.stickers.StickerPack; import org.signal.libsignal.metadata.ProtocolInvalidKeyException; @@ -525,12 +526,12 @@ public final class IncomingMessageHandler { } if (syncMessage.getBlockedList().isPresent()) { final var blockedListMessage = syncMessage.getBlockedList().get(); - for (var address : blockedListMessage.getAddresses()) { - context.getContactHelper() - .setContactBlocked(account.getRecipientResolver().resolveRecipient(address), true); + for (var individual : blockedListMessage.individuals) { + final var address = new RecipientAddress(individual.getAci(), individual.getE164()); + final var recipientId = account.getRecipientResolver().resolveRecipient(address); + context.getContactHelper().setContactBlocked(recipientId, true); } - for (var groupId : blockedListMessage.getGroupIds() - .stream() + for (var groupId : blockedListMessage.groupIds.stream() .map(GroupId::unknownVersion) .collect(Collectors.toSet())) { try { @@ -585,14 +586,22 @@ public final class IncomingMessageHandler { } if (syncMessage.getKeys().isPresent()) { final var keysMessage = syncMessage.getKeys().get(); - if (keysMessage.getStorageService().isPresent()) { - final var storageKey = keysMessage.getStorageService().get(); + if (keysMessage.getAccountEntropyPool() != null) { + final var aep = keysMessage.getAccountEntropyPool(); + account.setAccountEntropyPool(aep); + actions.add(SyncStorageDataAction.create()); + } else if (keysMessage.getMaster() != null) { + final var masterKey = keysMessage.getMaster(); + account.setMasterKey(masterKey); + actions.add(SyncStorageDataAction.create()); + } else if (keysMessage.getStorageService() != null) { + final var storageKey = keysMessage.getStorageService(); account.setStorageKey(storageKey); actions.add(SyncStorageDataAction.create()); } - if (keysMessage.getMaster().isPresent()) { - final var masterKey = keysMessage.getMaster().get(); - account.setMasterKey(masterKey); + if (keysMessage.getMediaRootBackupKey() != null) { + final var mrb = keysMessage.getMediaRootBackupKey(); + account.setMediaRootBackupKey(mrb); actions.add(SyncStorageDataAction.create()); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/PreKeyHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/PreKeyHelper.java index 339425c1..b17a9206 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/PreKeyHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/PreKeyHelper.java @@ -172,7 +172,7 @@ public class PreKeyHelper { // This can happen when the primary device has changed phone number logger.warn("Failed to updated pre keys: {}", e.getMessage()); } catch (NonSuccessfulResponseCodeException e) { - if (serviceIdType != ServiceIdType.PNI || e.getCode() != 422) { + if (serviceIdType != ServiceIdType.PNI || e.code != 422) { throw e; } logger.warn("Failed to set PNI pre keys, ignoring for now. Account needs to be reregistered to fix this."); 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 184eac6b..0905bb30 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 @@ -336,13 +336,6 @@ public final class ProfileHelper { final var profile = account.getProfileStore().getProfile(recipientId); - if (recipientId.equals(account.getSelfRecipientId())) { - final var isUnrestricted = encryptedProfile.isUnrestrictedUnidentifiedAccess(); - if (account.isUnrestrictedUnidentifiedAccess() != isUnrestricted) { - account.setUnrestrictedUnidentifiedAccess(isUnrestricted); - } - } - Profile newProfile = null; if (profileKey.isPresent()) { logger.trace("Decrypting profile"); @@ -358,6 +351,18 @@ public final class ProfileHelper { .build(); } + if (recipientId.equals(account.getSelfRecipientId())) { + final var isUnrestricted = encryptedProfile.isUnrestrictedUnidentifiedAccess(); + if (account.isUnrestrictedUnidentifiedAccess() != isUnrestricted) { + account.setUnrestrictedUnidentifiedAccess(isUnrestricted); + } + if (account.isPrimaryDevice() && profile != null && newProfile.getCapabilities() + .contains(Profile.Capability.storageServiceEncryptionV2Capability) && !profile.getCapabilities() + .contains(Profile.Capability.storageServiceEncryptionV2Capability)) { + context.getJobExecutor().enqueueJob(new SyncStorageJob(true)); + } + } + try { logger.trace("Storing identity"); final var identityKey = new IdentityKey(Base64.getDecoder().decode(encryptedProfile.getIdentityKey())); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java index 6225b982..5da66b7e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java @@ -239,7 +239,6 @@ public class RecipientHelper { newNumbers, account.getRecipientStore().getServiceIdToProfileKeyMap(), token, - dependencies.getServiceEnvironmentConfig().cdsiMrenclave(), null, dependencies.getLibSignalNetwork(), newToken -> { 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 8c6b3c52..740c0b5e 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 @@ -2,6 +2,7 @@ package org.asamk.signal.manager.helper; import org.asamk.signal.manager.api.GroupIdV1; import org.asamk.signal.manager.api.GroupIdV2; +import org.asamk.signal.manager.api.Profile; import org.asamk.signal.manager.internal.SignalDependencies; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.recipients.RecipientId; @@ -17,11 +18,17 @@ import org.signal.core.util.SetUtil; import org.signal.libsignal.protocol.InvalidKeyException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.storage.RecordIkm; import org.whispersystems.signalservice.api.storage.SignalStorageManifest; import org.whispersystems.signalservice.api.storage.SignalStorageRecord; import org.whispersystems.signalservice.api.storage.StorageId; import org.whispersystems.signalservice.api.storage.StorageKey; +import org.whispersystems.signalservice.api.storage.StorageRecordConvertersKt; +import org.whispersystems.signalservice.api.storage.StorageServiceRepository; +import org.whispersystems.signalservice.api.storage.StorageServiceRepository.ManifestIfDifferentVersionResult; +import org.whispersystems.signalservice.api.storage.StorageServiceRepository.WriteStorageRecordsResult; import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord; +import org.whispersystems.signalservice.internal.storage.protos.StorageRecord; import java.io.IOException; import java.sql.Connection; @@ -32,9 +39,10 @@ import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.stream.Collectors; +import static org.asamk.signal.manager.util.Utils.handleResponseException; + public class StorageHelper { private static final Logger logger = LoggerFactory.getLogger(StorageHelper.class); @@ -54,7 +62,7 @@ public class StorageHelper { } public void syncDataWithStorage() throws IOException { - final var storageKey = account.getOrCreateStorageKey(); + var storageKey = account.getOrCreateStorageKey(); if (storageKey == null) { if (!account.isPrimaryDevice()) { logger.debug("Storage key unknown, requesting from primary device."); @@ -65,52 +73,76 @@ public class StorageHelper { logger.trace("Reading manifest from remote storage"); final var localManifestVersion = account.getStorageManifestVersion(); - final var localManifest = account.getStorageManifest().orElse(SignalStorageManifest.EMPTY); - SignalStorageManifest remoteManifest; - try { - remoteManifest = dependencies.getAccountManager() - .getStorageManifestIfDifferentVersion(storageKey, localManifestVersion) - .orElse(localManifest); - } catch (InvalidKeyException e) { - logger.warn("Manifest couldn't be decrypted."); - if (account.isPrimaryDevice()) { - try { - forcePushToStorage(storageKey); - } catch (RetryLaterException rle) { - // TODO retry later - return; + final var localManifest = account.getStorageManifest().orElse(SignalStorageManifest.Companion.getEMPTY()); + final var storageServiceRepository = dependencies.getStorageServiceRepository(); + final var result = storageServiceRepository.getStorageManifestIfDifferentVersion(storageKey, + localManifestVersion); + + var needsForcePush = false; + final var remoteManifest = switch (result) { + case ManifestIfDifferentVersionResult.DifferentVersion diff -> { + final var manifest = diff.getManifest(); + storeManifestLocally(manifest); + yield manifest; + } + case ManifestIfDifferentVersionResult.DecryptionError ignore -> { + logger.warn("Manifest couldn't be decrypted."); + if (account.isPrimaryDevice()) { + needsForcePush = true; + } else { + context.getSyncHelper().requestSyncKeys(); } + yield null; } - return; - } + case ManifestIfDifferentVersionResult.SameVersion ignored -> localManifest; + case ManifestIfDifferentVersionResult.NetworkError e -> throw e.getException(); + case ManifestIfDifferentVersionResult.StatusCodeError e -> throw e.getException(); + default -> throw new RuntimeException("Unhandled ManifestIfDifferentVersionResult type"); + }; - logger.trace("Manifest versions: local {}, remote {}", localManifestVersion, remoteManifest.getVersion()); + if (remoteManifest != null) { + logger.trace("Manifest versions: local {}, remote {}", localManifestVersion, remoteManifest.version); - var needsForcePush = false; - if (remoteManifest.getVersion() > localManifestVersion) { - logger.trace("Remote version was newer, reading records."); - needsForcePush = readDataFromStorage(storageKey, localManifest, remoteManifest); - } else if (remoteManifest.getVersion() < localManifest.getVersion()) { - logger.debug("Remote storage manifest version was older. User might have switched accounts."); - } - logger.trace("Done reading data from remote storage"); + if (remoteManifest.version > localManifestVersion) { + logger.trace("Remote version was newer, reading records."); + needsForcePush = readDataFromStorage(storageKey, localManifest, remoteManifest); + } else if (remoteManifest.version < localManifest.version) { + logger.debug("Remote storage manifest version was older. User might have switched accounts."); + } + logger.trace("Done reading data from remote storage"); - if (localManifest != remoteManifest) { - storeManifestLocally(remoteManifest); + readRecordsWithPreviouslyUnknownTypes(storageKey, remoteManifest); } - readRecordsWithPreviouslyUnknownTypes(storageKey); - logger.trace("Adding missing storageIds to local data"); account.getRecipientStore().setMissingStorageIds(); account.getGroupStore().setMissingStorageIds(); var needsMultiDeviceSync = false; - try { - needsMultiDeviceSync = writeToStorage(storageKey, remoteManifest, needsForcePush); - } catch (RetryLaterException e) { - // TODO retry later - return; + + if (account.needsStorageKeyMigration()) { + logger.debug("Storage needs force push due to new account entropy pool"); + // Set new aep and reset previous master key and storage key + account.setAccountEntropyPool(account.getOrCreateAccountEntropyPool()); + storageKey = account.getOrCreateStorageKey(); + context.getSyncHelper().sendKeysMessage(); + needsForcePush = true; + } else if (remoteManifest == null) { + if (account.isPrimaryDevice()) { + needsForcePush = true; + } + } else if (remoteManifest.recordIkm == null && account.getSelfRecipientProfile() + .getCapabilities() + .contains(Profile.Capability.storageServiceEncryptionV2Capability)) { + logger.debug("The SSRE2 capability is supported, but no recordIkm is set! Force pushing."); + needsForcePush = true; + } else { + try { + needsMultiDeviceSync = writeToStorage(storageKey, remoteManifest, needsForcePush); + } catch (RetryLaterException e) { + // TODO retry later + return; + } } if (needsForcePush) { @@ -131,6 +163,23 @@ public class StorageHelper { logger.debug("Done syncing data with remote storage"); } + public void forcePushToStorage() throws IOException { + if (!account.isPrimaryDevice()) { + return; + } + + final var storageKey = account.getOrCreateStorageKey(); + if (storageKey == null) { + return; + } + + try { + forcePushToStorage(storageKey); + } catch (RetryLaterException e) { + // TODO retry later + } + } + private boolean readDataFromStorage( final StorageKey storageKey, final SignalStorageManifest localManifest, @@ -140,14 +189,14 @@ public class StorageHelper { try (final var connection = account.getAccountDatabase().getConnection()) { connection.setAutoCommit(false); - var idDifference = findIdDifference(remoteManifest.getStorageIds(), localManifest.getStorageIds()); + var idDifference = findIdDifference(remoteManifest.storageIds, localManifest.storageIds); if (idDifference.hasTypeMismatches() && account.isPrimaryDevice()) { logger.debug("Found type mismatches in the ID sets! Scheduling a force push after this sync completes."); needsForcePush = true; } - logger.debug("Pre-Merge ID Difference :: " + idDifference); + logger.debug("Pre-Merge ID Difference :: {}", idDifference); if (!idDifference.localOnlyIds().isEmpty()) { final var updated = account.getRecipientStore() @@ -161,15 +210,15 @@ public class StorageHelper { } if (!idDifference.isEmpty()) { - final var remoteOnlyRecords = getSignalStorageRecords(storageKey, idDifference.remoteOnlyIds()); + final var remoteOnlyRecords = getSignalStorageRecords(storageKey, + remoteManifest, + idDifference.remoteOnlyIds()); if (remoteOnlyRecords.size() != idDifference.remoteOnlyIds().size()) { - logger.debug("Could not find all remote-only records! Requested: " - + idDifference.remoteOnlyIds() - .size() - + ", Found: " - + remoteOnlyRecords.size() - + ". These stragglers should naturally get deleted during the sync."); + logger.debug( + "Could not find all remote-only records! Requested: {}, Found: {}. These stragglers should naturally get deleted during the sync.", + idDifference.remoteOnlyIds().size(), + remoteOnlyRecords.size()); } final var unknownInserts = processKnownRecords(connection, remoteOnlyRecords); @@ -194,18 +243,21 @@ public class StorageHelper { return needsForcePush; } - private void readRecordsWithPreviouslyUnknownTypes(final StorageKey storageKey) throws IOException { + private void readRecordsWithPreviouslyUnknownTypes( + final StorageKey storageKey, + final SignalStorageManifest remoteManifest + ) throws IOException { try (final var connection = account.getAccountDatabase().getConnection()) { connection.setAutoCommit(false); final var knownUnknownIds = account.getUnknownStorageIdStore() .getUnknownStorageIds(connection, KNOWN_TYPES); if (!knownUnknownIds.isEmpty()) { - logger.debug("We have " + knownUnknownIds.size() + " unknown records that we can now process."); + logger.debug("We have {} unknown records that we can now process.", knownUnknownIds.size()); - final var remote = getSignalStorageRecords(storageKey, knownUnknownIds); + final var remote = getSignalStorageRecords(storageKey, remoteManifest, knownUnknownIds); - logger.debug("Found " + remote.size() + " of the known-unknowns remotely."); + logger.debug("Found {} of the known-unknowns remotely.", remote.size()); processKnownRecords(connection, remote); account.getUnknownStorageIdStore() @@ -227,15 +279,16 @@ public class StorageHelper { connection.setAutoCommit(false); final var localStorageIds = getAllLocalStorageIds(connection); - final var idDifference = findIdDifference(remoteManifest.getStorageIds(), localStorageIds); - logger.debug("ID Difference :: " + idDifference); + final var idDifference = findIdDifference(remoteManifest.storageIds, localStorageIds); + logger.debug("ID Difference :: {}", idDifference); final var remoteDeletes = idDifference.remoteOnlyIds().stream().map(StorageId::getRaw).toList(); final var remoteInserts = buildLocalStorageRecords(connection, idDifference.localOnlyIds()); // TODO check if local storage record proto matches remote, then reset to remote storage_id - remoteWriteOperation = new WriteOperationResult(new SignalStorageManifest(remoteManifest.getVersion() + 1, + remoteWriteOperation = new WriteOperationResult(new SignalStorageManifest(remoteManifest.version + 1, account.getDeviceId(), + remoteManifest.recordIkm, localStorageIds), remoteInserts, remoteDeletes); connection.commit(); @@ -244,39 +297,37 @@ public class StorageHelper { } if (remoteWriteOperation.isEmpty()) { - logger.debug("No remote writes needed. Still at version: " + remoteManifest.getVersion()); + logger.debug("No remote writes needed. Still at version: {}", remoteManifest.version); return false; } logger.debug("We have something to write remotely."); - logger.debug("WriteOperationResult :: " + remoteWriteOperation); + logger.debug("WriteOperationResult :: {}", remoteWriteOperation); StorageSyncValidations.validate(remoteWriteOperation, remoteManifest, needsForcePush, account.getSelfRecipientAddress()); - final Optional conflict; - try { - conflict = dependencies.getAccountManager() - .writeStorageRecords(storageKey, - remoteWriteOperation.manifest(), - remoteWriteOperation.inserts(), - remoteWriteOperation.deletes()); - } catch (InvalidKeyException e) { - logger.warn("Failed to decrypt conflicting storage manifest: {}", e.getMessage()); - throw new IOException(e); - } - - if (conflict.isPresent()) { - logger.debug("Hit a conflict when trying to resolve the conflict! Retrying."); - throw new RetryLaterException(); + final var result = dependencies.getStorageServiceRepository() + .writeStorageRecords(storageKey, + remoteWriteOperation.manifest(), + remoteWriteOperation.inserts(), + remoteWriteOperation.deletes()); + switch (result) { + case WriteStorageRecordsResult.ConflictError ignored -> { + logger.debug("Hit a conflict when trying to resolve the conflict! Retrying."); + throw new RetryLaterException(); + } + case WriteStorageRecordsResult.NetworkError networkError -> throw networkError.getException(); + case WriteStorageRecordsResult.StatusCodeError statusCodeError -> throw statusCodeError.getException(); + case WriteStorageRecordsResult.Success ignored -> { + logger.debug("Saved new manifest. Now at version: {}", remoteWriteOperation.manifest().version); + storeManifestLocally(remoteWriteOperation.manifest()); + return true; + } + default -> throw new IllegalStateException("Unexpected value: " + result); } - - logger.debug("Saved new manifest. Now at version: " + remoteWriteOperation.manifest().getVersion()); - storeManifestLocally(remoteWriteOperation.manifest()); - - return true; } private void forcePushToStorage( @@ -284,7 +335,8 @@ public class StorageHelper { ) throws IOException, RetryLaterException { logger.debug("Force pushing local state to remote storage"); - final var currentVersion = dependencies.getAccountManager().getStorageManifestVersion(); + final var currentVersion = handleResponseException(dependencies.getStorageServiceRepository() + .getManifestVersion()); final var newVersion = currentVersion + 1; final var newStorageRecords = new ArrayList(); final Map newContactStorageIds; @@ -302,15 +354,16 @@ public class StorageHelper { final var recipient = account.getRecipientStore().getRecipient(connection, recipientId); final var accountRecord = StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(), recipient, - account.getUsernameLink(), - storageId.getRaw()); - newStorageRecords.add(accountRecord); + account.getUsernameLink()); + newStorageRecords.add(new SignalStorageRecord(storageId, + new StorageRecord.Builder().account(accountRecord).build())); } else { final var recipient = account.getRecipientStore().getRecipient(connection, recipientId); final var address = recipient.getAddress().getIdentifier(); final var identity = account.getIdentityKeyStore().getIdentityInfo(connection, address); - final var record = StorageSyncModels.localToRemoteRecord(recipient, identity, storageId.getRaw()); - newStorageRecords.add(record); + final var record = StorageSyncModels.localToRemoteRecord(recipient, identity); + newStorageRecords.add(new SignalStorageRecord(storageId, + new StorageRecord.Builder().contact(record).build())); } } @@ -319,8 +372,9 @@ public class StorageHelper { for (final var groupId : groupV1Ids) { final var storageId = newGroupV1StorageIds.get(groupId); final var group = account.getGroupStore().getGroup(connection, groupId); - final var record = StorageSyncModels.localToRemoteRecord(group, storageId.getRaw()); - newStorageRecords.add(record); + final var record = StorageSyncModels.localToRemoteRecord(group); + newStorageRecords.add(new SignalStorageRecord(storageId, + new StorageRecord.Builder().groupV1(record).build())); } final var groupV2Ids = account.getGroupStore().getGroupV2Ids(connection); @@ -328,8 +382,9 @@ public class StorageHelper { for (final var groupId : groupV2Ids) { final var storageId = newGroupV2StorageIds.get(groupId); final var group = account.getGroupStore().getGroup(connection, groupId); - final var record = StorageSyncModels.localToRemoteRecord(group, storageId.getRaw()); - newStorageRecords.add(record); + final var record = StorageSyncModels.localToRemoteRecord(group); + newStorageRecords.add(new SignalStorageRecord(storageId, + new StorageRecord.Builder().groupV2(record).build())); } connection.commit(); @@ -338,34 +393,46 @@ public class StorageHelper { } final var newStorageIds = newStorageRecords.stream().map(SignalStorageRecord::getId).toList(); - final var manifest = new SignalStorageManifest(newVersion, account.getDeviceId(), newStorageIds); + final RecordIkm recordIkm; + if (account.getSelfRecipientProfile() + .getCapabilities() + .contains(Profile.Capability.storageServiceEncryptionV2Capability)) { + logger.debug("Generating and including a new recordIkm."); + recordIkm = RecordIkm.Companion.generate(); + } else { + logger.debug("SSRE2 not yet supported. Not including recordIkm."); + recordIkm = null; + } + + final var manifest = new SignalStorageManifest(newVersion, account.getDeviceId(), recordIkm, newStorageIds); StorageSyncValidations.validateForcePush(manifest, newStorageRecords, account.getSelfRecipientAddress()); - final Optional conflict; - try { - if (newVersion > 1) { - logger.trace("Force-pushing data. Inserting {} IDs.", newStorageRecords.size()); - conflict = dependencies.getAccountManager() - .resetStorageRecords(storageServiceKey, manifest, newStorageRecords); - } else { - logger.trace("First version, normal push. Inserting {} IDs.", newStorageRecords.size()); - conflict = dependencies.getAccountManager() - .writeStorageRecords(storageServiceKey, manifest, newStorageRecords, Collections.emptyList()); - } - } catch (InvalidKeyException e) { - logger.debug("Hit an invalid key exception, which likely indicates a conflict.", e); - throw new RetryLaterException(); + final WriteStorageRecordsResult result; + if (newVersion > 1) { + logger.trace("Force-pushing data. Inserting {} IDs.", newStorageRecords.size()); + result = dependencies.getStorageServiceRepository() + .resetAndWriteStorageRecords(storageServiceKey, manifest, newStorageRecords); + } else { + logger.trace("First version, normal push. Inserting {} IDs.", newStorageRecords.size()); + result = dependencies.getStorageServiceRepository() + .writeStorageRecords(storageServiceKey, manifest, newStorageRecords, Collections.emptyList()); } - if (conflict.isPresent()) { - logger.debug("Hit a conflict. Trying again."); - throw new RetryLaterException(); + switch (result) { + case WriteStorageRecordsResult.ConflictError ignored -> { + logger.debug("Hit a conflict. Trying again."); + throw new RetryLaterException(); + } + case WriteStorageRecordsResult.NetworkError networkError -> throw networkError.getException(); + case WriteStorageRecordsResult.StatusCodeError statusCodeError -> throw statusCodeError.getException(); + case WriteStorageRecordsResult.Success ignored -> { + logger.debug("Force push succeeded. Updating local manifest version to: {}", manifest.version); + storeManifestLocally(manifest); + } + default -> throw new IllegalStateException("Unexpected value: " + result); } - logger.debug("Force push succeeded. Updating local manifest version to: " + manifest.getVersion()); - storeManifestLocally(manifest); - try (final var connection = account.getAccountDatabase().getConnection()) { connection.setAutoCommit(false); account.getRecipientStore().updateStorageIds(connection, newContactStorageIds); @@ -405,22 +472,35 @@ public class StorageHelper { private void storeManifestLocally( final SignalStorageManifest remoteManifest ) { - account.setStorageManifestVersion(remoteManifest.getVersion()); + account.setStorageManifestVersion(remoteManifest.version); account.setStorageManifest(remoteManifest); } private List getSignalStorageRecords( final StorageKey storageKey, + final SignalStorageManifest manifest, final List storageIds ) throws IOException { - List records; - try { - records = dependencies.getAccountManager().readStorageRecords(storageKey, storageIds); - } catch (InvalidKeyException e) { - logger.warn("Failed to read storage records, ignoring."); - return List.of(); - } - return records; + final var result = dependencies.getStorageServiceRepository() + .readStorageRecords(storageKey, manifest.recordIkm, storageIds); + return switch (result) { + case StorageServiceRepository.StorageRecordResult.DecryptionError decryptionError -> { + if (decryptionError.getException() instanceof InvalidKeyException) { + logger.warn("Failed to read storage records, ignoring."); + yield List.of(); + } else if (decryptionError.getException() instanceof IOException ioe) { + throw ioe; + } else { + throw new IOException(decryptionError.getException()); + } + } + case StorageServiceRepository.StorageRecordResult.NetworkError networkError -> + throw networkError.getException(); + case StorageServiceRepository.StorageRecordResult.StatusCodeError statusCodeError -> + throw statusCodeError.getException(); + case StorageServiceRepository.StorageRecordResult.Success success -> success.getRecords(); + default -> throw new IllegalStateException("Unexpected value: " + result); + }; } private List getAllLocalStorageIds(final Connection connection) throws SQLException { @@ -436,12 +516,10 @@ public class StorageHelper { final Connection connection, final List storageIds ) throws SQLException { - final var records = new ArrayList(); + final var records = new ArrayList(storageIds.size()); for (final var storageId : storageIds) { final var record = buildLocalStorageRecord(connection, storageId); - if (record != null) { - records.add(record); - } + records.add(record); } return records; } @@ -455,25 +533,31 @@ public class StorageHelper { final var recipient = account.getRecipientStore().getRecipient(connection, storageId); final var address = recipient.getAddress().getIdentifier(); final var identity = account.getIdentityKeyStore().getIdentityInfo(connection, address); - yield StorageSyncModels.localToRemoteRecord(recipient, identity, storageId.getRaw()); + final var record = StorageSyncModels.localToRemoteRecord(recipient, identity); + yield new SignalStorageRecord(storageId, new StorageRecord.Builder().contact(record).build()); } case ManifestRecord.Identifier.Type.GROUPV1 -> { final var groupV1 = account.getGroupStore().getGroupV1(connection, storageId); - yield StorageSyncModels.localToRemoteRecord(groupV1, storageId.getRaw()); + final var record = StorageSyncModels.localToRemoteRecord(groupV1); + yield new SignalStorageRecord(storageId, new StorageRecord.Builder().groupV1(record).build()); } case ManifestRecord.Identifier.Type.GROUPV2 -> { final var groupV2 = account.getGroupStore().getGroupV2(connection, storageId); - yield StorageSyncModels.localToRemoteRecord(groupV2, storageId.getRaw()); + final var record = StorageSyncModels.localToRemoteRecord(groupV2); + yield new SignalStorageRecord(storageId, new StorageRecord.Builder().groupV2(record).build()); } case ManifestRecord.Identifier.Type.ACCOUNT -> { final var selfRecipient = account.getRecipientStore() .getRecipient(connection, account.getSelfRecipientId()); - yield StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(), + + final var record = StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(), selfRecipient, - account.getUsernameLink(), - storageId.getRaw()); + account.getUsernameLink()); + yield new SignalStorageRecord(storageId, new StorageRecord.Builder().account(record).build()); + } + case null, default -> { + throw new AssertionError("Got unknown local storage record type: " + storageId); } - case null, default -> throw new AssertionError("Got unknown local storage record type: " + storageId); }; } @@ -537,13 +621,24 @@ public class StorageHelper { final var groupV2RecordProcessor = new GroupV2RecordProcessor(account, connection); for (final var record : records) { - logger.debug("Reading record of type {}", record.getType()); - switch (ManifestRecord.Identifier.Type.fromValue(record.getType())) { - case ACCOUNT -> accountRecordProcessor.process(record.getAccount().get()); - case GROUPV1 -> groupV1RecordProcessor.process(record.getGroupV1().get()); - case GROUPV2 -> groupV2RecordProcessor.process(record.getGroupV2().get()); - case CONTACT -> contactRecordProcessor.process(record.getContact().get()); - case null, default -> unknownRecords.add(record.getId()); + if (record.getProto().account != null) { + logger.debug("Reading record {} of type account", record.getId()); + accountRecordProcessor.process(StorageRecordConvertersKt.toSignalAccountRecord(record.getProto().account, + record.getId())); + } else if (record.getProto().groupV1 != null) { + logger.debug("Reading record {} of type groupV1", record.getId()); + groupV1RecordProcessor.process(StorageRecordConvertersKt.toSignalGroupV1Record(record.getProto().groupV1, + record.getId())); + } else if (record.getProto().groupV2 != null) { + logger.debug("Reading record {} of type groupV2", record.getId()); + groupV2RecordProcessor.process(StorageRecordConvertersKt.toSignalGroupV2Record(record.getProto().groupV2, + record.getId())); + } else if (record.getProto().contact != null) { + logger.debug("Reading record {} of type contact", record.getId()); + contactRecordProcessor.process(StorageRecordConvertersKt.toSignalContactRecord(record.getProto().contact, + record.getId())); + } else { + unknownRecords.add(record.getId()); } } 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 70427f00..39726df8 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 @@ -247,10 +247,14 @@ public class SyncHelper { } public SendMessageResult sendBlockedList() { - var addresses = new ArrayList(); + var addresses = new ArrayList(); for (var record : account.getContactStore().getContacts()) { if (record.second().isBlocked()) { - addresses.add(context.getRecipientHelper().resolveSignalServiceAddress(record.first())); + final var address = account.getRecipientAddressResolver().resolveRecipientAddress(record.first()); + if (address.aci().isPresent() || address.number().isPresent()) { + addresses.add(new BlockedListMessage.Individual(address.aci().orElse(null), + address.number().orElse(null))); + } } } var groupIds = new ArrayList(); @@ -276,8 +280,10 @@ public class SyncHelper { } public SendMessageResult sendKeysMessage() { - var keysMessage = new KeysMessage(Optional.ofNullable(account.getOrCreateStorageKey()), - Optional.ofNullable(account.getOrCreatePinMasterKey())); + var keysMessage = new KeysMessage(account.getOrCreateStorageKey(), + account.getOrCreatePinMasterKey(), + account.getOrCreateAccountEntropyPool(), + account.getOrCreateMediaRootBackupKey()); return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forKeys(keysMessage)); } @@ -405,7 +411,7 @@ public class SyncHelper { builder.withMessageExpirationTimeVersion(c.getExpirationTimerVersion().get()); } else { logger.debug( - "[ContactSync] {} was synced with an old expiration timer. Ignoring. Received: {} Current: ${}", + "[ContactSync] {} was synced with an old expiration timer. Ignoring. Received: {} Current: {}", recipientId, c.getExpirationTimerVersion(), contact == null ? 1 : contact.messageExpirationTimeVersion()); diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java b/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java index 7ee2b39e..90e3b159 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java @@ -15,10 +15,13 @@ import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; +import org.whispersystems.signalservice.api.link.LinkDeviceApi; import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.registration.RegistrationApi; import org.whispersystems.signalservice.api.services.ProfileService; +import org.whispersystems.signalservice.api.storage.StorageServiceApi; +import org.whispersystems.signalservice.api.storage.StorageServiceRepository; import org.whispersystems.signalservice.api.svr.SecureValueRecovery; import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.UptimeSleepTimer; @@ -49,6 +52,8 @@ public class SignalDependencies { private SignalServiceAccountManager accountManager; private GroupsV2Api groupsV2Api; private RegistrationApi registrationApi; + private LinkDeviceApi linkDeviceApi; + private StorageServiceApi storageServiceApi; private GroupsV2Operations groupsV2Operations; private ClientZkOperations clientZkOperations; @@ -155,6 +160,19 @@ public class SignalDependencies { return getOrCreate(() -> registrationApi, () -> registrationApi = getAccountManager().getRegistrationApi()); } + public LinkDeviceApi getLinkDeviceApi() { + return getOrCreate(() -> linkDeviceApi, () -> linkDeviceApi = new LinkDeviceApi(getPushServiceSocket())); + } + + private StorageServiceApi getStorageServiceApi() { + return getOrCreate(() -> storageServiceApi, + () -> storageServiceApi = new StorageServiceApi(getPushServiceSocket())); + } + + public StorageServiceRepository getStorageServiceRepository() { + return new StorageServiceRepository(getStorageServiceApi()); + } + public GroupsV2Operations getGroupsV2Operations() { return getOrCreate(() -> groupsV2Operations, () -> groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.signalServiceConfiguration()), diff --git a/lib/src/main/java/org/asamk/signal/manager/jobs/SyncStorageJob.java b/lib/src/main/java/org/asamk/signal/manager/jobs/SyncStorageJob.java index 85cfbe2c..d695c7b1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/jobs/SyncStorageJob.java +++ b/lib/src/main/java/org/asamk/signal/manager/jobs/SyncStorageJob.java @@ -8,13 +8,27 @@ import java.io.IOException; public class SyncStorageJob implements Job { + private final boolean forcePush; + private static final Logger logger = LoggerFactory.getLogger(SyncStorageJob.class); + public SyncStorageJob() { + this.forcePush = false; + } + + public SyncStorageJob(final boolean forcePush) { + this.forcePush = forcePush; + } + @Override public void run(Context context) { logger.trace("Running storage sync job"); try { - context.getStorageHelper().syncDataWithStorage(); + if (forcePush) { + context.getStorageHelper().forcePushToStorage(); + } else { + context.getStorageHelper().syncDataWithStorage(); + } } catch (IOException e) { logger.warn("Failed to sync storage data", e); } 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 5c8a5530..2529ad4a 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 @@ -65,10 +65,12 @@ import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.AccountEntropyPool; import org.whispersystems.signalservice.api.SignalServiceAccountDataStore; import org.whispersystems.signalservice.api.SignalServiceDataStore; import org.whispersystems.signalservice.api.account.AccountAttributes; import org.whispersystems.signalservice.api.account.PreKeyCollection; +import org.whispersystems.signalservice.api.backup.MediaRootBackupKey; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.api.push.ServiceId; @@ -138,6 +140,8 @@ public class SignalAccount implements Closeable { private String registrationLockPin; private MasterKey pinMasterKey; private StorageKey storageKey; + private AccountEntropyPool accountEntropyPool; + private MediaRootBackupKey mediaRootBackupKey; private ProfileKey profileKey; private Settings settings; @@ -305,6 +309,7 @@ public class SignalAccount implements Closeable { this.isMultiDevice = true; setLastReceiveTimestamp(0L); this.pinMasterKey = masterKey; + this.accountEntropyPool = null; getKeyValueStore().storeEntry(storageManifestVersion, -1L); this.setStorageManifest(null); this.storageKey = null; @@ -339,6 +344,7 @@ public class SignalAccount implements Closeable { final PreKeyCollection pniPreKeys ) { this.pinMasterKey = masterKey; + this.accountEntropyPool = null; getKeyValueStore().storeEntry(storageManifestVersion, -1L); this.setStorageManifest(null); this.storageKey = null; @@ -499,6 +505,12 @@ public class SignalAccount implements Closeable { if (storage.storageKey != null) { storageKey = new StorageKey(base64.decode(storage.storageKey)); } + if (storage.accountEntropyPool != null) { + accountEntropyPool = new AccountEntropyPool(storage.accountEntropyPool); + } + if (storage.mediaRootBackupKey != null) { + mediaRootBackupKey = new MediaRootBackupKey(base64.decode(storage.mediaRootBackupKey)); + } if (storage.profileKey != null) { try { profileKey = new ProfileKey(base64.decode(storage.profileKey)); @@ -981,6 +993,8 @@ public class SignalAccount implements Closeable { registrationLockPin, pinMasterKey == null ? null : base64.encodeToString(pinMasterKey.serialize()), storageKey == null ? null : base64.encodeToString(storageKey.serialize()), + accountEntropyPool == null ? null : accountEntropyPool.getValue(), + mediaRootBackupKey == null ? null : base64.encodeToString(mediaRootBackupKey.getValue()), profileKey == null ? null : base64.encodeToString(profileKey.serialize()), usernameLink == null ? null : base64.encodeToString(usernameLink.getEntropy()), usernameLink == null ? null : usernameLink.getServerId().toString()); @@ -1442,6 +1456,10 @@ public class SignalAccount implements Closeable { return selfRecipientId; } + public Profile getSelfRecipientProfile() { + return recipientStore.getProfile(selfRecipientId); + } + public String getSessionId(final String forNumber) { final var keyValueStore = getKeyValueStore(); final var sessionNumber = keyValueStore.getEntry(verificationSessionNumber); @@ -1512,31 +1530,50 @@ public class SignalAccount implements Closeable { public MasterKey getPinBackedMasterKey() { if (registrationLockPin == null) { return null; + } else if (!isPrimaryDevice()) { + return getMasterKey(); } - return pinMasterKey; + return getOrCreatePinMasterKey(); } public MasterKey getOrCreatePinMasterKey() { - if (pinMasterKey == null) { - pinMasterKey = KeyUtils.createMasterKey(); - save(); + final var key = getMasterKey(); + if (key != null) { + return key; } + + pinMasterKey = KeyUtils.createMasterKey(); + save(); return pinMasterKey; } + private MasterKey getMasterKey() { + if (pinMasterKey != null) { + return pinMasterKey; + } else if (accountEntropyPool != null) { + return accountEntropyPool.deriveMasterKey(); + } + return null; + } + public void setMasterKey(MasterKey masterKey) { if (isPrimaryDevice()) { return; } this.pinMasterKey = masterKey; + if (masterKey != null) { + this.storageKey = null; + } save(); } public StorageKey getOrCreateStorageKey() { - if (pinMasterKey != null) { - return pinMasterKey.deriveStorageServiceKey(); - } else if (storageKey != null) { + if (storageKey != null) { return storageKey; + } else if (pinMasterKey != null) { + return pinMasterKey.deriveStorageServiceKey(); + } else if (accountEntropyPool != null) { + return accountEntropyPool.deriveMasterKey().deriveStorageServiceKey(); } else if (!isPrimaryDevice() || !isMultiDevice()) { // Only upload storage, if a pin master key already exists or linked devices exist return null; @@ -1553,6 +1590,40 @@ public class SignalAccount implements Closeable { save(); } + public AccountEntropyPool getOrCreateAccountEntropyPool() { + if (accountEntropyPool == null) { + accountEntropyPool = AccountEntropyPool.Companion.generate(); + save(); + } + return accountEntropyPool; + } + + public void setAccountEntropyPool(final AccountEntropyPool accountEntropyPool) { + this.accountEntropyPool = accountEntropyPool; + if (accountEntropyPool != null) { + this.storageKey = null; + this.pinMasterKey = null; + } + save(); + } + + public boolean needsStorageKeyMigration() { + return isPrimaryDevice() && (storageKey != null || pinMasterKey != null); + } + + public MediaRootBackupKey getOrCreateMediaRootBackupKey() { + if (mediaRootBackupKey == null) { + mediaRootBackupKey = KeyUtils.createMediaRootBackupKey(); + save(); + } + return mediaRootBackupKey; + } + + public void setMediaRootBackupKey(final MediaRootBackupKey mediaRootBackupKey) { + this.mediaRootBackupKey = mediaRootBackupKey; + save(); + } + public String getRecoveryPassword() { final var masterKey = getPinBackedMasterKey(); if (masterKey == null) { @@ -1575,7 +1646,7 @@ public class SignalAccount implements Closeable { return Optional.empty(); } try (var inputStream = new FileInputStream(storageManifestFile)) { - return Optional.of(SignalStorageManifest.deserialize(inputStream.readAllBytes())); + return Optional.of(SignalStorageManifest.Companion.deserialize(inputStream.readAllBytes())); } catch (IOException e) { logger.warn("Failed to read local storage manifest.", e); return Optional.empty(); @@ -1882,6 +1953,8 @@ public class SignalAccount implements Closeable { String registrationLockPin, String pinMasterKey, String storageKey, + String accountEntropyPool, + String mediaRootBackupKey, String profileKey, String usernameLinkEntropy, String usernameLinkServerId diff --git a/lib/src/main/java/org/asamk/signal/manager/syncStorage/AccountRecordProcessor.java b/lib/src/main/java/org/asamk/signal/manager/syncStorage/AccountRecordProcessor.java index 18dd1869..b2f7dd29 100644 --- a/lib/src/main/java/org/asamk/signal/manager/syncStorage/AccountRecordProcessor.java +++ b/lib/src/main/java/org/asamk/signal/manager/syncStorage/AccountRecordProcessor.java @@ -12,8 +12,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.push.UsernameLinkComponents; import org.whispersystems.signalservice.api.storage.SignalAccountRecord; -import org.whispersystems.signalservice.api.util.OptionalUtil; +import org.whispersystems.signalservice.api.storage.StorageId; import org.whispersystems.signalservice.api.util.UuidUtil; +import org.whispersystems.signalservice.internal.storage.protos.AccountRecord; import org.whispersystems.signalservice.internal.storage.protos.OptionalBool; import java.sql.Connection; @@ -21,6 +22,9 @@ import java.sql.SQLException; import java.util.Arrays; import java.util.Optional; +import static org.asamk.signal.manager.util.Utils.firstNonEmpty; +import static org.asamk.signal.manager.util.Utils.firstNonNull; + /** * Processes {@link SignalAccountRecord}s. */ @@ -43,10 +47,10 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor update) throws SQLException { final var accountRecord = update.newRecord(); + final var accountProto = accountRecord.getProto(); - if (!accountRecord.getE164().equals(account.getNumber())) { + if (!accountProto.e164.equals(account.getNumber())) { jobExecutor.enqueueJob(new CheckWhoAmIJob()); } - account.getConfigurationStore().setReadReceipts(connection, accountRecord.isReadReceiptsEnabled()); - account.getConfigurationStore().setTypingIndicators(connection, accountRecord.isTypingIndicatorsEnabled()); + account.getConfigurationStore().setReadReceipts(connection, accountProto.readReceipts); + account.getConfigurationStore().setTypingIndicators(connection, accountProto.typingIndicators); account.getConfigurationStore() - .setUnidentifiedDeliveryIndicators(connection, accountRecord.isSealedSenderIndicatorsEnabled()); - account.getConfigurationStore().setLinkPreviews(connection, accountRecord.isLinkPreviewsEnabled()); + .setUnidentifiedDeliveryIndicators(connection, accountProto.sealedSenderIndicators); + account.getConfigurationStore().setLinkPreviews(connection, accountProto.linkPreviews); account.getConfigurationStore() .setPhoneNumberSharingMode(connection, - StorageSyncModels.remoteToLocal(accountRecord.getPhoneNumberSharingMode())); - account.getConfigurationStore().setPhoneNumberUnlisted(connection, accountRecord.isPhoneNumberUnlisted()); - - account.setUsername(accountRecord.getUsername() != null && !accountRecord.getUsername().isEmpty() - ? accountRecord.getUsername() - : null); - if (accountRecord.getUsernameLink() != null) { - final var usernameLink = accountRecord.getUsernameLink(); + StorageSyncModels.remoteToLocal(accountProto.phoneNumberSharingMode)); + account.getConfigurationStore().setPhoneNumberUnlisted(connection, accountProto.unlistedPhoneNumber); + + account.setUsername(!accountProto.username.isEmpty() ? accountProto.username : null); + if (accountProto.usernameLink != null) { + final var usernameLink = accountProto.usernameLink; account.setUsernameLink(new UsernameLinkComponents(usernameLink.entropy.toByteArray(), UuidUtil.parseOrThrow(usernameLink.serverId.toByteArray()))); account.getConfigurationStore().setUsernameLinkColor(connection, usernameLink.color.name()); } - if (accountRecord.getProfileKey().isPresent()) { + if (accountProto.profileKey.size() > 0) { ProfileKey profileKey; try { - profileKey = new ProfileKey(accountRecord.getProfileKey().get()); + profileKey = new ProfileKey(accountProto.profileKey.toByteArray()); } catch (InvalidInputException e) { logger.debug("Received invalid profile key from storage"); profileKey = null; } if (profileKey != null) { account.setProfileKey(profileKey); - final var avatarPath = accountRecord.getAvatarUrlPath().orElse(null); + final var avatarPath = accountProto.avatarUrlPath.isEmpty() ? null : accountProto.avatarUrlPath; jobExecutor.enqueueJob(new DownloadProfileAvatarJob(avatarPath)); } } final var profile = account.getRecipientStore().getProfile(connection, account.getSelfRecipientId()); final var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile); - builder.withGivenName(accountRecord.getGivenName().orElse(null)); - builder.withFamilyName(accountRecord.getFamilyName().orElse(null)); + builder.withGivenName(accountProto.givenName); + builder.withFamilyName(accountProto.familyName); account.getRecipientStore().storeProfile(connection, account.getSelfRecipientId(), builder.build()); account.getRecipientStore() .storeStorageRecord(connection, account.getSelfRecipientId(), accountRecord.getId(), - accountRecord.toProto().encode()); + accountProto.encode()); } @Override @@ -221,7 +198,7 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor { private static final Logger logger = LoggerFactory.getLogger(ContactRecordProcessor.class); @@ -55,20 +61,24 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor getMatching(SignalContactRecord remote) throws SQLException { - final var address = getRecipientAddress(remote); + final var address = getRecipientAddress(remote.getProto()); final var recipientId = account.getRecipientStore().resolveRecipient(connection, address); final var recipient = account.getRecipientStore().getRecipient(connection, recipientId); @@ -85,141 +95,120 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor 0 && ( + !account.isPrimaryDevice() + || remote.identityState != local.identityState + || local.identityKey.size() == 0 )) { - identityState = remote.getIdentityState(); - identityKey = remote.getIdentityKey().get(); + identityState = remote.identityState; + identityKey = remote.identityKey; } else { - identityState = local.getIdentityState(); - identityKey = local.getIdentityKey().orElse(null); + identityState = local.identityState; + identityKey = local.identityKey.size() > 0 ? local.identityKey : ByteString.EMPTY; } - if (local.getAci().isPresent() - && local.getIdentityKey().isPresent() - && remote.getIdentityKey().isPresent() - && !Arrays.equals(local.getIdentityKey().get(), remote.getIdentityKey().get())) { + if (!local.aci.isEmpty() + && local.identityKey.size() > 0 + && remote.identityKey.size() > 0 + && !local.identityKey.equals(remote.identityKey)) { logger.debug("The local and remote identity keys do not match for {}. Enqueueing a profile fetch.", - local.getAci().orElse(null)); + local.aci); final var address = getRecipientAddress(local); jobExecutor.enqueueJob(new DownloadProfileJob(address)); } - final var e164sMatchButPnisDont = local.getNumber().isPresent() - && local.getNumber() - .get() - .equals(remote.getNumber().orElse(null)) - && local.getPni().isPresent() - && remote.getPni().isPresent() - && !local.getPni().get().equals(remote.getPni().get()); - - final var pnisMatchButE164sDont = local.getPni().isPresent() - && local.getPni() - .get() - .equals(remote.getPni().orElse(null)) - && local.getNumber().isPresent() - && remote.getNumber().isPresent() - && !local.getNumber().get().equals(remote.getNumber().get()); - - PNI pni; + String pni; String e164; - if (!account.isPrimaryDevice() && (e164sMatchButPnisDont || pnisMatchButE164sDont)) { - if (e164sMatchButPnisDont) { - logger.debug("Matching E164s, but the PNIs differ! Trusting our local pair."); - } else if (pnisMatchButE164sDont) { - logger.debug("Matching PNIs, but the E164s differ! Trusting our local pair."); + if (account.isPrimaryDevice()) { + final var e164sMatchButPnisDont = !local.e164.isEmpty() + && local.e164.equals(remote.e164) + && !local.pni.isEmpty() + && !remote.pni.isEmpty() + && !local.pni.equals(remote.pni); + + final var pnisMatchButE164sDont = !local.pni.isEmpty() + && local.pni.equals(remote.pni) + && !local.e164.isEmpty() + && !remote.e164.isEmpty() + && !local.e164.equals(remote.e164); + + if (e164sMatchButPnisDont || pnisMatchButE164sDont) { + if (e164sMatchButPnisDont) { + logger.debug("Matching E164s, but the PNIs differ! Trusting our local pair."); + } else if (pnisMatchButE164sDont) { + logger.debug("Matching PNIs, but the E164s differ! Trusting our local pair."); + } + jobExecutor.enqueueJob(new RefreshRecipientsJob()); + pni = local.pni; + e164 = local.e164; + } else { + pni = firstNonEmpty(remote.pni, local.pni); + e164 = firstNonEmpty(remote.e164, local.e164); } - jobExecutor.enqueueJob(new RefreshRecipientsJob()); - pni = local.getPni().get(); - e164 = local.getNumber().get(); } else { - pni = OptionalUtil.or(remote.getPni(), local.getPni()).orElse(null); - e164 = OptionalUtil.or(remote.getNumber(), local.getNumber()).orElse(null); + pni = firstNonEmpty(remote.pni, local.pni); + e164 = firstNonEmpty(remote.e164, local.e164); } - final var unknownFields = remote.serializeUnknownFields(); - final var aci = local.getAci().isEmpty() ? remote.getAci().orElse(null) : local.getAci().get(); - final var profileKey = OptionalUtil.or(remote.getProfileKey(), local.getProfileKey()).orElse(null); - final var username = OptionalUtil.or(remote.getUsername(), local.getUsername()).orElse(""); - final var blocked = remote.isBlocked(); - final var profileSharing = remote.isProfileSharingEnabled(); - final var archived = remote.isArchived(); - final var forcedUnread = remote.isForcedUnread(); - final var muteUntil = remote.getMuteUntil(); - final var hideStory = remote.shouldHideStory(); - final var unregisteredTimestamp = remote.getUnregisteredTimestamp(); - final var hidden = remote.isHidden(); - final var systemGivenName = account.isPrimaryDevice() - ? local.getSystemGivenName().orElse("") - : remote.getSystemGivenName().orElse(""); - final var systemFamilyName = account.isPrimaryDevice() - ? local.getSystemFamilyName().orElse("") - : remote.getSystemFamilyName().orElse(""); - final var systemNickname = remote.getSystemNickname().orElse(""); - final var nicknameGivenName = remote.getNicknameGivenName().orElse(""); - final var nicknameFamilyName = remote.getNicknameFamilyName().orElse(""); - final var pniSignatureVerified = remote.isPniSignatureVerified() || local.isPniSignatureVerified(); - final var note = remote.getNote().or(local::getNote).orElse(""); - - final var mergedBuilder = new SignalContactRecord.Builder(remote.getId().getRaw(), aci, unknownFields).setE164( - e164) - .setPni(pni) - .setProfileGivenName(profileGivenName) - .setProfileFamilyName(profileFamilyName) - .setSystemGivenName(systemGivenName) - .setSystemFamilyName(systemFamilyName) - .setSystemNickname(systemNickname) - .setProfileKey(profileKey) - .setUsername(username) - .setIdentityState(identityState) - .setIdentityKey(identityKey) - .setBlocked(blocked) - .setProfileSharingEnabled(profileSharing) - .setArchived(archived) - .setForcedUnread(forcedUnread) - .setMuteUntil(muteUntil) - .setHideStory(hideStory) - .setUnregisteredTimestamp(unregisteredTimestamp) - .setHidden(hidden) - .setPniSignatureVerified(pniSignatureVerified) - .setNicknameGivenName(nicknameGivenName) - .setNicknameFamilyName(nicknameFamilyName) - .setNote(note); + final var mergedBuilder = SignalContactRecord.Companion.newBuilder(remote.unknownFields().toByteArray()) + .aci(local.aci.isEmpty() ? remote.aci : local.aci) + .e164(e164) + .pni(pni) + .givenName(profileGivenName) + .familyName(profileFamilyName) + .systemGivenName(account.isPrimaryDevice() ? local.systemGivenName : remote.systemGivenName) + .systemFamilyName(account.isPrimaryDevice() ? local.systemFamilyName : remote.systemFamilyName) + .systemNickname(remote.systemNickname) + .profileKey(firstNonEmpty(remote.profileKey, local.profileKey)) + .username(firstNonEmpty(remote.username, local.username)) + .identityState(identityState) + .identityKey(identityKey) + .blocked(remote.blocked) + .whitelisted(remote.whitelisted) + .archived(remote.archived) + .markedUnread(remote.markedUnread) + .mutedUntilTimestamp(remote.mutedUntilTimestamp) + .hideStory(remote.hideStory) + .unregisteredAtTimestamp(remote.unregisteredAtTimestamp) + .hidden(remote.hidden) + .pniSignatureVerified(remote.pniSignatureVerified || local.pniSignatureVerified) + .nickname(remote.nickname) + .note(remote.note); final var merged = mergedBuilder.build(); final var matchesRemote = doProtosMatch(merged, remote); if (matchesRemote) { - return remote; + return remoteRecord; } final var matchesLocal = doProtosMatch(merged, local); if (matchesLocal) { - return local; + return localRecord; } - return mergedBuilder.setId(KeyUtils.createRawStorageId()).build(); + return new SignalContactRecord(StorageId.forContact(KeyUtils.createRawStorageId()), mergedBuilder.build()); } @Override @@ -231,7 +220,8 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor update) throws SQLException { final var contactRecord = update.newRecord(); - final var address = getRecipientAddress(contactRecord); + final var contactProto = contactRecord.getProto(); + final var address = getRecipientAddress(contactProto); final var recipientId = account.getRecipientStore().resolveRecipientTrusted(connection, address); final var recipient = account.getRecipientStore().getRecipient(connection, recipientId); @@ -251,95 +241,92 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor