]> nmode's Git Repositories - signal-cli/commitdiff
Update libsignal-service
authorAsamK <asamk@gmx.de>
Sat, 23 Nov 2024 21:29:01 +0000 (22:29 +0100)
committerAsamK <asamk@gmx.de>
Sat, 23 Nov 2024 22:57:23 +0000 (23:57 +0100)
Support for storage encryption v2 and account entropy pool

Fixes #1632

29 files changed:
graalvm-config-dir/reflect-config.json
gradle/libs.versions.toml
lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java
lib/src/main/java/org/asamk/signal/manager/api/Profile.java
lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java
lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java
lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java
lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java
lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java
lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java
lib/src/main/java/org/asamk/signal/manager/helper/PreKeyHelper.java
lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java
lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java
lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java
lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java
lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java
lib/src/main/java/org/asamk/signal/manager/jobs/SyncStorageJob.java
lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java
lib/src/main/java/org/asamk/signal/manager/syncStorage/AccountRecordProcessor.java
lib/src/main/java/org/asamk/signal/manager/syncStorage/ContactRecordProcessor.java
lib/src/main/java/org/asamk/signal/manager/syncStorage/GroupV1RecordProcessor.java
lib/src/main/java/org/asamk/signal/manager/syncStorage/GroupV2RecordProcessor.java
lib/src/main/java/org/asamk/signal/manager/syncStorage/StorageSyncModels.java
lib/src/main/java/org/asamk/signal/manager/syncStorage/StorageSyncValidations.java
lib/src/main/java/org/asamk/signal/manager/syncStorage/WriteOperationResult.java
lib/src/main/java/org/asamk/signal/manager/util/KeyUtils.java
lib/src/main/java/org/asamk/signal/manager/util/NumberVerificationUtils.java
lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java
lib/src/main/java/org/asamk/signal/manager/util/Utils.java

index abe02ac423e154f99fd3615999d753908ef25327..230ede2f9226e4c839e01cec3d5be78a37525b5f 100644 (file)
   "name":"com.fasterxml.jackson.databind.ext.Java7SupportImpl",
   "methods":[{"name":"<init>","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,
 {
   "name":"java.io.FilePermission"
 },
+{
+  "name":"java.io.OutputStream"
+},
 {
   "name":"java.io.Serializable",
-  "allDeclaredMethods":true
+  "allDeclaredFields":true,
+  "allDeclaredMethods":true,
+  "allDeclaredClasses":true
 },
 {
   "name":"java.lang.Boolean",
 {
   "name":"kotlin.String"
 },
+{
+  "name":"kotlin.Unit"
+},
 {
   "name":"kotlin.collections.AbstractCollection",
   "allDeclaredFields":true,
 {
   "name":"long[]"
 },
+{
+  "name":"okio.BufferedSink"
+},
 {
   "name":"okio.ByteString"
 },
   "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",
   "allDeclaredFields":true,
   "queryAllDeclaredMethods":true,
   "queryAllDeclaredConstructors":true,
-  "methods":[{"name":"<init>","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":"<init>","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":"<init>","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":"<init>","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":"<init>","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":"<init>","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":"<init>","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",
   "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",
 {
   "name":"org.whispersystems.signalservice.api.groupsv2.TemporalCredential[]"
 },
+{
+  "name":"org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse",
+  "allDeclaredFields":true,
+  "queryAllDeclaredMethods":true,
+  "queryAllDeclaredConstructors":true,
+  "methods":[{"name":"<init>","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"<init>","parameterTypes":["java.lang.String","java.lang.String","int","kotlin.jvm.internal.DefaultConstructorMarker"] }]
+},
 {
   "name":"org.whispersystems.signalservice.api.messages.calls.HangupMessage",
   "allDeclaredFields":true,
 },
 {
   "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",
 },
 {
   "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",
 },
 {
   "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",
   "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
index d7d56d71c0cedf6bdd09b4e192bd22b7e3cb29a3..e52a4ae7a227802ba47762c60e1ae12f9dfed736 100644 (file)
@@ -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"
index 5598d438523503b74711a2fe3dc18849817dac18..d8438f5dce0ee0336dd50fe3171f1b64c9b85c75 100644 (file)
@@ -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());
             }
         }
 
index 5b1ecf3a07e76e5b6b251cfe5884f8c0a4c4f388..acbaac11c40747f396506f1138ec5c3ca6480a11 100644 (file)
@@ -161,7 +161,8 @@ public class Profile {
     }
 
     public enum Capability {
-        storage;
+        storage,
+        storageServiceEncryptionV2Capability;
 
         public static Capability valueOfOrNull(String value) {
             try {
index f7859dae5ceb82c74749e043971815a0bdcc590d..8e8443927b1037b823c2c8ea4c4d564be56e7d2e 100644 (file)
@@ -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<Interceptor> interceptors
@@ -71,7 +71,8 @@ class LiveConfig {
                 proxy,
                 zkGroupServerPublicParams,
                 genericServerPublicParams,
-                backupServerPublicParams);
+                backupServerPublicParams,
+                false);
     }
 
     static ECPublicKey getUnidentifiedSenderTrustRoot() {
index 482e0f60c71928154ec2a7dc073018c0e08f58ef..24365cfe8f84ca6cd4ea72f01bf1df647eeac00c 100644 (file)
@@ -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(
index a3212c077f8596b024f7b28e7286636138073f2f..741afa169491d10954596c832baf4e296ff7fe0c 100644 (file)
@@ -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<Interceptor> interceptors
@@ -71,7 +71,8 @@ class StagingConfig {
                 proxy,
                 zkGroupServerPublicParams,
                 genericServerPublicParams,
-                backupServerPublicParams);
+                backupServerPublicParams,
+                false);
     }
 
     static ECPublicKey getUnidentifiedSenderTrustRoot() {
index d5e8581e93554fb946293304d4d1ebcbd8382948..3eb2051d4e9d4b5f3c82c2a109b7de0366ca0c53 100644 (file)
@@ -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());
     }
index 05bc876a9802b6f65df8a17ef356ba8bc9b70f10..a3330d5c07f0fce5124d92089940665643981fba 100644 (file)
@@ -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());
index 376a671b965b4b567cbde89faa7f5030b445f8a1..5deffb3e22e3851afaaec90966684cd56e09e58f 100644 (file)
@@ -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());
             }
         }
index 339425c1dfc5f56029bc1b6085ba2fd637f1146c..b17a9206ca68b074133c818ebe9d52dc3f57f50c 100644 (file)
@@ -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.");
index 184eac6b26711af917ea941092e478d6d04fbe33..0905bb30aaeac2054cf6f83e42370fb6f5ad7ab5 100644 (file)
@@ -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()));
index 6225b982146da48a71727bdc7c1f6e31e121537f..5da66b7e40fc554f78294bdd648a0768d6a75953 100644 (file)
@@ -239,7 +239,6 @@ public class RecipientHelper {
                             newNumbers,
                             account.getRecipientStore().getServiceIdToProfileKeyMap(),
                             token,
-                            dependencies.getServiceEnvironmentConfig().cdsiMrenclave(),
                             null,
                             dependencies.getLibSignalNetwork(),
                             newToken -> {
index 8c6b3c528ed3a1dea870ad379ae0b7c69e530f09..740c0b5e901078795b8a46060d520cff30e50b10 100644 (file)
@@ -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<SignalStorageManifest> 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<SignalStorageRecord>();
         final Map<RecipientId, StorageId> 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<SignalStorageManifest> 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<SignalStorageRecord> getSignalStorageRecords(
             final StorageKey storageKey,
+            final SignalStorageManifest manifest,
             final List<StorageId> storageIds
     ) throws IOException {
-        List<SignalStorageRecord> 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<StorageId> getAllLocalStorageIds(final Connection connection) throws SQLException {
@@ -436,12 +516,10 @@ public class StorageHelper {
             final Connection connection,
             final List<StorageId> storageIds
     ) throws SQLException {
-        final var records = new ArrayList<SignalStorageRecord>();
+        final var records = new ArrayList<SignalStorageRecord>(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());
             }
         }
 
index 70427f001118d14b1ad40dce359360eae9a1284e..39726df8c8fc4fb946439ad09aca1381aea78448 100644 (file)
@@ -247,10 +247,14 @@ public class SyncHelper {
     }
 
     public SendMessageResult sendBlockedList() {
-        var addresses = new ArrayList<SignalServiceAddress>();
+        var addresses = new ArrayList<BlockedListMessage.Individual>();
         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<byte[]>();
@@ -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());
index 7ee2b39e94e4eabee7bb6ff9f818b8671b1e2442..90e3b1595e0ad4389b3b6c72cf04a6238a6e5c1f 100644 (file)
@@ -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()),
index 85cfbe2c2170bf95c24b96138473af6861f17210..d695c7b1edcad668b27d3d61ac70ffc8db63469c 100644 (file)
@@ -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);
         }
index 5c8a553071cfa92fd5a053c1c3181c2bc6aa76c4..2529ad4ac8deabb2a97255286c226302a72afcf4 100644 (file)
@@ -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
index 18dd1869ae6f7d7dba84f5caf7ac362b9d247896..b2f7dd2931801d0c306e2f5ccd35e33c6e1b1f75 100644 (file)
@@ -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<Signal
         final var selfRecipientId = account.getSelfRecipientId();
         final var recipient = account.getRecipientStore().getRecipient(connection, selfRecipientId);
         final var storageId = account.getRecipientStore().getSelfStorageId(connection);
-        this.localAccountRecord = StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(),
-                recipient,
-                account.getUsernameLink(),
-                storageId.getRaw()).getAccount().get();
+        this.localAccountRecord = new SignalAccountRecord(storageId,
+                StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(),
+                        recipient,
+                        account.getUsernameLink()));
     }
 
     @Override
@@ -60,99 +64,73 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
     }
 
     @Override
-    protected SignalAccountRecord merge(SignalAccountRecord remote, SignalAccountRecord local) {
+    protected SignalAccountRecord merge(SignalAccountRecord remoteRecord, SignalAccountRecord localRecord) {
+        final var remote = remoteRecord.getProto();
+        final var local = localRecord.getProto();
         String givenName;
         String familyName;
-        if (remote.getGivenName().isPresent() || remote.getFamilyName().isPresent()) {
-            givenName = remote.getGivenName().orElse("");
-            familyName = remote.getFamilyName().orElse("");
+        if (!remote.givenName.isEmpty() || !remote.familyName.isEmpty()) {
+            givenName = remote.givenName;
+            familyName = remote.familyName;
         } else {
-            givenName = local.getGivenName().orElse("");
-            familyName = local.getFamilyName().orElse("");
+            givenName = local.givenName;
+            familyName = local.familyName;
         }
 
-        final var payments = remote.getPayments().getEntropy().isPresent() ? remote.getPayments() : local.getPayments();
-        final var subscriber = remote.getSubscriber().getId().isPresent()
-                ? remote.getSubscriber()
-                : local.getSubscriber();
-        final var storyViewReceiptsState = remote.getStoryViewReceiptsState() == OptionalBool.UNSET
-                ? local.getStoryViewReceiptsState()
-                : remote.getStoryViewReceiptsState();
-        final var unknownFields = remote.serializeUnknownFields();
-        final var avatarUrlPath = OptionalUtil.or(remote.getAvatarUrlPath(), local.getAvatarUrlPath()).orElse("");
-        final var profileKey = OptionalUtil.or(remote.getProfileKey(), local.getProfileKey()).orElse(null);
-        final var noteToSelfArchived = remote.isNoteToSelfArchived();
-        final var noteToSelfForcedUnread = remote.isNoteToSelfForcedUnread();
-        final var readReceipts = remote.isReadReceiptsEnabled();
-        final var typingIndicators = remote.isTypingIndicatorsEnabled();
-        final var sealedSenderIndicators = remote.isSealedSenderIndicatorsEnabled();
-        final var linkPreviews = remote.isLinkPreviewsEnabled();
-        final var unlisted = remote.isPhoneNumberUnlisted();
-        final var pinnedConversations = remote.getPinnedConversations();
-        final var phoneNumberSharingMode = remote.getPhoneNumberSharingMode();
-        final var preferContactAvatars = remote.isPreferContactAvatars();
-        final var universalExpireTimer = remote.getUniversalExpireTimer();
-        final var e164 = account.isPrimaryDevice() ? local.getE164() : remote.getE164();
-        final var defaultReactions = !remote.getDefaultReactions().isEmpty()
-                ? remote.getDefaultReactions()
-                : local.getDefaultReactions();
-        final var displayBadgesOnProfile = remote.isDisplayBadgesOnProfile();
-        final var subscriptionManuallyCancelled = remote.isSubscriptionManuallyCancelled();
-        final var keepMutedChatsArchived = remote.isKeepMutedChatsArchived();
-        final var hasSetMyStoriesPrivacy = remote.hasSetMyStoriesPrivacy();
-        final var hasViewedOnboardingStory = remote.hasViewedOnboardingStory() || local.hasViewedOnboardingStory();
-        final var storiesDisabled = remote.isStoriesDisabled();
-        final var hasSeenGroupStoryEducation = remote.hasSeenGroupStoryEducationSheet()
-                || local.hasSeenGroupStoryEducationSheet();
-        boolean hasSeenUsernameOnboarding = remote.hasCompletedUsernameOnboarding()
-                || local.hasCompletedUsernameOnboarding();
-        final var username = remote.getUsername();
-        final var usernameLink = remote.getUsernameLink();
-
-        final var mergedBuilder = new SignalAccountRecord.Builder(remote.getId().getRaw(), unknownFields).setGivenName(
-                        givenName)
-                .setFamilyName(familyName)
-                .setAvatarUrlPath(avatarUrlPath)
-                .setProfileKey(profileKey)
-                .setNoteToSelfArchived(noteToSelfArchived)
-                .setNoteToSelfForcedUnread(noteToSelfForcedUnread)
-                .setReadReceiptsEnabled(readReceipts)
-                .setTypingIndicatorsEnabled(typingIndicators)
-                .setSealedSenderIndicatorsEnabled(sealedSenderIndicators)
-                .setLinkPreviewsEnabled(linkPreviews)
-                .setUnlistedPhoneNumber(unlisted)
-                .setPhoneNumberSharingMode(phoneNumberSharingMode)
-                .setPinnedConversations(pinnedConversations)
-                .setPreferContactAvatars(preferContactAvatars)
-                .setPayments(payments.isEnabled(), payments.getEntropy().orElse(null))
-                .setUniversalExpireTimer(universalExpireTimer)
-                .setDefaultReactions(defaultReactions)
-                .setSubscriber(subscriber)
-                .setDisplayBadgesOnProfile(displayBadgesOnProfile)
-                .setSubscriptionManuallyCancelled(subscriptionManuallyCancelled)
-                .setKeepMutedChatsArchived(keepMutedChatsArchived)
-                .setHasSetMyStoriesPrivacy(hasSetMyStoriesPrivacy)
-                .setHasViewedOnboardingStory(hasViewedOnboardingStory)
-                .setStoriesDisabled(storiesDisabled)
-                .setHasSeenGroupStoryEducationSheet(hasSeenGroupStoryEducation)
-                .setHasCompletedUsernameOnboarding(hasSeenUsernameOnboarding)
-                .setStoryViewReceiptsState(storyViewReceiptsState)
-                .setUsername(username)
-                .setUsernameLink(usernameLink)
-                .setE164(e164);
+        final var mergedBuilder = SignalAccountRecord.Companion.newBuilder(remote.unknownFields().toByteArray())
+                .givenName(givenName)
+                .familyName(familyName)
+                .avatarUrlPath(firstNonEmpty(remote.avatarUrlPath, local.avatarUrlPath))
+                .profileKey(firstNonEmpty(remote.profileKey, local.profileKey))
+                .noteToSelfArchived(remote.noteToSelfArchived)
+                .noteToSelfMarkedUnread(remote.noteToSelfMarkedUnread)
+                .readReceipts(remote.readReceipts)
+                .typingIndicators(remote.typingIndicators)
+                .sealedSenderIndicators(remote.sealedSenderIndicators)
+                .linkPreviews(remote.linkPreviews)
+                .unlistedPhoneNumber(remote.unlistedPhoneNumber)
+                .phoneNumberSharingMode(remote.phoneNumberSharingMode)
+                .pinnedConversations(remote.pinnedConversations)
+                .preferContactAvatars(remote.preferContactAvatars)
+                .universalExpireTimer(remote.universalExpireTimer)
+                .preferredReactionEmoji(firstNonEmpty(remote.preferredReactionEmoji, local.preferredReactionEmoji))
+                .subscriberId(firstNonEmpty(remote.subscriberId, local.subscriberId))
+                .subscriberCurrencyCode(firstNonEmpty(remote.subscriberCurrencyCode, local.subscriberCurrencyCode))
+                .backupsSubscriberId(firstNonEmpty(remote.backupsSubscriberId, local.backupsSubscriberId))
+                .backupsSubscriberCurrencyCode(firstNonEmpty(remote.backupsSubscriberCurrencyCode,
+                        local.backupsSubscriberCurrencyCode))
+                .displayBadgesOnProfile(remote.displayBadgesOnProfile)
+                .subscriptionManuallyCancelled(remote.subscriptionManuallyCancelled)
+                .keepMutedChatsArchived(remote.keepMutedChatsArchived)
+                .hasSetMyStoriesPrivacy(remote.hasSetMyStoriesPrivacy)
+                .hasViewedOnboardingStory(remote.hasViewedOnboardingStory || local.hasViewedOnboardingStory)
+                .storiesDisabled(remote.storiesDisabled)
+                .hasSeenGroupStoryEducationSheet(remote.hasSeenGroupStoryEducationSheet
+                        || local.hasSeenGroupStoryEducationSheet)
+                .hasCompletedUsernameOnboarding(remote.hasCompletedUsernameOnboarding
+                        || local.hasCompletedUsernameOnboarding)
+                .storyViewReceiptsEnabled(remote.storyViewReceiptsEnabled == OptionalBool.UNSET
+                        ? local.storyViewReceiptsEnabled
+                        : remote.storyViewReceiptsEnabled)
+                .username(remote.username)
+                .usernameLink(remote.usernameLink)
+                .e164(account.isPrimaryDevice() ? local.e164 : remote.e164);
+        if (firstNonNull(remote.payments, local.payments) != null) {
+            mergedBuilder.payments(firstNonNull(remote.payments, local.payments));
+        }
         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 SignalAccountRecord(StorageId.forAccount(KeyUtils.createRawStorageId()), mergedBuilder.build());
     }
 
     @Override
@@ -164,56 +142,55 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
     @Override
     protected void updateLocal(StorageRecordUpdate<SignalAccountRecord> 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<Signal
         return 0;
     }
 
-    private static boolean doProtosMatch(SignalAccountRecord merged, SignalAccountRecord other) {
-        return Arrays.equals(merged.toProto().encode(), other.toProto().encode());
+    private static boolean doProtosMatch(AccountRecord merged, AccountRecord other) {
+        return Arrays.equals(merged.encode(), other.encode());
     }
 }
index 3fa8598f6a6375b11678782321317add188a5253..ee4b04fe45cddb47501e9b157681fa817c9dca14 100644 (file)
@@ -17,7 +17,8 @@ import org.slf4j.LoggerFactory;
 import org.whispersystems.signalservice.api.push.ServiceId.ACI;
 import org.whispersystems.signalservice.api.push.ServiceId.PNI;
 import org.whispersystems.signalservice.api.storage.SignalContactRecord;
-import org.whispersystems.signalservice.api.util.OptionalUtil;
+import org.whispersystems.signalservice.api.storage.StorageId;
+import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
 import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
 
 import java.sql.Connection;
@@ -27,6 +28,11 @@ import java.util.Objects;
 import java.util.Optional;
 import java.util.regex.Pattern;
 
+import okio.ByteString;
+
+import static org.asamk.signal.manager.util.Utils.firstNonEmpty;
+import static org.asamk.signal.manager.util.Utils.nullIfEmpty;
+
 public class ContactRecordProcessor extends DefaultStorageRecordProcessor<SignalContactRecord> {
 
     private static final Logger logger = LoggerFactory.getLogger(ContactRecordProcessor.class);
@@ -55,20 +61,24 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
      * - You can't have a contact record for yourself. That should be an account record.
      */
     @Override
-    protected boolean isInvalid(SignalContactRecord remote) {
-        boolean hasAci = remote.getAci().isPresent() && remote.getAci().get().isValid();
-        boolean hasPni = remote.getPni().isPresent() && remote.getPni().get().isValid();
+    protected boolean isInvalid(SignalContactRecord remoteRecord) {
+        final var remote = remoteRecord.getProto();
+        final var aci = ACI.parseOrNull(remote.aci);
+        final var pni = PNI.parseOrNull(remote.pni);
+        final var e164 = nullIfEmpty(remote.e164);
+        boolean hasAci = aci != null && aci.isValid();
+        boolean hasPni = pni != null && pni.isValid();
 
         if (!hasAci && !hasPni) {
             logger.debug("Found a ContactRecord with neither an ACI nor a PNI -- marking as invalid.");
             return true;
-        } else if (selfAci != null && selfAci.equals(remote.getAci().orElse(null)) || (
-                selfPni != null && selfPni.equals(remote.getPni().orElse(null))
-        ) || (selfNumber != null && selfNumber.equals(remote.getNumber().orElse(null)))) {
+        } else if (selfAci != null && selfAci.equals(aci) || (
+                selfPni != null && selfPni.equals(pni)
+        ) || (selfNumber != null && selfNumber.equals(e164))) {
             logger.debug("Found a ContactRecord for ourselves -- marking as invalid.");
             return true;
-        } else if (remote.getNumber().isPresent() && !isValidE164(remote.getNumber().get())) {
-            logger.debug("Found a record with an invalid E164. Marking as invalid.");
+        } else if (e164 != null && !isValidE164(e164)) {
+            logger.debug("Found a record with an invalid E164 ({}). Marking as invalid.", e164);
             return true;
         } else {
             return false;
@@ -77,7 +87,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
 
     @Override
     protected Optional<SignalContactRecord> 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<Signal
         final var identity = account.getIdentityKeyStore().getIdentityInfo(connection, identifier);
         final var storageId = account.getRecipientStore().getStorageId(connection, recipientId);
 
-        return Optional.of(StorageSyncModels.localToRemoteRecord(recipient, identity, storageId.getRaw())
-                .getContact()
-                .get());
+        return Optional.of(new SignalContactRecord(storageId,
+                StorageSyncModels.localToRemoteRecord(recipient, identity)));
     }
 
     @Override
-    protected SignalContactRecord merge(SignalContactRecord remote, SignalContactRecord local) {
+    protected SignalContactRecord merge(SignalContactRecord remoteRecord, SignalContactRecord localRecord) {
+        final var remote = remoteRecord.getProto();
+        final var local = localRecord.getProto();
+
         String profileGivenName;
         String profileFamilyName;
-        if (remote.getProfileGivenName().isPresent() || remote.getProfileFamilyName().isPresent()) {
-            profileGivenName = remote.getProfileGivenName().orElse("");
-            profileFamilyName = remote.getProfileFamilyName().orElse("");
+        if (!remote.givenName.isEmpty() || !remote.familyName.isEmpty()) {
+            profileGivenName = remote.givenName;
+            profileFamilyName = remote.familyName;
         } else {
-            profileGivenName = local.getProfileGivenName().orElse("");
-            profileFamilyName = local.getProfileFamilyName().orElse("");
+            profileGivenName = local.givenName;
+            profileFamilyName = local.familyName;
         }
 
         IdentityState identityState;
-        byte[] identityKey;
-        if (remote.getIdentityKey().isPresent() && (
-                remote.getIdentityState() != local.getIdentityState()
-                        || local.getIdentityKey().isEmpty()
-                        || !account.isPrimaryDevice()
+        ByteString identityKey;
+        if (remote.identityKey.size() > 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<Signal
     @Override
     protected void updateLocal(StorageRecordUpdate<SignalContactRecord> 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<Signal
         final var contactNickGivenName = contact == null ? null : contact.nickNameGivenName();
         final var contactNickFamilyName = contact == null ? null : contact.nickNameFamilyName();
         final var contactNote = contact == null ? null : contact.note();
-        if (blocked != contactRecord.isBlocked()
-                || profileShared != contactRecord.isProfileSharingEnabled()
-                || archived != contactRecord.isArchived()
-                || hidden != contactRecord.isHidden()
-                || hideStory != contactRecord.shouldHideStory()
-                || muteUntil != contactRecord.getMuteUntil()
-                || unregisteredTimestamp != contactRecord.getUnregisteredTimestamp()
-                || !Objects.equals(contactRecord.getSystemGivenName().orElse(null), contactGivenName)
-                || !Objects.equals(contactRecord.getSystemFamilyName().orElse(null), contactFamilyName)
-                || !Objects.equals(contactRecord.getSystemNickname().orElse(null), contactNickName)
-                || !Objects.equals(contactRecord.getNicknameGivenName().orElse(null), contactNickGivenName)
-                || !Objects.equals(contactRecord.getNicknameFamilyName().orElse(null), contactNickFamilyName)
-                || !Objects.equals(contactRecord.getNote().orElse(null), contactNote)) {
+        if (blocked != contactProto.blocked
+                || profileShared != contactProto.whitelisted
+                || archived != contactProto.archived
+                || hidden != contactProto.hidden
+                || hideStory != contactProto.hideStory
+                || muteUntil != contactProto.mutedUntilTimestamp
+                || unregisteredTimestamp != contactProto.unregisteredAtTimestamp
+                || !Objects.equals(nullIfEmpty(contactProto.systemGivenName), contactGivenName)
+                || !Objects.equals(nullIfEmpty(contactProto.systemFamilyName), contactFamilyName)
+                || !Objects.equals(nullIfEmpty(contactProto.systemNickname), contactNickName)
+                || !Objects.equals(nullIfEmpty(contactProto.nickname == null ? "" : contactProto.nickname.given),
+                contactNickGivenName)
+                || !Objects.equals(nullIfEmpty(contactProto.nickname == null ? "" : contactProto.nickname.family),
+                contactNickFamilyName)
+                || !Objects.equals(nullIfEmpty(contactProto.note), contactNote)) {
             logger.debug("Storing new or updated contact {}", recipientId);
             final var contactBuilder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
-            final var newContact = contactBuilder.withIsBlocked(contactRecord.isBlocked())
-                    .withIsProfileSharingEnabled(contactRecord.isProfileSharingEnabled())
-                    .withIsArchived(contactRecord.isArchived())
-                    .withIsHidden(contactRecord.isHidden())
-                    .withMuteUntil(contactRecord.getMuteUntil())
-                    .withHideStory(contactRecord.shouldHideStory())
-                    .withGivenName(contactRecord.getSystemGivenName().orElse(null))
-                    .withFamilyName(contactRecord.getSystemFamilyName().orElse(null))
-                    .withNickName(contactRecord.getSystemNickname().orElse(null))
-                    .withNickNameGivenName(contactRecord.getNicknameGivenName().orElse(null))
-                    .withNickNameFamilyName(contactRecord.getNicknameFamilyName().orElse(null))
-                    .withNote(contactRecord.getNote().orElse(null))
-                    .withUnregisteredTimestamp(contactRecord.getUnregisteredTimestamp() == 0
-                            ? null
-                            : contactRecord.getUnregisteredTimestamp());
+            final var newContact = contactBuilder.withIsBlocked(contactProto.blocked)
+                    .withIsProfileSharingEnabled(contactProto.whitelisted)
+                    .withIsArchived(contactProto.archived)
+                    .withIsHidden(contactProto.hidden)
+                    .withMuteUntil(contactProto.mutedUntilTimestamp)
+                    .withHideStory(contactProto.hideStory)
+                    .withGivenName(nullIfEmpty(contactProto.systemGivenName))
+                    .withFamilyName(nullIfEmpty(contactProto.systemFamilyName))
+                    .withNickName(nullIfEmpty(contactProto.systemNickname))
+                    .withNickNameGivenName(nullIfEmpty(contactProto.givenName))
+                    .withNickNameFamilyName(nullIfEmpty(contactProto.familyName))
+                    .withNote(nullIfEmpty(contactProto.note))
+                    .withUnregisteredTimestamp(contactProto.unregisteredAtTimestamp);
             account.getRecipientStore().storeContact(connection, recipientId, newContact.build());
         }
 
         final var profile = recipient.getProfile();
         final var profileGivenName = profile == null ? null : profile.getGivenName();
         final var profileFamilyName = profile == null ? null : profile.getFamilyName();
-        if (!Objects.equals(contactRecord.getProfileGivenName().orElse(null), profileGivenName) || !Objects.equals(
-                contactRecord.getProfileFamilyName().orElse(null),
-                profileFamilyName)) {
+        if (!Objects.equals(nullIfEmpty(contactProto.givenName), profileGivenName) || !Objects.equals(nullIfEmpty(
+                contactProto.familyName), profileFamilyName)) {
             final var profileBuilder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile);
-            final var newProfile = profileBuilder.withGivenName(contactRecord.getProfileGivenName().orElse(null))
-                    .withFamilyName(contactRecord.getProfileFamilyName().orElse(null))
+            final var newProfile = profileBuilder.withGivenName(nullIfEmpty(contactProto.givenName))
+                    .withFamilyName(nullIfEmpty(contactProto.familyName))
                     .build();
             account.getRecipientStore().storeProfile(connection, recipientId, newProfile);
         }
-        if (contactRecord.getProfileKey().isPresent()) {
+        if (contactProto.profileKey.size() > 0) {
             try {
                 logger.trace("Storing profile key {}", recipientId);
-                final var profileKey = new ProfileKey(contactRecord.getProfileKey().get());
+                final var profileKey = new ProfileKey(contactProto.profileKey.toByteArray());
                 account.getRecipientStore().storeProfileKey(connection, recipientId, profileKey);
             } catch (InvalidInputException e) {
                 logger.warn("Received invalid contact profile key from storage");
             }
         }
-        if (contactRecord.getIdentityKey().isPresent() && contactRecord.getAci().isPresent()) {
+        if (contactProto.identityKey.size() > 0 && address.aci().isPresent()) {
             try {
                 logger.trace("Storing identity key {}", recipientId);
-                final var identityKey = new IdentityKey(contactRecord.getIdentityKey().get());
-                account.getIdentityKeyStore()
-                        .saveIdentity(connection, contactRecord.getAci().orElse(null), identityKey);
+                final var identityKey = new IdentityKey(contactProto.identityKey.toByteArray());
+                account.getIdentityKeyStore().saveIdentity(connection, address.aci().get(), identityKey);
 
-                final var trustLevel = StorageSyncModels.remoteToLocal(contactRecord.getIdentityState());
+                final var trustLevel = StorageSyncModels.remoteToLocal(contactProto.identityState);
                 if (trustLevel != null) {
                     account.getIdentityKeyStore()
-                            .setIdentityTrustLevel(connection,
-                                    contactRecord.getAci().orElse(null),
-                                    identityKey,
-                                    trustLevel);
+                            .setIdentityTrustLevel(connection, address.aci().get(), identityKey, trustLevel);
                 }
             } catch (InvalidKeyException e) {
                 logger.warn("Received invalid contact identity key from storage");
             }
         }
         account.getRecipientStore()
-                .storeStorageRecord(connection, recipientId, contactRecord.getId(), contactRecord.toProto().encode());
+                .storeStorageRecord(connection, recipientId, contactRecord.getId(), contactProto.encode());
     }
 
-    private static RecipientAddress getRecipientAddress(final SignalContactRecord contactRecord) {
-        return new RecipientAddress(contactRecord.getAci().orElse(null),
-                contactRecord.getPni().orElse(null),
-                contactRecord.getNumber().orElse(null),
-                contactRecord.getUsername().orElse(null));
+    private static RecipientAddress getRecipientAddress(final ContactRecord contactRecord) {
+        return new RecipientAddress(ACI.parseOrNull(contactRecord.aci),
+                PNI.parseOrNull(contactRecord.pni),
+                nullIfEmpty(contactRecord.e164),
+                nullIfEmpty(contactRecord.username));
     }
 
     @Override
-    public int compare(SignalContactRecord lhs, SignalContactRecord rhs) {
-        if ((lhs.getAci().isPresent() && Objects.equals(lhs.getAci(), rhs.getAci())) || (
-                lhs.getNumber().isPresent() && Objects.equals(lhs.getNumber(), rhs.getNumber())
-        ) || (lhs.getPni().isPresent() && Objects.equals(lhs.getPni(), rhs.getPni()))) {
+    public int compare(SignalContactRecord lhsRecord, SignalContactRecord rhsRecord) {
+        final var lhs = lhsRecord.getProto();
+        final var rhs = rhsRecord.getProto();
+        if ((!lhs.aci.isEmpty() && Objects.equals(lhs.aci, rhs.aci)) || (
+                !lhs.e164.isEmpty() && Objects.equals(lhs.e164, rhs.e164)
+        ) || (!lhs.pni.isEmpty() && Objects.equals(lhs.pni, rhs.pni))) {
             return 0;
         } else {
             return 1;
@@ -350,7 +337,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
         return E164_PATTERN.matcher(value).matches();
     }
 
-    private static boolean doProtosMatch(SignalContactRecord merged, SignalContactRecord other) {
-        return Arrays.equals(merged.toProto().encode(), other.toProto().encode());
+    private static boolean doProtosMatch(ContactRecord merged, ContactRecord other) {
+        return Arrays.equals(merged.encode(), other.encode());
     }
 }
index 14d3c882e9fd4316c303c18bdf146000ba709279..22d18c8b610f569206f8dd2c5f50923d5f2cba2e 100644 (file)
@@ -8,6 +8,8 @@ import org.asamk.signal.manager.util.KeyUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
+import org.whispersystems.signalservice.api.storage.StorageId;
+import org.whispersystems.signalservice.internal.storage.protos.GroupV1Record;
 
 import java.sql.Connection;
 import java.sql.SQLException;
@@ -36,7 +38,7 @@ public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor<
     @Override
     protected boolean isInvalid(SignalGroupV1Record remote) throws SQLException {
         try {
-            final var id = GroupId.unknownVersion(remote.getGroupId());
+            final var id = GroupId.unknownVersion(remote.getProto().id.toByteArray());
             if (!(id instanceof GroupIdV1)) {
                 return true;
             }
@@ -56,7 +58,7 @@ public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor<
 
     @Override
     protected Optional<SignalGroupV1Record> getMatching(SignalGroupV1Record remote) throws SQLException {
-        final var id = GroupId.v1(remote.getGroupId());
+        final var id = GroupId.v1(remote.getProto().id.toByteArray());
         final var group = account.getGroupStore().getGroup(connection, id);
 
         if (group == null) {
@@ -64,39 +66,35 @@ public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor<
         }
 
         final var storageId = account.getGroupStore().getGroupStorageId(connection, id);
-        return Optional.of(StorageSyncModels.localToRemoteRecord(group, storageId.getRaw()).getGroupV1().get());
+        return Optional.of(new SignalGroupV1Record(storageId, StorageSyncModels.localToRemoteRecord(group)));
     }
 
     @Override
-    protected SignalGroupV1Record merge(SignalGroupV1Record remote, SignalGroupV1Record local) {
-        final var unknownFields = remote.serializeUnknownFields();
-        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 mergedBuilder = new SignalGroupV1Record.Builder(remote.getId().getRaw(),
-                remote.getGroupId(),
-                unknownFields).setBlocked(blocked)
-                .setProfileSharingEnabled(profileSharing)
-                .setForcedUnread(forcedUnread)
-                .setMuteUntil(muteUntil)
-                .setArchived(archived);
+    protected SignalGroupV1Record merge(SignalGroupV1Record remoteRecord, SignalGroupV1Record localRecord) {
+        final var remote = remoteRecord.getProto();
+        final var local = localRecord.getProto();
+
+        final var mergedBuilder = SignalGroupV1Record.Companion.newBuilder(remote.unknownFields().toByteArray())
+                .id(remote.id)
+                .blocked(remote.blocked)
+                .whitelisted(remote.whitelisted)
+                .markedUnread(remote.markedUnread)
+                .mutedUntilTimestamp(remote.mutedUntilTimestamp)
+                .archived(remote.archived);
 
         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 SignalGroupV1Record(StorageId.forGroupV1(KeyUtils.createRawStorageId()), mergedBuilder.build());
     }
 
     @Override
@@ -110,30 +108,28 @@ public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor<
     @Override
     protected void updateLocal(StorageRecordUpdate<SignalGroupV1Record> update) throws SQLException {
         final var groupV1Record = update.newRecord();
-        final var groupIdV1 = GroupId.v1(groupV1Record.getGroupId());
+        final var groupV1Proto = groupV1Record.getProto();
+        final var groupIdV1 = GroupId.v1(groupV1Proto.id.toByteArray());
 
         final var group = account.getGroupStore().getOrCreateGroupV1(connection, groupIdV1);
         if (group != null) {
-            group.setBlocked(groupV1Record.isBlocked());
+            group.setBlocked(groupV1Proto.blocked);
             account.getGroupStore().updateGroup(connection, group);
             account.getGroupStore()
-                    .storeStorageRecord(connection,
-                            group.getGroupId(),
-                            groupV1Record.getId(),
-                            groupV1Record.toProto().encode());
+                    .storeStorageRecord(connection, group.getGroupId(), groupV1Record.getId(), groupV1Proto.encode());
         }
     }
 
     @Override
     public int compare(SignalGroupV1Record lhs, SignalGroupV1Record rhs) {
-        if (Arrays.equals(lhs.getGroupId(), rhs.getGroupId())) {
+        if (lhs.getProto().id.equals(rhs.getProto().id)) {
             return 0;
         } else {
             return 1;
         }
     }
 
-    private static boolean doProtosMatch(SignalGroupV1Record merged, SignalGroupV1Record other) {
-        return Arrays.equals(merged.toProto().encode(), other.toProto().encode());
+    private static boolean doProtosMatch(GroupV1Record merged, GroupV1Record other) {
+        return Arrays.equals(merged.encode(), other.encode());
     }
 }
index 1b1827697ffab6f01da42d890c0ebe7885944231..af77e857cf356da1cd05b182ddfcb6f74574b342 100644 (file)
@@ -3,16 +3,22 @@ package org.asamk.signal.manager.syncStorage;
 import org.asamk.signal.manager.groups.GroupUtils;
 import org.asamk.signal.manager.storage.SignalAccount;
 import org.asamk.signal.manager.util.KeyUtils;
+import org.jetbrains.annotations.NotNull;
+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.storage.SignalGroupV2Record;
+import org.whispersystems.signalservice.api.storage.StorageId;
+import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record;
 
 import java.sql.Connection;
 import java.sql.SQLException;
 import java.util.Arrays;
 import java.util.Optional;
 
+import okio.ByteString;
+
 public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<SignalGroupV2Record> {
 
     private static final Logger logger = LoggerFactory.getLogger(GroupV2RecordProcessor.class);
@@ -26,12 +32,12 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
 
     @Override
     protected boolean isInvalid(SignalGroupV2Record remote) {
-        return remote.getMasterKeyBytes().length != GroupMasterKey.SIZE;
+        return remote.getProto().masterKey.size() != GroupMasterKey.SIZE;
     }
 
     @Override
     protected Optional<SignalGroupV2Record> getMatching(SignalGroupV2Record remote) throws SQLException {
-        final var id = GroupUtils.getGroupIdV2(remote.getMasterKeyOrThrow());
+        final var id = GroupUtils.getGroupIdV2(getGroupMasterKeyOrThrow(remote.getProto().masterKey));
         final var group = account.getGroupStore().getGroup(connection, id);
 
         if (group == null) {
@@ -39,44 +45,37 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
         }
 
         final var storageId = account.getGroupStore().getGroupStorageId(connection, id);
-        return Optional.of(StorageSyncModels.localToRemoteRecord(group, storageId.getRaw()).getGroupV2().get());
+        return Optional.of(new SignalGroupV2Record(storageId, StorageSyncModels.localToRemoteRecord(group)));
     }
 
     @Override
-    protected SignalGroupV2Record merge(SignalGroupV2Record remote, SignalGroupV2Record local) {
-        final var unknownFields = remote.serializeUnknownFields();
-        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 notifyForMentionsWhenMuted = remote.notifyForMentionsWhenMuted();
-        final var hideStory = remote.shouldHideStory();
-        final var storySendMode = remote.getStorySendMode();
-
-        final var mergedBuilder = new SignalGroupV2Record.Builder(remote.getId().getRaw(),
-                remote.getMasterKeyBytes(),
-                unknownFields).setBlocked(blocked)
-                .setProfileSharingEnabled(profileSharing)
-                .setArchived(archived)
-                .setForcedUnread(forcedUnread)
-                .setMuteUntil(muteUntil)
-                .setNotifyForMentionsWhenMuted(notifyForMentionsWhenMuted)
-                .setHideStory(hideStory)
-                .setStorySendMode(storySendMode);
+    protected SignalGroupV2Record merge(SignalGroupV2Record remoteRecord, SignalGroupV2Record localRecord) {
+        final var remote = remoteRecord.getProto();
+        final var local = localRecord.getProto();
+
+        final var mergedBuilder = SignalGroupV2Record.Companion.newBuilder(remote.unknownFields().toByteArray())
+                .masterKey(remote.masterKey)
+                .blocked(remote.blocked)
+                .whitelisted(remote.whitelisted)
+                .archived(remote.archived)
+                .markedUnread(remote.markedUnread)
+                .mutedUntilTimestamp(remote.mutedUntilTimestamp)
+                .dontNotifyForMentionsIfMuted(remote.dontNotifyForMentionsIfMuted)
+                .hideStory(remote.hideStory)
+                .storySendMode(remote.storySendMode);
         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 SignalGroupV2Record(StorageId.forGroupV2(KeyUtils.createRawStorageId()), mergedBuilder.build());
     }
 
     @Override
@@ -88,29 +87,36 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
     @Override
     protected void updateLocal(StorageRecordUpdate<SignalGroupV2Record> update) throws SQLException {
         final var groupV2Record = update.newRecord();
-        final var groupMasterKey = groupV2Record.getMasterKeyOrThrow();
+        final var groupV2Proto = groupV2Record.getProto();
+        final var groupMasterKey = getGroupMasterKeyOrThrow(groupV2Proto.masterKey);
 
         final var group = account.getGroupStore().getGroupOrPartialMigrate(connection, groupMasterKey);
-        group.setBlocked(groupV2Record.isBlocked());
-        group.setProfileSharingEnabled(groupV2Record.isProfileSharingEnabled());
+        group.setBlocked(groupV2Proto.blocked);
+        group.setProfileSharingEnabled(groupV2Proto.whitelisted);
         account.getGroupStore().updateGroup(connection, group);
         account.getGroupStore()
-                .storeStorageRecord(connection,
-                        group.getGroupId(),
-                        groupV2Record.getId(),
-                        groupV2Record.toProto().encode());
+                .storeStorageRecord(connection, group.getGroupId(), groupV2Record.getId(), groupV2Proto.encode());
+    }
+
+    @NotNull
+    private static GroupMasterKey getGroupMasterKeyOrThrow(final ByteString masterKey) {
+        try {
+            return new GroupMasterKey(masterKey.toByteArray());
+        } catch (InvalidInputException e) {
+            throw new AssertionError(e);
+        }
     }
 
     @Override
     public int compare(SignalGroupV2Record lhs, SignalGroupV2Record rhs) {
-        if (Arrays.equals(lhs.getMasterKeyBytes(), rhs.getMasterKeyBytes())) {
+        if (lhs.getProto().masterKey.equals(rhs.getProto().masterKey)) {
             return 0;
         } else {
             return 1;
         }
     }
 
-    private static boolean doProtosMatch(SignalGroupV2Record merged, SignalGroupV2Record other) {
-        return Arrays.equals(merged.toProto().encode(), other.toProto().encode());
+    private static boolean doProtosMatch(GroupV2Record merged, GroupV2Record other) {
+        return Arrays.equals(merged.encode(), other.encode());
     }
 }
index 9c6b7b5359c624bf0d248d51b45f1181114b3ccd..ceea180ce18f3fe1109ec579931b5fe8693934b4 100644 (file)
@@ -7,21 +7,27 @@ import org.asamk.signal.manager.storage.groups.GroupInfoV1;
 import org.asamk.signal.manager.storage.groups.GroupInfoV2;
 import org.asamk.signal.manager.storage.identities.IdentityInfo;
 import org.asamk.signal.manager.storage.recipients.Recipient;
+import org.whispersystems.signalservice.api.push.ServiceId.ACI;
+import org.whispersystems.signalservice.api.push.ServiceId.PNI;
 import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
 import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
 import org.whispersystems.signalservice.api.storage.SignalContactRecord;
 import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
 import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
-import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
 import org.whispersystems.signalservice.api.util.UuidUtil;
 import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
 import org.whispersystems.signalservice.internal.storage.protos.AccountRecord.UsernameLink;
+import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
 import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
+import org.whispersystems.signalservice.internal.storage.protos.GroupV1Record;
+import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record;
 
 import java.util.Optional;
 
 import okio.ByteString;
 
+import static org.signal.core.util.StringExtensionsKt.emptyIfNull;
+
 public final class StorageSyncModels {
 
     private StorageSyncModels() {
@@ -42,104 +48,97 @@ public final class StorageSyncModels {
         };
     }
 
-    public static SignalStorageRecord localToRemoteRecord(
+    public static AccountRecord localToRemoteRecord(
             ConfigurationStore configStore,
             Recipient self,
-            UsernameLinkComponents usernameLinkComponents,
-            byte[] rawStorageId
+            UsernameLinkComponents usernameLinkComponents
     ) {
-        final var builder = new SignalAccountRecord.Builder(rawStorageId, self.getStorageRecord());
+        final var builder = SignalAccountRecord.Companion.newBuilder(self.getStorageRecord());
         if (self.getProfileKey() != null) {
-            builder.setProfileKey(self.getProfileKey().serialize());
+            builder.profileKey(ByteString.of(self.getProfileKey().serialize()));
         }
         if (self.getProfile() != null) {
-            builder.setGivenName(self.getProfile().getGivenName())
-                    .setFamilyName(self.getProfile().getFamilyName())
-                    .setAvatarUrlPath(self.getProfile().getAvatarUrlPath());
+            builder.givenName(emptyIfNull(self.getProfile().getGivenName()))
+                    .familyName(emptyIfNull(self.getProfile().getFamilyName()))
+                    .avatarUrlPath(emptyIfNull(self.getProfile().getAvatarUrlPath()));
         }
-        builder.setTypingIndicatorsEnabled(Optional.ofNullable(configStore.getTypingIndicators()).orElse(true))
-                .setReadReceiptsEnabled(Optional.ofNullable(configStore.getReadReceipts()).orElse(true))
-                .setSealedSenderIndicatorsEnabled(Optional.ofNullable(configStore.getUnidentifiedDeliveryIndicators())
+        builder.typingIndicators(Optional.ofNullable(configStore.getTypingIndicators()).orElse(true))
+                .readReceipts(Optional.ofNullable(configStore.getReadReceipts()).orElse(true))
+                .sealedSenderIndicators(Optional.ofNullable(configStore.getUnidentifiedDeliveryIndicators())
                         .orElse(true))
-                .setLinkPreviewsEnabled(Optional.ofNullable(configStore.getLinkPreviews()).orElse(true))
-                .setUnlistedPhoneNumber(Optional.ofNullable(configStore.getPhoneNumberUnlisted()).orElse(false))
-                .setPhoneNumberSharingMode(Optional.ofNullable(configStore.getPhoneNumberSharingMode())
+                .linkPreviews(Optional.ofNullable(configStore.getLinkPreviews()).orElse(true))
+                .unlistedPhoneNumber(Optional.ofNullable(configStore.getPhoneNumberUnlisted()).orElse(false))
+                .phoneNumberSharingMode(Optional.ofNullable(configStore.getPhoneNumberSharingMode())
                         .map(StorageSyncModels::localToRemote)
                         .orElse(AccountRecord.PhoneNumberSharingMode.UNKNOWN))
-                .setE164(self.getAddress().number().orElse(""))
-                .setUsername(self.getAddress().username().orElse(null));
+                .e164(self.getAddress().number().orElse(""))
+                .username(self.getAddress().username().orElse(""));
         if (usernameLinkComponents != null) {
             final var linkColor = configStore.getUsernameLinkColor();
-            builder.setUsernameLink(new UsernameLink.Builder().entropy(ByteString.of(usernameLinkComponents.getEntropy()))
+            builder.usernameLink(new UsernameLink.Builder().entropy(ByteString.of(usernameLinkComponents.getEntropy()))
                     .serverId(UuidUtil.toByteString(usernameLinkComponents.getServerId()))
                     .color(linkColor == null ? UsernameLink.Color.UNKNOWN : UsernameLink.Color.valueOf(linkColor))
                     .build());
         }
 
-        return SignalStorageRecord.forAccount(builder.build());
+        return builder.build();
     }
 
-    public static SignalStorageRecord localToRemoteRecord(
-            Recipient recipient,
-            IdentityInfo identity,
-            byte[] rawStorageId
-    ) {
+    public static ContactRecord localToRemoteRecord(Recipient recipient, IdentityInfo identity) {
         final var address = recipient.getAddress();
-        final var builder = new SignalContactRecord.Builder(rawStorageId,
-                address.aci().orElse(null),
-                recipient.getStorageRecord()).setE164(address.number().orElse(null))
-                .setPni(address.pni().orElse(null))
-                .setUsername(address.username().orElse(null))
-                .setProfileKey(recipient.getProfileKey() == null ? null : recipient.getProfileKey().serialize());
+        final var builder = SignalContactRecord.Companion.newBuilder(recipient.getStorageRecord())
+                .aci(address.aci().map(ACI::toString).orElse(""))
+                .e164(address.number().orElse(""))
+                .pni(address.pni().map(PNI::toString).orElse(""))
+                .username(address.username().orElse(""))
+                .profileKey(recipient.getProfileKey() == null
+                        ? ByteString.EMPTY
+                        : ByteString.of(recipient.getProfileKey().serialize()));
         if (recipient.getProfile() != null) {
-            builder.setProfileGivenName(recipient.getProfile().getGivenName())
-                    .setProfileFamilyName(recipient.getProfile().getFamilyName());
+            builder.givenName(emptyIfNull(recipient.getProfile().getGivenName()))
+                    .familyName(emptyIfNull(recipient.getProfile().getFamilyName()));
         }
         if (recipient.getContact() != null) {
-            builder.setSystemGivenName(recipient.getContact().givenName())
-                    .setSystemFamilyName(recipient.getContact().familyName())
-                    .setSystemNickname(recipient.getContact().nickName())
-                    .setNicknameGivenName(recipient.getContact().nickNameGivenName() == null
-                            ? ""
-                            : recipient.getContact().nickNameGivenName())
-                    .setNicknameFamilyName(recipient.getContact().nickNameFamilyName() == null
-                            ? ""
-                            : recipient.getContact().nickNameFamilyName())
-                    .setNote(recipient.getContact().note())
-                    .setBlocked(recipient.getContact().isBlocked())
-                    .setProfileSharingEnabled(recipient.getContact().isProfileSharingEnabled())
-                    .setMuteUntil(recipient.getContact().muteUntil())
-                    .setHideStory(recipient.getContact().hideStory())
-                    .setUnregisteredTimestamp(recipient.getContact().unregisteredTimestamp() == null
+            builder.systemGivenName(emptyIfNull(recipient.getContact().givenName()))
+                    .systemFamilyName(emptyIfNull(recipient.getContact().familyName()))
+                    .systemNickname(emptyIfNull(recipient.getContact().nickName()))
+                    .nickname(new ContactRecord.Name.Builder().given(emptyIfNull(recipient.getContact()
+                                    .nickNameGivenName()))
+                            .family(emptyIfNull(recipient.getContact().nickNameFamilyName()))
+                            .build())
+                    .note(emptyIfNull(recipient.getContact().note()))
+                    .blocked(recipient.getContact().isBlocked())
+                    .whitelisted(recipient.getContact().isProfileSharingEnabled())
+                    .mutedUntilTimestamp(recipient.getContact().muteUntil())
+                    .hideStory(recipient.getContact().hideStory())
+                    .unregisteredAtTimestamp(recipient.getContact().unregisteredTimestamp() == null
                             ? 0
                             : recipient.getContact().unregisteredTimestamp())
-                    .setArchived(recipient.getContact().isArchived())
-                    .setHidden(recipient.getContact().isHidden());
+                    .archived(recipient.getContact().isArchived())
+                    .hidden(recipient.getContact().isHidden());
         }
         if (identity != null) {
-            builder.setIdentityKey(identity.getIdentityKey().serialize())
-                    .setIdentityState(localToRemote(identity.getTrustLevel()));
+            builder.identityKey(ByteString.of(identity.getIdentityKey().serialize()))
+                    .identityState(localToRemote(identity.getTrustLevel()));
         }
-        return SignalStorageRecord.forContact(builder.build());
+        return builder.build();
     }
 
-    public static SignalStorageRecord localToRemoteRecord(GroupInfoV1 group, byte[] rawStorageId) {
-        final var builder = new SignalGroupV1Record.Builder(rawStorageId,
-                group.getGroupId().serialize(),
-                group.getStorageRecord());
-        builder.setBlocked(group.isBlocked());
-        builder.setArchived(group.archived);
-        builder.setProfileSharingEnabled(true);
-        return SignalStorageRecord.forGroupV1(builder.build());
+    public static GroupV1Record localToRemoteRecord(GroupInfoV1 group) {
+        final var builder = SignalGroupV1Record.Companion.newBuilder(group.getStorageRecord());
+        builder.id(ByteString.of(group.getGroupId().serialize()));
+        builder.blocked(group.isBlocked());
+        builder.archived(group.archived);
+        builder.whitelisted(true);
+        return builder.build();
     }
 
-    public static SignalStorageRecord localToRemoteRecord(GroupInfoV2 group, byte[] rawStorageId) {
-        final var builder = new SignalGroupV2Record.Builder(rawStorageId,
-                group.getMasterKey(),
-                group.getStorageRecord());
-        builder.setBlocked(group.isBlocked());
-        builder.setProfileSharingEnabled(group.isProfileSharingEnabled());
-        return SignalStorageRecord.forGroupV2(builder.build());
+    public static GroupV2Record localToRemoteRecord(GroupInfoV2 group) {
+        final var builder = SignalGroupV2Record.Companion.newBuilder(group.getStorageRecord());
+        builder.masterKey(ByteString.of(group.getMasterKey().serialize()));
+        builder.blocked(group.isBlocked());
+        builder.whitelisted(group.isProfileSharingEnabled());
+        return builder.build();
     }
 
     public static TrustLevel remoteToLocal(IdentityState identityState) {
index b9b7025a0f0245cf17ac99203182e3314c711a0b..249a792d17f1061ca7b189d889e14cbfee9e085d 100644 (file)
@@ -5,6 +5,8 @@ import org.signal.core.util.Base64;
 import org.signal.core.util.SetUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.whispersystems.signalservice.api.push.ServiceId.ACI;
+import org.whispersystems.signalservice.api.push.ServiceId.PNI;
 import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
 import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
 import org.whispersystems.signalservice.api.storage.StorageId;
@@ -32,9 +34,7 @@ public final class StorageSyncValidations {
         validateManifestAndInserts(result.manifest(), result.inserts(), self);
 
         if (!result.deletes().isEmpty()) {
-            Set<String> allSetEncoded = result.manifest()
-                    .getStorageIds()
-                    .stream()
+            Set<String> allSetEncoded = result.manifest().storageIds.stream()
                     .map(StorageId::getRaw)
                     .map(Base64::encodeWithPadding)
                     .collect(Collectors.toSet());
@@ -47,13 +47,13 @@ public final class StorageSyncValidations {
             }
         }
 
-        if (previousManifest.getVersion() == 0) {
+        if (previousManifest.version == 0) {
             logger.debug(
                     "Previous manifest is empty, not bothering with additional validations around the diffs between the two manifests.");
             return;
         }
 
-        if (result.manifest().getVersion() != previousManifest.getVersion() + 1) {
+        if (result.manifest().version != previousManifest.version + 1) {
             throw new IncorrectManifestVersionError();
         }
 
@@ -63,13 +63,10 @@ public final class StorageSyncValidations {
             return;
         }
 
-        Set<ByteBuffer> previousIds = previousManifest.getStorageIds()
-                .stream()
+        Set<ByteBuffer> previousIds = previousManifest.storageIds.stream()
                 .map(id -> ByteBuffer.wrap(id.getRaw()))
                 .collect(Collectors.toSet());
-        Set<ByteBuffer> newIds = result.manifest()
-                .getStorageIds()
-                .stream()
+        Set<ByteBuffer> newIds = result.manifest().storageIds.stream()
                 .map(id -> ByteBuffer.wrap(id.getRaw()))
                 .collect(Collectors.toSet());
 
@@ -83,12 +80,12 @@ public final class StorageSyncValidations {
         Set<ByteBuffer> declaredDeletes = result.deletes().stream().map(ByteBuffer::wrap).collect(Collectors.toSet());
 
         if (declaredInserts.size() > manifestInserts.size()) {
-            logger.debug("DeclaredInserts: " + declaredInserts.size() + ", ManifestInserts: " + manifestInserts.size());
+            logger.debug("DeclaredInserts: {}, ManifestInserts: {}", declaredInserts.size(), manifestInserts.size());
             throw new MoreInsertsThanExpectedError();
         }
 
         if (declaredInserts.size() < manifestInserts.size()) {
-            logger.debug("DeclaredInserts: " + declaredInserts.size() + ", ManifestInserts: " + manifestInserts.size());
+            logger.debug("DeclaredInserts: {}, ManifestInserts: {}", declaredInserts.size(), manifestInserts.size());
             throw new LessInsertsThanExpectedError();
         }
 
@@ -97,12 +94,12 @@ public final class StorageSyncValidations {
         }
 
         if (declaredDeletes.size() > manifestDeletes.size()) {
-            logger.debug("DeclaredDeletes: " + declaredDeletes.size() + ", ManifestDeletes: " + manifestDeletes.size());
+            logger.debug("DeclaredDeletes: {}, ManifestDeletes: {}", declaredDeletes.size(), manifestDeletes.size());
             throw new MoreDeletesThanExpectedError();
         }
 
         if (declaredDeletes.size() < manifestDeletes.size()) {
-            logger.debug("DeclaredDeletes: " + declaredDeletes.size() + ", ManifestDeletes: " + manifestDeletes.size());
+            logger.debug("DeclaredDeletes: {}, ManifestDeletes: {}", declaredDeletes.size(), manifestDeletes.size());
             throw new LessDeletesThanExpectedError();
         }
 
@@ -125,7 +122,7 @@ public final class StorageSyncValidations {
             RecipientAddress self
     ) {
         int accountCount = 0;
-        for (StorageId id : manifest.getStorageIds()) {
+        for (StorageId id : manifest.storageIds) {
             accountCount += id.getType() == ManifestRecord.Identifier.Type.ACCOUNT.getValue() ? 1 : 0;
         }
 
@@ -137,11 +134,11 @@ public final class StorageSyncValidations {
             throw new MissingAccountError();
         }
 
-        Set<StorageId> allSet = new HashSet<>(manifest.getStorageIds());
+        Set<StorageId> allSet = new HashSet<>(manifest.storageIds);
         Set<StorageId> insertSet = inserts.stream().map(SignalStorageRecord::getId).collect(Collectors.toSet());
         Set<ByteBuffer> rawIdSet = allSet.stream().map(id -> ByteBuffer.wrap(id.getRaw())).collect(Collectors.toSet());
 
-        if (allSet.size() != manifest.getStorageIds().size()) {
+        if (allSet.size() != manifest.storageIds.size()) {
             throw new DuplicateStorageIdError();
         }
 
@@ -166,6 +163,11 @@ public final class StorageSyncValidations {
                 throw new DuplicateDistributionListIdError();
             }
 
+            ids = manifest.getStorageIdsByType().get(ManifestRecord.Identifier.Type.CALL_LINK.getValue());
+            if (ids.size() != new HashSet<>(ids).size()) {
+                throw new DuplicateCallLinkError();
+            }
+
             throw new DuplicateRawIdAcrossTypesError();
         }
 
@@ -182,18 +184,18 @@ public final class StorageSyncValidations {
                 throw new UnknownInsertError();
             }
 
-            if (insert.getContact().isPresent()) {
-                final var contact = insert.getContact().get();
-                final var aci = contact.getAci();
-                final var pni = contact.getPni();
-                final var number = contact.getNumber();
-                final var username = contact.getUsername();
+            if (insert.getProto().contact != null) {
+                final var contact = insert.getProto().contact;
+                final var aci = ACI.parseOrNull(contact.aci);
+                final var pni = PNI.parseOrNull(contact.pni);
+                final var number = contact.e164.isEmpty() ? null : contact.e164;
+                final var username = contact.username.isEmpty() ? null : contact.username;
                 final var address = new RecipientAddress(aci, pni, number, username);
                 if (self.matches(address)) {
                     throw new SelfAddedAsContactError();
                 }
             }
-            if (insert.getAccount().isPresent() && insert.getAccount().get().getProfileKey().isEmpty()) {
+            if (insert.getProto().account != null && insert.getProto().account.profileKey.size() == 0) {
                 logger.debug("Uploading a null profile key in our AccountRecord!");
             }
         }
@@ -211,6 +213,8 @@ public final class StorageSyncValidations {
 
     private static final class DuplicateDistributionListIdError extends Error {}
 
+    private static final class DuplicateCallLinkError extends Error {}
+
     private static final class DuplicateInsertInWriteError extends Error {}
 
     private static final class InsertNotPresentInFullIdSetError extends Error {}
index 97e3579a7bf50c7ab3e8877df155666f7ef47a31..5bbd996ed5a150f4ae9a6c5ed46e0271cda2a421 100644 (file)
@@ -21,8 +21,8 @@ public record WriteOperationResult(
         } else {
             return String.format(Locale.ROOT,
                     "ManifestVersion: %d, Total Keys: %d, Inserts: %d, Deletes: %d",
-                    manifest.getVersion(),
-                    manifest.getStorageIds().size(),
+                    manifest.version,
+                    manifest.storageIds.size(),
                     inserts.size(),
                     deletes.size());
         }
index c3dcf3fed1aa2689ff70bd0289e40b9f9a60cf8f..21c8aa960b76d1d13344f3225c617b4f948d61cd 100644 (file)
@@ -14,6 +14,7 @@ import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
 import org.signal.libsignal.zkgroup.InvalidInputException;
 import org.signal.libsignal.zkgroup.profiles.ProfileKey;
 import org.whispersystems.signalservice.api.account.PreKeyCollection;
+import org.whispersystems.signalservice.api.backup.MediaRootBackupKey;
 import org.whispersystems.signalservice.api.kbs.MasterKey;
 
 import java.security.SecureRandom;
@@ -112,6 +113,10 @@ public class KeyUtils {
         return MasterKey.createNew(secureRandom);
     }
 
+    public static MediaRootBackupKey createMediaRootBackupKey() {
+        return new MediaRootBackupKey(getSecretBytes(32));
+    }
+
     public static byte[] createRawStorageId() {
         return getSecretBytes(16);
     }
index e27ba9808fe47b0e261e2b814961ebb91b257ab2..e0c4b61924e66e70bc7ed32645fd58ef5c032c24 100644 (file)
@@ -184,7 +184,7 @@ public class NumberVerificationUtils {
                  TokenNotAcceptedException _e) {
             throw new CaptchaRequiredException("Captcha not accepted");
         } catch (NonSuccessfulResponseCodeException e) {
-            if (e.getCode() == 400) {
+            if (e.code == 400) {
                 throw new CaptchaRequiredException("Captcha has invalid format");
             }
             throw e;
index def75049b03b1f626431ce7defd62bfb5f1c1f49..0b5b418a95bf0746f7309c242d52ae23324192bb 100644 (file)
@@ -84,6 +84,9 @@ public class ProfileUtils {
         if (encryptedProfile.getCapabilities().isStorage()) {
             capabilities.add(Profile.Capability.storage);
         }
+        if (encryptedProfile.getCapabilities().isStorageServiceEncryptionV2()) {
+            capabilities.add(Profile.Capability.storageServiceEncryptionV2Capability);
+        }
 
         return capabilities;
     }
index ee1f608f9a45a5e806cf40825bc53bd971ea709c..945b2756872dd11970b06da6b6f45e967c514943 100644 (file)
@@ -18,6 +18,7 @@ import java.io.InputStream;
 import java.net.URLDecoder;
 import java.nio.charset.StandardCharsets;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
@@ -30,6 +31,8 @@ import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
 
+import okio.ByteString;
+
 public class Utils {
 
     private static final Logger logger = LoggerFactory.getLogger(Utils.class);
@@ -157,4 +160,46 @@ public class Utils {
         }
         return response.successOrThrow();
     }
+
+    public static ByteString firstNonEmpty(ByteString... strings) {
+        for (final var s : strings) {
+            if (s.size() > 0) {
+                return s;
+            }
+        }
+        return ByteString.EMPTY;
+    }
+
+    @SafeVarargs
+    public static <T> List<T> firstNonEmpty(List<T>... values) {
+        for (final var s : values) {
+            if (!s.isEmpty()) {
+                return s;
+            }
+        }
+        return List.of();
+    }
+
+    public static String firstNonEmpty(String... strings) {
+        for (final var s : strings) {
+            if (!s.isEmpty()) {
+                return s;
+            }
+        }
+        return "";
+    }
+
+    @SafeVarargs
+    public static <T> T firstNonNull(T... values) {
+        for (final var v : values) {
+            if (v != null) {
+                return v;
+            }
+        }
+        return null;
+    }
+
+    public static String nullIfEmpty(String string) {
+        return string == null || string.isEmpty() ? null : string;
+    }
 }