Update libsignal-service

Support for storage encryption v2 and account entropy pool

Fixes #1632
This commit is contained in:
AsamK 2024-11-23 22:29:01 +01:00
parent f2005593ec
commit ff6cb5262a
29 changed files with 952 additions and 639 deletions

View file

@ -124,6 +124,13 @@
"name":"com.fasterxml.jackson.databind.ext.Java7SupportImpl", "name":"com.fasterxml.jackson.databind.ext.Java7SupportImpl",
"methods":[{"name":"<init>","parameterTypes":[] }] "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", "name":"com.squareup.wire.internal.ImmutableList",
"allDeclaredFields":true, "allDeclaredFields":true,
@ -209,9 +216,14 @@
{ {
"name":"java.io.FilePermission" "name":"java.io.FilePermission"
}, },
{
"name":"java.io.OutputStream"
},
{ {
"name":"java.io.Serializable", "name":"java.io.Serializable",
"allDeclaredMethods":true "allDeclaredFields":true,
"allDeclaredMethods":true,
"allDeclaredClasses":true
}, },
{ {
"name":"java.lang.Boolean", "name":"java.lang.Boolean",
@ -577,6 +589,9 @@
{ {
"name":"kotlin.String" "name":"kotlin.String"
}, },
{
"name":"kotlin.Unit"
},
{ {
"name":"kotlin.collections.AbstractCollection", "name":"kotlin.collections.AbstractCollection",
"allDeclaredFields":true, "allDeclaredFields":true,
@ -629,6 +644,9 @@
{ {
"name":"long[]" "name":"long[]"
}, },
{
"name":"okio.BufferedSink"
},
{ {
"name":"okio.ByteString" "name":"okio.ByteString"
}, },
@ -1025,7 +1043,7 @@
"allDeclaredFields":true, "allDeclaredFields":true,
"allDeclaredMethods":true, "allDeclaredMethods":true,
"allDeclaredConstructors":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", "name":"org.asamk.signal.json.JsonMention",
@ -1247,7 +1265,7 @@
"allDeclaredFields":true, "allDeclaredFields":true,
"queryAllDeclaredMethods":true, "queryAllDeclaredMethods":true,
"queryAllDeclaredConstructors":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", "name":"org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData",
@ -2257,7 +2275,7 @@
"allDeclaredFields":true, "allDeclaredFields":true,
"allDeclaredMethods":true, "allDeclaredMethods":true,
"allDeclaredConstructors":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.account.ChangePhoneNumberRequest",
@ -2284,6 +2302,13 @@
{ {
"name":"org.whispersystems.signalservice.api.groupsv2.TemporalCredential[]" "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", "name":"org.whispersystems.signalservice.api.messages.calls.HangupMessage",
"allDeclaredFields":true, "allDeclaredFields":true,
@ -2891,7 +2916,17 @@
}, },
{ {
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord", "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.AccountRecord$PinnedConversation",
@ -2907,7 +2942,17 @@
}, },
{ {
"name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord", "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.ContactRecord$Name",
@ -2915,11 +2960,28 @@
}, },
{ {
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV1Record", "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", "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",
@ -2929,6 +2991,9 @@
"name":"org.whispersystems.signalservice.internal.storage.protos.ManifestRecord$Identifier", "name":"org.whispersystems.signalservice.internal.storage.protos.ManifestRecord$Identifier",
"fields":[{"name":"raw_"}, {"name":"type_"}] "fields":[{"name":"raw_"}, {"name":"type_"}]
}, },
{
"name":"org.whispersystems.signalservice.internal.storage.protos.OptionalBool"
},
{ {
"name":"org.whispersystems.signalservice.internal.storage.protos.Payments", "name":"org.whispersystems.signalservice.internal.storage.protos.Payments",
"allDeclaredFields":true "allDeclaredFields":true

View 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" } slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" }
logback = "ch.qos.logback:logback-classic:1.5.12" 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" sqlite = "org.xerial:sqlite-jdbc:3.47.0.0"
hikari = "com.zaxxer:HikariCP:6.2.1" hikari = "com.zaxxer:HikariCP:6.2.1"
junit-jupiter = "org.junit.jupiter:junit-jupiter:5.11.3" junit-jupiter = "org.junit.jupiter:junit-jupiter:5.11.3"

View file

@ -611,11 +611,12 @@ public record MessageEnvelope(
RecipientResolver recipientResolver, RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver RecipientAddressResolver addressResolver
) { ) {
return new Blocked(blockedListMessage.getAddresses() return new Blocked(blockedListMessage.individuals.stream()
.stream() .map(d -> new RecipientAddress(d.getAci() == null ? null : d.getAci().toString(),
.map(d -> addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(d)) null,
.toApiRecipientAddress()) d.getE164(),
.toList(), blockedListMessage.getGroupIds().stream().map(GroupId::unknownVersion).toList()); null))
.toList(), blockedListMessage.groupIds.stream().map(GroupId::unknownVersion).toList());
} }
} }

View file

@ -161,7 +161,8 @@ public class Profile {
} }
public enum Capability { public enum Capability {
storage; storage,
storageServiceEncryptionV2Capability;
public static Capability valueOfOrNull(String value) { public static Capability valueOfOrNull(String value) {
try { try {

View file

@ -51,7 +51,7 @@ class LiveConfig {
private static final byte[] backupServerPublicParams = Base64.getDecoder() private static final byte[] backupServerPublicParams = Base64.getDecoder()
.decode("AJwNSU55fsFCbgaxGRD11wO1juAs8Yr5GF8FPlGzzvdJJIKH5/4CC7ZJSOe3yL2vturVaRU2Cx0n751Vt8wkj1bozK3CBV1UokxV09GWf+hdVImLGjXGYLLhnI1J2TWEe7iWHyb553EEnRb5oxr9n3lUbNAJuRmFM7hrr0Al0F0wrDD4S8lo2mGaXe0MJCOM166F8oYRQqpFeEHfiLnxA1O8ZLh7vMdv4g9jI5phpRBTsJ5IjiJrWeP0zdIGHEssUeprDZ9OUJ14m0v61eYJMKsf59Bn+mAT2a7YfB+Don9O"); .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( static SignalServiceConfiguration createDefaultServiceConfiguration(
final List<Interceptor> interceptors final List<Interceptor> interceptors
@ -71,7 +71,8 @@ class LiveConfig {
proxy, proxy,
zkGroupServerPublicParams, zkGroupServerPublicParams,
genericServerPublicParams, genericServerPublicParams,
backupServerPublicParams); backupServerPublicParams,
false);
} }
static ECPublicKey getUnidentifiedSenderTrustRoot() { static ECPublicKey getUnidentifiedSenderTrustRoot() {

View file

@ -29,7 +29,8 @@ public class ServiceConfig {
public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) { public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) {
final var deleteSync = !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( public static ServiceEnvironmentConfig getServiceEnvironmentConfig(

View file

@ -51,7 +51,7 @@ class StagingConfig {
private static final byte[] backupServerPublicParams = Base64.getDecoder() private static final byte[] backupServerPublicParams = Base64.getDecoder()
.decode("AHYrGb9IfugAAJiPKp+mdXUx+OL9zBolPYHYQz6GI1gWjpEu5me3zVNSvmYY4zWboZHif+HG1sDHSuvwFd0QszSwuSF4X4kRP3fJREdTZ5MCR0n55zUppTwfHRW2S4sdQ0JGz7YDQIJCufYSKh0pGNEHL6hv79Agrdnr4momr3oXdnkpVBIp3HWAQ6IbXQVSG18X36GaicI1vdT0UFmTwU2KTneluC2eyL9c5ff8PcmiS+YcLzh0OKYQXB5ZfQ06d6DiINvDQLy75zcfUOniLAj0lGJiHxGczin/RXisKSR8"); .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( static SignalServiceConfiguration createDefaultServiceConfiguration(
final List<Interceptor> interceptors final List<Interceptor> interceptors
@ -71,7 +71,8 @@ class StagingConfig {
proxy, proxy,
zkGroupServerPublicParams, zkGroupServerPublicParams,
genericServerPublicParams, genericServerPublicParams,
backupServerPublicParams); backupServerPublicParams,
false);
} }
static ECPublicKey getUnidentifiedSenderTrustRoot() { static ECPublicKey getUnidentifiedSenderTrustRoot() {

View file

@ -3,7 +3,6 @@ package org.asamk.signal.manager.helper;
import org.asamk.signal.manager.api.CaptchaRequiredException; import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.DeviceLinkUrl; import org.asamk.signal.manager.api.DeviceLinkUrl;
import org.asamk.signal.manager.api.IncorrectPinException; 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.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.RateLimitException;
@ -27,6 +26,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest; import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; 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.ACI;
import org.whispersystems.signalservice.api.push.ServiceId.PNI; import org.whispersystems.signalservice.api.push.ServiceId.PNI;
import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.ServiceIdType;
@ -56,6 +56,7 @@ import java.util.concurrent.TimeUnit;
import okio.ByteString; import okio.ByteString;
import static org.asamk.signal.manager.config.ServiceConfig.PREKEY_MAXIMUM_ID; 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; import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
public class AccountHelper { public class AccountHelper {
@ -289,12 +290,11 @@ public class AccountHelper {
(sessionId1, verificationCode1, registrationLock) -> { (sessionId1, verificationCode1, registrationLock) -> {
final var registrationApi = dependencies.getRegistrationApi(); final var registrationApi = dependencies.getRegistrationApi();
try { try {
Utils.handleResponseException(registrationApi.verifyAccount(sessionId1, verificationCode1)); handleResponseException(registrationApi.verifyAccount(sessionId1, verificationCode1));
} catch (AlreadyVerifiedException e) { } catch (AlreadyVerifiedException e) {
// Already verified so can continue changing number // Already verified so can continue changing number
} }
return Utils.handleResponseException(registrationApi.changeNumber(new ChangePhoneNumberRequest( return handleResponseException(registrationApi.changeNumber(new ChangePhoneNumberRequest(sessionId1,
sessionId1,
null, null,
newNumber, newNumber,
registrationLock, registrationLock,
@ -482,26 +482,28 @@ public class AccountHelper {
dependencies.getAccountManager().setAccountAttributes(account.getAccountAttributes(null)); dependencies.getAccountManager().setAccountAttributes(account.getAccountAttributes(null));
} }
public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, InvalidDeviceLinkException, org.asamk.signal.manager.api.DeviceLimitExceededException { public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, org.asamk.signal.manager.api.DeviceLimitExceededException {
String verificationCode; final var linkDeviceApi = dependencies.getLinkDeviceApi();
final LinkedDeviceVerificationCodeResponse verificationCode;
try { try {
verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode(); verificationCode = handleResponseException(linkDeviceApi.getDeviceVerificationCode());
} catch (DeviceLimitExceededException e) { } catch (DeviceLimitExceededException e) {
throw new org.asamk.signal.manager.api.DeviceLimitExceededException("Too many linked devices", e); throw new org.asamk.signal.manager.api.DeviceLimitExceededException("Too many linked devices", e);
} }
try { handleResponseException(dependencies.getLinkDeviceApi()
dependencies.getAccountManager() .linkDevice(account.getNumber(),
.addDevice(deviceLinkInfo.deviceIdentifier(), account.getAci(),
deviceLinkInfo.deviceKey(), account.getPni(),
account.getAciIdentityKeyPair(), deviceLinkInfo.deviceIdentifier(),
account.getPniIdentityKeyPair(), deviceLinkInfo.deviceKey(),
account.getProfileKey(), account.getAciIdentityKeyPair(),
account.getOrCreatePinMasterKey(), account.getPniIdentityKeyPair(),
verificationCode); account.getProfileKey(),
} catch (InvalidKeyException e) { account.getOrCreatePinMasterKey(),
throw new InvalidDeviceLinkException("Invalid device link", e); account.getOrCreateMediaRootBackupKey(),
} verificationCode.getVerificationCode(),
null));
account.setMultiDevice(true); account.setMultiDevice(true);
context.getJobExecutor().enqueueJob(new SyncStorageJob()); context.getJobExecutor().enqueueJob(new SyncStorageJob());
} }

View file

@ -82,7 +82,7 @@ class GroupV2Helper {
final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams); final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams);
return dependencies.getGroupsV2Api().getGroup(groupSecretParams, groupsV2AuthorizationString); return dependencies.getGroupsV2Api().getGroup(groupSecretParams, groupsV2AuthorizationString);
} catch (NonSuccessfulResponseCodeException e) { } catch (NonSuccessfulResponseCodeException e) {
if (e.getCode() == 403) { if (e.code == 403) {
throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null); throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
} }
logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage()); logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage());
@ -119,7 +119,7 @@ class GroupV2Helper {
false, false,
sendEndorsementsExpirationMs); sendEndorsementsExpirationMs);
} catch (NonSuccessfulResponseCodeException e) { } catch (NonSuccessfulResponseCodeException e) {
if (e.getCode() == 403) { if (e.code == 403) {
throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null); throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
} }
logger.warn("Failed to retrieve Group V2 history, ignoring: {}", e.getMessage()); logger.warn("Failed to retrieve Group V2 history, ignoring: {}", e.getMessage());

View file

@ -31,6 +31,7 @@ import org.asamk.signal.manager.internal.SignalDependencies;
import org.asamk.signal.manager.jobs.RetrieveStickerPackJob; import org.asamk.signal.manager.jobs.RetrieveStickerPackJob;
import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.groups.GroupInfoV1; 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.recipients.RecipientId;
import org.asamk.signal.manager.storage.stickers.StickerPack; import org.asamk.signal.manager.storage.stickers.StickerPack;
import org.signal.libsignal.metadata.ProtocolInvalidKeyException; import org.signal.libsignal.metadata.ProtocolInvalidKeyException;
@ -525,12 +526,12 @@ public final class IncomingMessageHandler {
} }
if (syncMessage.getBlockedList().isPresent()) { if (syncMessage.getBlockedList().isPresent()) {
final var blockedListMessage = syncMessage.getBlockedList().get(); final var blockedListMessage = syncMessage.getBlockedList().get();
for (var address : blockedListMessage.getAddresses()) { for (var individual : blockedListMessage.individuals) {
context.getContactHelper() final var address = new RecipientAddress(individual.getAci(), individual.getE164());
.setContactBlocked(account.getRecipientResolver().resolveRecipient(address), true); final var recipientId = account.getRecipientResolver().resolveRecipient(address);
context.getContactHelper().setContactBlocked(recipientId, true);
} }
for (var groupId : blockedListMessage.getGroupIds() for (var groupId : blockedListMessage.groupIds.stream()
.stream()
.map(GroupId::unknownVersion) .map(GroupId::unknownVersion)
.collect(Collectors.toSet())) { .collect(Collectors.toSet())) {
try { try {
@ -585,14 +586,22 @@ public final class IncomingMessageHandler {
} }
if (syncMessage.getKeys().isPresent()) { if (syncMessage.getKeys().isPresent()) {
final var keysMessage = syncMessage.getKeys().get(); final var keysMessage = syncMessage.getKeys().get();
if (keysMessage.getStorageService().isPresent()) { if (keysMessage.getAccountEntropyPool() != null) {
final var storageKey = keysMessage.getStorageService().get(); 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); account.setStorageKey(storageKey);
actions.add(SyncStorageDataAction.create()); actions.add(SyncStorageDataAction.create());
} }
if (keysMessage.getMaster().isPresent()) { if (keysMessage.getMediaRootBackupKey() != null) {
final var masterKey = keysMessage.getMaster().get(); final var mrb = keysMessage.getMediaRootBackupKey();
account.setMasterKey(masterKey); account.setMediaRootBackupKey(mrb);
actions.add(SyncStorageDataAction.create()); actions.add(SyncStorageDataAction.create());
} }
} }

View file

@ -172,7 +172,7 @@ public class PreKeyHelper {
// This can happen when the primary device has changed phone number // This can happen when the primary device has changed phone number
logger.warn("Failed to updated pre keys: {}", e.getMessage()); logger.warn("Failed to updated pre keys: {}", e.getMessage());
} catch (NonSuccessfulResponseCodeException e) { } catch (NonSuccessfulResponseCodeException e) {
if (serviceIdType != ServiceIdType.PNI || e.getCode() != 422) { if (serviceIdType != ServiceIdType.PNI || e.code != 422) {
throw e; throw e;
} }
logger.warn("Failed to set PNI pre keys, ignoring for now. Account needs to be reregistered to fix this."); logger.warn("Failed to set PNI pre keys, ignoring for now. Account needs to be reregistered to fix this.");

View file

@ -336,13 +336,6 @@ public final class ProfileHelper {
final var profile = account.getProfileStore().getProfile(recipientId); 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; Profile newProfile = null;
if (profileKey.isPresent()) { if (profileKey.isPresent()) {
logger.trace("Decrypting profile"); logger.trace("Decrypting profile");
@ -358,6 +351,18 @@ public final class ProfileHelper {
.build(); .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 { try {
logger.trace("Storing identity"); logger.trace("Storing identity");
final var identityKey = new IdentityKey(Base64.getDecoder().decode(encryptedProfile.getIdentityKey())); final var identityKey = new IdentityKey(Base64.getDecoder().decode(encryptedProfile.getIdentityKey()));

View file

@ -239,7 +239,6 @@ public class RecipientHelper {
newNumbers, newNumbers,
account.getRecipientStore().getServiceIdToProfileKeyMap(), account.getRecipientStore().getServiceIdToProfileKeyMap(),
token, token,
dependencies.getServiceEnvironmentConfig().cdsiMrenclave(),
null, null,
dependencies.getLibSignalNetwork(), dependencies.getLibSignalNetwork(),
newToken -> { newToken -> {

View file

@ -2,6 +2,7 @@ package org.asamk.signal.manager.helper;
import org.asamk.signal.manager.api.GroupIdV1; import org.asamk.signal.manager.api.GroupIdV1;
import org.asamk.signal.manager.api.GroupIdV2; 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.internal.SignalDependencies;
import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.recipients.RecipientId; 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.signal.libsignal.protocol.InvalidKeyException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.storage.RecordIkm;
import org.whispersystems.signalservice.api.storage.SignalStorageManifest; import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord; import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import org.whispersystems.signalservice.api.storage.StorageId; import org.whispersystems.signalservice.api.storage.StorageId;
import org.whispersystems.signalservice.api.storage.StorageKey; 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.ManifestRecord;
import org.whispersystems.signalservice.internal.storage.protos.StorageRecord;
import java.io.IOException; import java.io.IOException;
import java.sql.Connection; import java.sql.Connection;
@ -32,9 +39,10 @@ import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.asamk.signal.manager.util.Utils.handleResponseException;
public class StorageHelper { public class StorageHelper {
private static final Logger logger = LoggerFactory.getLogger(StorageHelper.class); private static final Logger logger = LoggerFactory.getLogger(StorageHelper.class);
@ -54,7 +62,7 @@ public class StorageHelper {
} }
public void syncDataWithStorage() throws IOException { public void syncDataWithStorage() throws IOException {
final var storageKey = account.getOrCreateStorageKey(); var storageKey = account.getOrCreateStorageKey();
if (storageKey == null) { if (storageKey == null) {
if (!account.isPrimaryDevice()) { if (!account.isPrimaryDevice()) {
logger.debug("Storage key unknown, requesting from primary device."); logger.debug("Storage key unknown, requesting from primary device.");
@ -65,52 +73,76 @@ public class StorageHelper {
logger.trace("Reading manifest from remote storage"); logger.trace("Reading manifest from remote storage");
final var localManifestVersion = account.getStorageManifestVersion(); final var localManifestVersion = account.getStorageManifestVersion();
final var localManifest = account.getStorageManifest().orElse(SignalStorageManifest.EMPTY); final var localManifest = account.getStorageManifest().orElse(SignalStorageManifest.Companion.getEMPTY());
SignalStorageManifest remoteManifest; final var storageServiceRepository = dependencies.getStorageServiceRepository();
try { final var result = storageServiceRepository.getStorageManifestIfDifferentVersion(storageKey,
remoteManifest = dependencies.getAccountManager() localManifestVersion);
.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;
}
}
return;
}
logger.trace("Manifest versions: local {}, remote {}", localManifestVersion, remoteManifest.getVersion());
var needsForcePush = false; var needsForcePush = false;
if (remoteManifest.getVersion() > localManifestVersion) { final var remoteManifest = switch (result) {
logger.trace("Remote version was newer, reading records."); case ManifestIfDifferentVersionResult.DifferentVersion diff -> {
needsForcePush = readDataFromStorage(storageKey, localManifest, remoteManifest); final var manifest = diff.getManifest();
} else if (remoteManifest.getVersion() < localManifest.getVersion()) { storeManifestLocally(manifest);
logger.debug("Remote storage manifest version was older. User might have switched accounts."); yield manifest;
} }
logger.trace("Done reading data from remote storage"); case ManifestIfDifferentVersionResult.DecryptionError ignore -> {
logger.warn("Manifest couldn't be decrypted.");
if (account.isPrimaryDevice()) {
needsForcePush = true;
} else {
context.getSyncHelper().requestSyncKeys();
}
yield null;
}
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");
};
if (localManifest != remoteManifest) { if (remoteManifest != null) {
storeManifestLocally(remoteManifest); logger.trace("Manifest versions: local {}, remote {}", localManifestVersion, remoteManifest.version);
}
readRecordsWithPreviouslyUnknownTypes(storageKey); 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");
readRecordsWithPreviouslyUnknownTypes(storageKey, remoteManifest);
}
logger.trace("Adding missing storageIds to local data"); logger.trace("Adding missing storageIds to local data");
account.getRecipientStore().setMissingStorageIds(); account.getRecipientStore().setMissingStorageIds();
account.getGroupStore().setMissingStorageIds(); account.getGroupStore().setMissingStorageIds();
var needsMultiDeviceSync = false; var needsMultiDeviceSync = false;
try {
needsMultiDeviceSync = writeToStorage(storageKey, remoteManifest, needsForcePush); if (account.needsStorageKeyMigration()) {
} catch (RetryLaterException e) { logger.debug("Storage needs force push due to new account entropy pool");
// TODO retry later // Set new aep and reset previous master key and storage key
return; 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) { if (needsForcePush) {
@ -131,6 +163,23 @@ public class StorageHelper {
logger.debug("Done syncing data with remote storage"); 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( private boolean readDataFromStorage(
final StorageKey storageKey, final StorageKey storageKey,
final SignalStorageManifest localManifest, final SignalStorageManifest localManifest,
@ -140,14 +189,14 @@ public class StorageHelper {
try (final var connection = account.getAccountDatabase().getConnection()) { try (final var connection = account.getAccountDatabase().getConnection()) {
connection.setAutoCommit(false); connection.setAutoCommit(false);
var idDifference = findIdDifference(remoteManifest.getStorageIds(), localManifest.getStorageIds()); var idDifference = findIdDifference(remoteManifest.storageIds, localManifest.storageIds);
if (idDifference.hasTypeMismatches() && account.isPrimaryDevice()) { if (idDifference.hasTypeMismatches() && account.isPrimaryDevice()) {
logger.debug("Found type mismatches in the ID sets! Scheduling a force push after this sync completes."); logger.debug("Found type mismatches in the ID sets! Scheduling a force push after this sync completes.");
needsForcePush = true; needsForcePush = true;
} }
logger.debug("Pre-Merge ID Difference :: " + idDifference); logger.debug("Pre-Merge ID Difference :: {}", idDifference);
if (!idDifference.localOnlyIds().isEmpty()) { if (!idDifference.localOnlyIds().isEmpty()) {
final var updated = account.getRecipientStore() final var updated = account.getRecipientStore()
@ -161,15 +210,15 @@ public class StorageHelper {
} }
if (!idDifference.isEmpty()) { if (!idDifference.isEmpty()) {
final var remoteOnlyRecords = getSignalStorageRecords(storageKey, idDifference.remoteOnlyIds()); final var remoteOnlyRecords = getSignalStorageRecords(storageKey,
remoteManifest,
idDifference.remoteOnlyIds());
if (remoteOnlyRecords.size() != idDifference.remoteOnlyIds().size()) { if (remoteOnlyRecords.size() != idDifference.remoteOnlyIds().size()) {
logger.debug("Could not find all remote-only records! Requested: " logger.debug(
+ idDifference.remoteOnlyIds() "Could not find all remote-only records! Requested: {}, Found: {}. These stragglers should naturally get deleted during the sync.",
.size() idDifference.remoteOnlyIds().size(),
+ ", Found: " remoteOnlyRecords.size());
+ remoteOnlyRecords.size()
+ ". These stragglers should naturally get deleted during the sync.");
} }
final var unknownInserts = processKnownRecords(connection, remoteOnlyRecords); final var unknownInserts = processKnownRecords(connection, remoteOnlyRecords);
@ -194,18 +243,21 @@ public class StorageHelper {
return needsForcePush; 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()) { try (final var connection = account.getAccountDatabase().getConnection()) {
connection.setAutoCommit(false); connection.setAutoCommit(false);
final var knownUnknownIds = account.getUnknownStorageIdStore() final var knownUnknownIds = account.getUnknownStorageIdStore()
.getUnknownStorageIds(connection, KNOWN_TYPES); .getUnknownStorageIds(connection, KNOWN_TYPES);
if (!knownUnknownIds.isEmpty()) { 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); processKnownRecords(connection, remote);
account.getUnknownStorageIdStore() account.getUnknownStorageIdStore()
@ -227,15 +279,16 @@ public class StorageHelper {
connection.setAutoCommit(false); connection.setAutoCommit(false);
final var localStorageIds = getAllLocalStorageIds(connection); final var localStorageIds = getAllLocalStorageIds(connection);
final var idDifference = findIdDifference(remoteManifest.getStorageIds(), localStorageIds); final var idDifference = findIdDifference(remoteManifest.storageIds, localStorageIds);
logger.debug("ID Difference :: " + idDifference); logger.debug("ID Difference :: {}", idDifference);
final var remoteDeletes = idDifference.remoteOnlyIds().stream().map(StorageId::getRaw).toList(); final var remoteDeletes = idDifference.remoteOnlyIds().stream().map(StorageId::getRaw).toList();
final var remoteInserts = buildLocalStorageRecords(connection, idDifference.localOnlyIds()); final var remoteInserts = buildLocalStorageRecords(connection, idDifference.localOnlyIds());
// TODO check if local storage record proto matches remote, then reset to remote storage_id // 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(), account.getDeviceId(),
remoteManifest.recordIkm,
localStorageIds), remoteInserts, remoteDeletes); localStorageIds), remoteInserts, remoteDeletes);
connection.commit(); connection.commit();
@ -244,39 +297,37 @@ public class StorageHelper {
} }
if (remoteWriteOperation.isEmpty()) { 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; return false;
} }
logger.debug("We have something to write remotely."); logger.debug("We have something to write remotely.");
logger.debug("WriteOperationResult :: " + remoteWriteOperation); logger.debug("WriteOperationResult :: {}", remoteWriteOperation);
StorageSyncValidations.validate(remoteWriteOperation, StorageSyncValidations.validate(remoteWriteOperation,
remoteManifest, remoteManifest,
needsForcePush, needsForcePush,
account.getSelfRecipientAddress()); account.getSelfRecipientAddress());
final Optional<SignalStorageManifest> conflict; final var result = dependencies.getStorageServiceRepository()
try { .writeStorageRecords(storageKey,
conflict = dependencies.getAccountManager() remoteWriteOperation.manifest(),
.writeStorageRecords(storageKey, remoteWriteOperation.inserts(),
remoteWriteOperation.manifest(), remoteWriteOperation.deletes());
remoteWriteOperation.inserts(), switch (result) {
remoteWriteOperation.deletes()); case WriteStorageRecordsResult.ConflictError ignored -> {
} catch (InvalidKeyException e) { logger.debug("Hit a conflict when trying to resolve the conflict! Retrying.");
logger.warn("Failed to decrypt conflicting storage manifest: {}", e.getMessage()); throw new RetryLaterException();
throw new IOException(e); }
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);
} }
if (conflict.isPresent()) {
logger.debug("Hit a conflict when trying to resolve the conflict! Retrying.");
throw new RetryLaterException();
}
logger.debug("Saved new manifest. Now at version: " + remoteWriteOperation.manifest().getVersion());
storeManifestLocally(remoteWriteOperation.manifest());
return true;
} }
private void forcePushToStorage( private void forcePushToStorage(
@ -284,7 +335,8 @@ public class StorageHelper {
) throws IOException, RetryLaterException { ) throws IOException, RetryLaterException {
logger.debug("Force pushing local state to remote storage"); 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 newVersion = currentVersion + 1;
final var newStorageRecords = new ArrayList<SignalStorageRecord>(); final var newStorageRecords = new ArrayList<SignalStorageRecord>();
final Map<RecipientId, StorageId> newContactStorageIds; final Map<RecipientId, StorageId> newContactStorageIds;
@ -302,15 +354,16 @@ public class StorageHelper {
final var recipient = account.getRecipientStore().getRecipient(connection, recipientId); final var recipient = account.getRecipientStore().getRecipient(connection, recipientId);
final var accountRecord = StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(), final var accountRecord = StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(),
recipient, recipient,
account.getUsernameLink(), account.getUsernameLink());
storageId.getRaw()); newStorageRecords.add(new SignalStorageRecord(storageId,
newStorageRecords.add(accountRecord); new StorageRecord.Builder().account(accountRecord).build()));
} else { } else {
final var recipient = account.getRecipientStore().getRecipient(connection, recipientId); final var recipient = account.getRecipientStore().getRecipient(connection, recipientId);
final var address = recipient.getAddress().getIdentifier(); final var address = recipient.getAddress().getIdentifier();
final var identity = account.getIdentityKeyStore().getIdentityInfo(connection, address); final var identity = account.getIdentityKeyStore().getIdentityInfo(connection, address);
final var record = StorageSyncModels.localToRemoteRecord(recipient, identity, storageId.getRaw()); final var record = StorageSyncModels.localToRemoteRecord(recipient, identity);
newStorageRecords.add(record); newStorageRecords.add(new SignalStorageRecord(storageId,
new StorageRecord.Builder().contact(record).build()));
} }
} }
@ -319,8 +372,9 @@ public class StorageHelper {
for (final var groupId : groupV1Ids) { for (final var groupId : groupV1Ids) {
final var storageId = newGroupV1StorageIds.get(groupId); final var storageId = newGroupV1StorageIds.get(groupId);
final var group = account.getGroupStore().getGroup(connection, groupId); final var group = account.getGroupStore().getGroup(connection, groupId);
final var record = StorageSyncModels.localToRemoteRecord(group, storageId.getRaw()); final var record = StorageSyncModels.localToRemoteRecord(group);
newStorageRecords.add(record); newStorageRecords.add(new SignalStorageRecord(storageId,
new StorageRecord.Builder().groupV1(record).build()));
} }
final var groupV2Ids = account.getGroupStore().getGroupV2Ids(connection); final var groupV2Ids = account.getGroupStore().getGroupV2Ids(connection);
@ -328,8 +382,9 @@ public class StorageHelper {
for (final var groupId : groupV2Ids) { for (final var groupId : groupV2Ids) {
final var storageId = newGroupV2StorageIds.get(groupId); final var storageId = newGroupV2StorageIds.get(groupId);
final var group = account.getGroupStore().getGroup(connection, groupId); final var group = account.getGroupStore().getGroup(connection, groupId);
final var record = StorageSyncModels.localToRemoteRecord(group, storageId.getRaw()); final var record = StorageSyncModels.localToRemoteRecord(group);
newStorageRecords.add(record); newStorageRecords.add(new SignalStorageRecord(storageId,
new StorageRecord.Builder().groupV2(record).build()));
} }
connection.commit(); connection.commit();
@ -338,34 +393,46 @@ public class StorageHelper {
} }
final var newStorageIds = newStorageRecords.stream().map(SignalStorageRecord::getId).toList(); 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()); StorageSyncValidations.validateForcePush(manifest, newStorageRecords, account.getSelfRecipientAddress());
final Optional<SignalStorageManifest> conflict; final WriteStorageRecordsResult result;
try { if (newVersion > 1) {
if (newVersion > 1) { logger.trace("Force-pushing data. Inserting {} IDs.", newStorageRecords.size());
logger.trace("Force-pushing data. Inserting {} IDs.", newStorageRecords.size()); result = dependencies.getStorageServiceRepository()
conflict = dependencies.getAccountManager() .resetAndWriteStorageRecords(storageServiceKey, manifest, newStorageRecords);
.resetStorageRecords(storageServiceKey, manifest, newStorageRecords); } else {
} else { logger.trace("First version, normal push. Inserting {} IDs.", newStorageRecords.size());
logger.trace("First version, normal push. Inserting {} IDs.", newStorageRecords.size()); result = dependencies.getStorageServiceRepository()
conflict = dependencies.getAccountManager() .writeStorageRecords(storageServiceKey, manifest, newStorageRecords, Collections.emptyList());
.writeStorageRecords(storageServiceKey, manifest, newStorageRecords, Collections.emptyList()); }
switch (result) {
case WriteStorageRecordsResult.ConflictError ignored -> {
logger.debug("Hit a conflict. Trying again.");
throw new RetryLaterException();
} }
} catch (InvalidKeyException e) { case WriteStorageRecordsResult.NetworkError networkError -> throw networkError.getException();
logger.debug("Hit an invalid key exception, which likely indicates a conflict.", e); case WriteStorageRecordsResult.StatusCodeError statusCodeError -> throw statusCodeError.getException();
throw new RetryLaterException(); 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);
} }
if (conflict.isPresent()) {
logger.debug("Hit a conflict. Trying again.");
throw new RetryLaterException();
}
logger.debug("Force push succeeded. Updating local manifest version to: " + manifest.getVersion());
storeManifestLocally(manifest);
try (final var connection = account.getAccountDatabase().getConnection()) { try (final var connection = account.getAccountDatabase().getConnection()) {
connection.setAutoCommit(false); connection.setAutoCommit(false);
account.getRecipientStore().updateStorageIds(connection, newContactStorageIds); account.getRecipientStore().updateStorageIds(connection, newContactStorageIds);
@ -405,22 +472,35 @@ public class StorageHelper {
private void storeManifestLocally( private void storeManifestLocally(
final SignalStorageManifest remoteManifest final SignalStorageManifest remoteManifest
) { ) {
account.setStorageManifestVersion(remoteManifest.getVersion()); account.setStorageManifestVersion(remoteManifest.version);
account.setStorageManifest(remoteManifest); account.setStorageManifest(remoteManifest);
} }
private List<SignalStorageRecord> getSignalStorageRecords( private List<SignalStorageRecord> getSignalStorageRecords(
final StorageKey storageKey, final StorageKey storageKey,
final SignalStorageManifest manifest,
final List<StorageId> storageIds final List<StorageId> storageIds
) throws IOException { ) throws IOException {
List<SignalStorageRecord> records; final var result = dependencies.getStorageServiceRepository()
try { .readStorageRecords(storageKey, manifest.recordIkm, storageIds);
records = dependencies.getAccountManager().readStorageRecords(storageKey, storageIds); return switch (result) {
} catch (InvalidKeyException e) { case StorageServiceRepository.StorageRecordResult.DecryptionError decryptionError -> {
logger.warn("Failed to read storage records, ignoring."); if (decryptionError.getException() instanceof InvalidKeyException) {
return List.of(); logger.warn("Failed to read storage records, ignoring.");
} yield List.of();
return records; } 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 { private List<StorageId> getAllLocalStorageIds(final Connection connection) throws SQLException {
@ -436,12 +516,10 @@ public class StorageHelper {
final Connection connection, final Connection connection,
final List<StorageId> storageIds final List<StorageId> storageIds
) throws SQLException { ) throws SQLException {
final var records = new ArrayList<SignalStorageRecord>(); final var records = new ArrayList<SignalStorageRecord>(storageIds.size());
for (final var storageId : storageIds) { for (final var storageId : storageIds) {
final var record = buildLocalStorageRecord(connection, storageId); final var record = buildLocalStorageRecord(connection, storageId);
if (record != null) { records.add(record);
records.add(record);
}
} }
return records; return records;
} }
@ -455,25 +533,31 @@ public class StorageHelper {
final var recipient = account.getRecipientStore().getRecipient(connection, storageId); final var recipient = account.getRecipientStore().getRecipient(connection, storageId);
final var address = recipient.getAddress().getIdentifier(); final var address = recipient.getAddress().getIdentifier();
final var identity = account.getIdentityKeyStore().getIdentityInfo(connection, address); 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 -> { case ManifestRecord.Identifier.Type.GROUPV1 -> {
final var groupV1 = account.getGroupStore().getGroupV1(connection, storageId); 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 -> { case ManifestRecord.Identifier.Type.GROUPV2 -> {
final var groupV2 = account.getGroupStore().getGroupV2(connection, storageId); 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 -> { case ManifestRecord.Identifier.Type.ACCOUNT -> {
final var selfRecipient = account.getRecipientStore() final var selfRecipient = account.getRecipientStore()
.getRecipient(connection, account.getSelfRecipientId()); .getRecipient(connection, account.getSelfRecipientId());
yield StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(),
final var record = StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(),
selfRecipient, selfRecipient,
account.getUsernameLink(), account.getUsernameLink());
storageId.getRaw()); 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); final var groupV2RecordProcessor = new GroupV2RecordProcessor(account, connection);
for (final var record : records) { for (final var record : records) {
logger.debug("Reading record of type {}", record.getType()); if (record.getProto().account != null) {
switch (ManifestRecord.Identifier.Type.fromValue(record.getType())) { logger.debug("Reading record {} of type account", record.getId());
case ACCOUNT -> accountRecordProcessor.process(record.getAccount().get()); accountRecordProcessor.process(StorageRecordConvertersKt.toSignalAccountRecord(record.getProto().account,
case GROUPV1 -> groupV1RecordProcessor.process(record.getGroupV1().get()); record.getId()));
case GROUPV2 -> groupV2RecordProcessor.process(record.getGroupV2().get()); } else if (record.getProto().groupV1 != null) {
case CONTACT -> contactRecordProcessor.process(record.getContact().get()); logger.debug("Reading record {} of type groupV1", record.getId());
case null, default -> unknownRecords.add(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());
} }
} }

View file

@ -247,10 +247,14 @@ public class SyncHelper {
} }
public SendMessageResult sendBlockedList() { public SendMessageResult sendBlockedList() {
var addresses = new ArrayList<SignalServiceAddress>(); var addresses = new ArrayList<BlockedListMessage.Individual>();
for (var record : account.getContactStore().getContacts()) { for (var record : account.getContactStore().getContacts()) {
if (record.second().isBlocked()) { 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[]>(); var groupIds = new ArrayList<byte[]>();
@ -276,8 +280,10 @@ public class SyncHelper {
} }
public SendMessageResult sendKeysMessage() { public SendMessageResult sendKeysMessage() {
var keysMessage = new KeysMessage(Optional.ofNullable(account.getOrCreateStorageKey()), var keysMessage = new KeysMessage(account.getOrCreateStorageKey(),
Optional.ofNullable(account.getOrCreatePinMasterKey())); account.getOrCreatePinMasterKey(),
account.getOrCreateAccountEntropyPool(),
account.getOrCreateMediaRootBackupKey());
return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forKeys(keysMessage)); return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forKeys(keysMessage));
} }
@ -405,7 +411,7 @@ public class SyncHelper {
builder.withMessageExpirationTimeVersion(c.getExpirationTimerVersion().get()); builder.withMessageExpirationTimeVersion(c.getExpirationTimerVersion().get());
} else { } else {
logger.debug( 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, recipientId,
c.getExpirationTimerVersion(), c.getExpirationTimerVersion(),
contact == null ? 1 : contact.messageExpirationTimeVersion()); contact == null ? 1 : contact.messageExpirationTimeVersion());

View 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.ClientZkOperations;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; 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.ServiceIdType;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.registration.RegistrationApi; import org.whispersystems.signalservice.api.registration.RegistrationApi;
import org.whispersystems.signalservice.api.services.ProfileService; 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.svr.SecureValueRecovery;
import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.api.util.UptimeSleepTimer; import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
@ -49,6 +52,8 @@ public class SignalDependencies {
private SignalServiceAccountManager accountManager; private SignalServiceAccountManager accountManager;
private GroupsV2Api groupsV2Api; private GroupsV2Api groupsV2Api;
private RegistrationApi registrationApi; private RegistrationApi registrationApi;
private LinkDeviceApi linkDeviceApi;
private StorageServiceApi storageServiceApi;
private GroupsV2Operations groupsV2Operations; private GroupsV2Operations groupsV2Operations;
private ClientZkOperations clientZkOperations; private ClientZkOperations clientZkOperations;
@ -155,6 +160,19 @@ public class SignalDependencies {
return getOrCreate(() -> registrationApi, () -> registrationApi = getAccountManager().getRegistrationApi()); 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() { public GroupsV2Operations getGroupsV2Operations() {
return getOrCreate(() -> groupsV2Operations, return getOrCreate(() -> groupsV2Operations,
() -> groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.signalServiceConfiguration()), () -> groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.signalServiceConfiguration()),

View file

@ -8,13 +8,27 @@ import java.io.IOException;
public class SyncStorageJob implements Job { public class SyncStorageJob implements Job {
private final boolean forcePush;
private static final Logger logger = LoggerFactory.getLogger(SyncStorageJob.class); private static final Logger logger = LoggerFactory.getLogger(SyncStorageJob.class);
public SyncStorageJob() {
this.forcePush = false;
}
public SyncStorageJob(final boolean forcePush) {
this.forcePush = forcePush;
}
@Override @Override
public void run(Context context) { public void run(Context context) {
logger.trace("Running storage sync job"); logger.trace("Running storage sync job");
try { try {
context.getStorageHelper().syncDataWithStorage(); if (forcePush) {
context.getStorageHelper().forcePushToStorage();
} else {
context.getStorageHelper().syncDataWithStorage();
}
} catch (IOException e) { } catch (IOException e) {
logger.warn("Failed to sync storage data", e); logger.warn("Failed to sync storage data", e);
} }

View file

@ -65,10 +65,12 @@ import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.AccountEntropyPool;
import org.whispersystems.signalservice.api.SignalServiceAccountDataStore; import org.whispersystems.signalservice.api.SignalServiceAccountDataStore;
import org.whispersystems.signalservice.api.SignalServiceDataStore; import org.whispersystems.signalservice.api.SignalServiceDataStore;
import org.whispersystems.signalservice.api.account.AccountAttributes; import org.whispersystems.signalservice.api.account.AccountAttributes;
import org.whispersystems.signalservice.api.account.PreKeyCollection; 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.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.ServiceId;
@ -138,6 +140,8 @@ public class SignalAccount implements Closeable {
private String registrationLockPin; private String registrationLockPin;
private MasterKey pinMasterKey; private MasterKey pinMasterKey;
private StorageKey storageKey; private StorageKey storageKey;
private AccountEntropyPool accountEntropyPool;
private MediaRootBackupKey mediaRootBackupKey;
private ProfileKey profileKey; private ProfileKey profileKey;
private Settings settings; private Settings settings;
@ -305,6 +309,7 @@ public class SignalAccount implements Closeable {
this.isMultiDevice = true; this.isMultiDevice = true;
setLastReceiveTimestamp(0L); setLastReceiveTimestamp(0L);
this.pinMasterKey = masterKey; this.pinMasterKey = masterKey;
this.accountEntropyPool = null;
getKeyValueStore().storeEntry(storageManifestVersion, -1L); getKeyValueStore().storeEntry(storageManifestVersion, -1L);
this.setStorageManifest(null); this.setStorageManifest(null);
this.storageKey = null; this.storageKey = null;
@ -339,6 +344,7 @@ public class SignalAccount implements Closeable {
final PreKeyCollection pniPreKeys final PreKeyCollection pniPreKeys
) { ) {
this.pinMasterKey = masterKey; this.pinMasterKey = masterKey;
this.accountEntropyPool = null;
getKeyValueStore().storeEntry(storageManifestVersion, -1L); getKeyValueStore().storeEntry(storageManifestVersion, -1L);
this.setStorageManifest(null); this.setStorageManifest(null);
this.storageKey = null; this.storageKey = null;
@ -499,6 +505,12 @@ public class SignalAccount implements Closeable {
if (storage.storageKey != null) { if (storage.storageKey != null) {
storageKey = new StorageKey(base64.decode(storage.storageKey)); 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) { if (storage.profileKey != null) {
try { try {
profileKey = new ProfileKey(base64.decode(storage.profileKey)); profileKey = new ProfileKey(base64.decode(storage.profileKey));
@ -981,6 +993,8 @@ public class SignalAccount implements Closeable {
registrationLockPin, registrationLockPin,
pinMasterKey == null ? null : base64.encodeToString(pinMasterKey.serialize()), pinMasterKey == null ? null : base64.encodeToString(pinMasterKey.serialize()),
storageKey == null ? null : base64.encodeToString(storageKey.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()), profileKey == null ? null : base64.encodeToString(profileKey.serialize()),
usernameLink == null ? null : base64.encodeToString(usernameLink.getEntropy()), usernameLink == null ? null : base64.encodeToString(usernameLink.getEntropy()),
usernameLink == null ? null : usernameLink.getServerId().toString()); usernameLink == null ? null : usernameLink.getServerId().toString());
@ -1442,6 +1456,10 @@ public class SignalAccount implements Closeable {
return selfRecipientId; return selfRecipientId;
} }
public Profile getSelfRecipientProfile() {
return recipientStore.getProfile(selfRecipientId);
}
public String getSessionId(final String forNumber) { public String getSessionId(final String forNumber) {
final var keyValueStore = getKeyValueStore(); final var keyValueStore = getKeyValueStore();
final var sessionNumber = keyValueStore.getEntry(verificationSessionNumber); final var sessionNumber = keyValueStore.getEntry(verificationSessionNumber);
@ -1512,31 +1530,50 @@ public class SignalAccount implements Closeable {
public MasterKey getPinBackedMasterKey() { public MasterKey getPinBackedMasterKey() {
if (registrationLockPin == null) { if (registrationLockPin == null) {
return null; return null;
} else if (!isPrimaryDevice()) {
return getMasterKey();
} }
return pinMasterKey; return getOrCreatePinMasterKey();
} }
public MasterKey getOrCreatePinMasterKey() { public MasterKey getOrCreatePinMasterKey() {
if (pinMasterKey == null) { final var key = getMasterKey();
pinMasterKey = KeyUtils.createMasterKey(); if (key != null) {
save(); return key;
} }
pinMasterKey = KeyUtils.createMasterKey();
save();
return pinMasterKey; return pinMasterKey;
} }
private MasterKey getMasterKey() {
if (pinMasterKey != null) {
return pinMasterKey;
} else if (accountEntropyPool != null) {
return accountEntropyPool.deriveMasterKey();
}
return null;
}
public void setMasterKey(MasterKey masterKey) { public void setMasterKey(MasterKey masterKey) {
if (isPrimaryDevice()) { if (isPrimaryDevice()) {
return; return;
} }
this.pinMasterKey = masterKey; this.pinMasterKey = masterKey;
if (masterKey != null) {
this.storageKey = null;
}
save(); save();
} }
public StorageKey getOrCreateStorageKey() { public StorageKey getOrCreateStorageKey() {
if (pinMasterKey != null) { if (storageKey != null) {
return pinMasterKey.deriveStorageServiceKey();
} else if (storageKey != null) {
return storageKey; return storageKey;
} else if (pinMasterKey != null) {
return pinMasterKey.deriveStorageServiceKey();
} else if (accountEntropyPool != null) {
return accountEntropyPool.deriveMasterKey().deriveStorageServiceKey();
} else if (!isPrimaryDevice() || !isMultiDevice()) { } else if (!isPrimaryDevice() || !isMultiDevice()) {
// Only upload storage, if a pin master key already exists or linked devices exist // Only upload storage, if a pin master key already exists or linked devices exist
return null; return null;
@ -1553,6 +1590,40 @@ public class SignalAccount implements Closeable {
save(); 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() { public String getRecoveryPassword() {
final var masterKey = getPinBackedMasterKey(); final var masterKey = getPinBackedMasterKey();
if (masterKey == null) { if (masterKey == null) {
@ -1575,7 +1646,7 @@ public class SignalAccount implements Closeable {
return Optional.empty(); return Optional.empty();
} }
try (var inputStream = new FileInputStream(storageManifestFile)) { try (var inputStream = new FileInputStream(storageManifestFile)) {
return Optional.of(SignalStorageManifest.deserialize(inputStream.readAllBytes())); return Optional.of(SignalStorageManifest.Companion.deserialize(inputStream.readAllBytes()));
} catch (IOException e) { } catch (IOException e) {
logger.warn("Failed to read local storage manifest.", e); logger.warn("Failed to read local storage manifest.", e);
return Optional.empty(); return Optional.empty();
@ -1882,6 +1953,8 @@ public class SignalAccount implements Closeable {
String registrationLockPin, String registrationLockPin,
String pinMasterKey, String pinMasterKey,
String storageKey, String storageKey,
String accountEntropyPool,
String mediaRootBackupKey,
String profileKey, String profileKey,
String usernameLinkEntropy, String usernameLinkEntropy,
String usernameLinkServerId String usernameLinkServerId

View file

@ -12,8 +12,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.UsernameLinkComponents; import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
import org.whispersystems.signalservice.api.storage.SignalAccountRecord; 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.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
import org.whispersystems.signalservice.internal.storage.protos.OptionalBool; import org.whispersystems.signalservice.internal.storage.protos.OptionalBool;
import java.sql.Connection; import java.sql.Connection;
@ -21,6 +22,9 @@ import java.sql.SQLException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Optional; 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. * Processes {@link SignalAccountRecord}s.
*/ */
@ -43,10 +47,10 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
final var selfRecipientId = account.getSelfRecipientId(); final var selfRecipientId = account.getSelfRecipientId();
final var recipient = account.getRecipientStore().getRecipient(connection, selfRecipientId); final var recipient = account.getRecipientStore().getRecipient(connection, selfRecipientId);
final var storageId = account.getRecipientStore().getSelfStorageId(connection); final var storageId = account.getRecipientStore().getSelfStorageId(connection);
this.localAccountRecord = StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(), this.localAccountRecord = new SignalAccountRecord(storageId,
recipient, StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(),
account.getUsernameLink(), recipient,
storageId.getRaw()).getAccount().get(); account.getUsernameLink()));
} }
@Override @Override
@ -60,99 +64,73 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
} }
@Override @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 givenName;
String familyName; String familyName;
if (remote.getGivenName().isPresent() || remote.getFamilyName().isPresent()) { if (!remote.givenName.isEmpty() || !remote.familyName.isEmpty()) {
givenName = remote.getGivenName().orElse(""); givenName = remote.givenName;
familyName = remote.getFamilyName().orElse(""); familyName = remote.familyName;
} else { } else {
givenName = local.getGivenName().orElse(""); givenName = local.givenName;
familyName = local.getFamilyName().orElse(""); familyName = local.familyName;
} }
final var payments = remote.getPayments().getEntropy().isPresent() ? remote.getPayments() : local.getPayments(); final var mergedBuilder = SignalAccountRecord.Companion.newBuilder(remote.unknownFields().toByteArray())
final var subscriber = remote.getSubscriber().getId().isPresent() .givenName(givenName)
? remote.getSubscriber() .familyName(familyName)
: local.getSubscriber(); .avatarUrlPath(firstNonEmpty(remote.avatarUrlPath, local.avatarUrlPath))
final var storyViewReceiptsState = remote.getStoryViewReceiptsState() == OptionalBool.UNSET .profileKey(firstNonEmpty(remote.profileKey, local.profileKey))
? local.getStoryViewReceiptsState() .noteToSelfArchived(remote.noteToSelfArchived)
: remote.getStoryViewReceiptsState(); .noteToSelfMarkedUnread(remote.noteToSelfMarkedUnread)
final var unknownFields = remote.serializeUnknownFields(); .readReceipts(remote.readReceipts)
final var avatarUrlPath = OptionalUtil.or(remote.getAvatarUrlPath(), local.getAvatarUrlPath()).orElse(""); .typingIndicators(remote.typingIndicators)
final var profileKey = OptionalUtil.or(remote.getProfileKey(), local.getProfileKey()).orElse(null); .sealedSenderIndicators(remote.sealedSenderIndicators)
final var noteToSelfArchived = remote.isNoteToSelfArchived(); .linkPreviews(remote.linkPreviews)
final var noteToSelfForcedUnread = remote.isNoteToSelfForcedUnread(); .unlistedPhoneNumber(remote.unlistedPhoneNumber)
final var readReceipts = remote.isReadReceiptsEnabled(); .phoneNumberSharingMode(remote.phoneNumberSharingMode)
final var typingIndicators = remote.isTypingIndicatorsEnabled(); .pinnedConversations(remote.pinnedConversations)
final var sealedSenderIndicators = remote.isSealedSenderIndicatorsEnabled(); .preferContactAvatars(remote.preferContactAvatars)
final var linkPreviews = remote.isLinkPreviewsEnabled(); .universalExpireTimer(remote.universalExpireTimer)
final var unlisted = remote.isPhoneNumberUnlisted(); .preferredReactionEmoji(firstNonEmpty(remote.preferredReactionEmoji, local.preferredReactionEmoji))
final var pinnedConversations = remote.getPinnedConversations(); .subscriberId(firstNonEmpty(remote.subscriberId, local.subscriberId))
final var phoneNumberSharingMode = remote.getPhoneNumberSharingMode(); .subscriberCurrencyCode(firstNonEmpty(remote.subscriberCurrencyCode, local.subscriberCurrencyCode))
final var preferContactAvatars = remote.isPreferContactAvatars(); .backupsSubscriberId(firstNonEmpty(remote.backupsSubscriberId, local.backupsSubscriberId))
final var universalExpireTimer = remote.getUniversalExpireTimer(); .backupsSubscriberCurrencyCode(firstNonEmpty(remote.backupsSubscriberCurrencyCode,
final var e164 = account.isPrimaryDevice() ? local.getE164() : remote.getE164(); local.backupsSubscriberCurrencyCode))
final var defaultReactions = !remote.getDefaultReactions().isEmpty() .displayBadgesOnProfile(remote.displayBadgesOnProfile)
? remote.getDefaultReactions() .subscriptionManuallyCancelled(remote.subscriptionManuallyCancelled)
: local.getDefaultReactions(); .keepMutedChatsArchived(remote.keepMutedChatsArchived)
final var displayBadgesOnProfile = remote.isDisplayBadgesOnProfile(); .hasSetMyStoriesPrivacy(remote.hasSetMyStoriesPrivacy)
final var subscriptionManuallyCancelled = remote.isSubscriptionManuallyCancelled(); .hasViewedOnboardingStory(remote.hasViewedOnboardingStory || local.hasViewedOnboardingStory)
final var keepMutedChatsArchived = remote.isKeepMutedChatsArchived(); .storiesDisabled(remote.storiesDisabled)
final var hasSetMyStoriesPrivacy = remote.hasSetMyStoriesPrivacy(); .hasSeenGroupStoryEducationSheet(remote.hasSeenGroupStoryEducationSheet
final var hasViewedOnboardingStory = remote.hasViewedOnboardingStory() || local.hasViewedOnboardingStory(); || local.hasSeenGroupStoryEducationSheet)
final var storiesDisabled = remote.isStoriesDisabled(); .hasCompletedUsernameOnboarding(remote.hasCompletedUsernameOnboarding
final var hasSeenGroupStoryEducation = remote.hasSeenGroupStoryEducationSheet() || local.hasCompletedUsernameOnboarding)
|| local.hasSeenGroupStoryEducationSheet(); .storyViewReceiptsEnabled(remote.storyViewReceiptsEnabled == OptionalBool.UNSET
boolean hasSeenUsernameOnboarding = remote.hasCompletedUsernameOnboarding() ? local.storyViewReceiptsEnabled
|| local.hasCompletedUsernameOnboarding(); : remote.storyViewReceiptsEnabled)
final var username = remote.getUsername(); .username(remote.username)
final var usernameLink = remote.getUsernameLink(); .usernameLink(remote.usernameLink)
.e164(account.isPrimaryDevice() ? local.e164 : remote.e164);
final var mergedBuilder = new SignalAccountRecord.Builder(remote.getId().getRaw(), unknownFields).setGivenName( if (firstNonNull(remote.payments, local.payments) != null) {
givenName) mergedBuilder.payments(firstNonNull(remote.payments, local.payments));
.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 merged = mergedBuilder.build(); final var merged = mergedBuilder.build();
final var matchesRemote = doProtosMatch(merged, remote); final var matchesRemote = doProtosMatch(merged, remote);
if (matchesRemote) { if (matchesRemote) {
return remote; return remoteRecord;
} }
final var matchesLocal = doProtosMatch(merged, local); final var matchesLocal = doProtosMatch(merged, local);
if (matchesLocal) { if (matchesLocal) {
return local; return localRecord;
} }
return mergedBuilder.setId(KeyUtils.createRawStorageId()).build(); return new SignalAccountRecord(StorageId.forAccount(KeyUtils.createRawStorageId()), mergedBuilder.build());
} }
@Override @Override
@ -164,56 +142,55 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
@Override @Override
protected void updateLocal(StorageRecordUpdate<SignalAccountRecord> update) throws SQLException { protected void updateLocal(StorageRecordUpdate<SignalAccountRecord> update) throws SQLException {
final var accountRecord = update.newRecord(); 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()); jobExecutor.enqueueJob(new CheckWhoAmIJob());
} }
account.getConfigurationStore().setReadReceipts(connection, accountRecord.isReadReceiptsEnabled()); account.getConfigurationStore().setReadReceipts(connection, accountProto.readReceipts);
account.getConfigurationStore().setTypingIndicators(connection, accountRecord.isTypingIndicatorsEnabled()); account.getConfigurationStore().setTypingIndicators(connection, accountProto.typingIndicators);
account.getConfigurationStore() account.getConfigurationStore()
.setUnidentifiedDeliveryIndicators(connection, accountRecord.isSealedSenderIndicatorsEnabled()); .setUnidentifiedDeliveryIndicators(connection, accountProto.sealedSenderIndicators);
account.getConfigurationStore().setLinkPreviews(connection, accountRecord.isLinkPreviewsEnabled()); account.getConfigurationStore().setLinkPreviews(connection, accountProto.linkPreviews);
account.getConfigurationStore() account.getConfigurationStore()
.setPhoneNumberSharingMode(connection, .setPhoneNumberSharingMode(connection,
StorageSyncModels.remoteToLocal(accountRecord.getPhoneNumberSharingMode())); StorageSyncModels.remoteToLocal(accountProto.phoneNumberSharingMode));
account.getConfigurationStore().setPhoneNumberUnlisted(connection, accountRecord.isPhoneNumberUnlisted()); account.getConfigurationStore().setPhoneNumberUnlisted(connection, accountProto.unlistedPhoneNumber);
account.setUsername(accountRecord.getUsername() != null && !accountRecord.getUsername().isEmpty() account.setUsername(!accountProto.username.isEmpty() ? accountProto.username : null);
? accountRecord.getUsername() if (accountProto.usernameLink != null) {
: null); final var usernameLink = accountProto.usernameLink;
if (accountRecord.getUsernameLink() != null) {
final var usernameLink = accountRecord.getUsernameLink();
account.setUsernameLink(new UsernameLinkComponents(usernameLink.entropy.toByteArray(), account.setUsernameLink(new UsernameLinkComponents(usernameLink.entropy.toByteArray(),
UuidUtil.parseOrThrow(usernameLink.serverId.toByteArray()))); UuidUtil.parseOrThrow(usernameLink.serverId.toByteArray())));
account.getConfigurationStore().setUsernameLinkColor(connection, usernameLink.color.name()); account.getConfigurationStore().setUsernameLinkColor(connection, usernameLink.color.name());
} }
if (accountRecord.getProfileKey().isPresent()) { if (accountProto.profileKey.size() > 0) {
ProfileKey profileKey; ProfileKey profileKey;
try { try {
profileKey = new ProfileKey(accountRecord.getProfileKey().get()); profileKey = new ProfileKey(accountProto.profileKey.toByteArray());
} catch (InvalidInputException e) { } catch (InvalidInputException e) {
logger.debug("Received invalid profile key from storage"); logger.debug("Received invalid profile key from storage");
profileKey = null; profileKey = null;
} }
if (profileKey != null) { if (profileKey != null) {
account.setProfileKey(profileKey); account.setProfileKey(profileKey);
final var avatarPath = accountRecord.getAvatarUrlPath().orElse(null); final var avatarPath = accountProto.avatarUrlPath.isEmpty() ? null : accountProto.avatarUrlPath;
jobExecutor.enqueueJob(new DownloadProfileAvatarJob(avatarPath)); jobExecutor.enqueueJob(new DownloadProfileAvatarJob(avatarPath));
} }
} }
final var profile = account.getRecipientStore().getProfile(connection, account.getSelfRecipientId()); final var profile = account.getRecipientStore().getProfile(connection, account.getSelfRecipientId());
final var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile); final var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile);
builder.withGivenName(accountRecord.getGivenName().orElse(null)); builder.withGivenName(accountProto.givenName);
builder.withFamilyName(accountRecord.getFamilyName().orElse(null)); builder.withFamilyName(accountProto.familyName);
account.getRecipientStore().storeProfile(connection, account.getSelfRecipientId(), builder.build()); account.getRecipientStore().storeProfile(connection, account.getSelfRecipientId(), builder.build());
account.getRecipientStore() account.getRecipientStore()
.storeStorageRecord(connection, .storeStorageRecord(connection,
account.getSelfRecipientId(), account.getSelfRecipientId(),
accountRecord.getId(), accountRecord.getId(),
accountRecord.toProto().encode()); accountProto.encode());
} }
@Override @Override
@ -221,7 +198,7 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
return 0; return 0;
} }
private static boolean doProtosMatch(SignalAccountRecord merged, SignalAccountRecord other) { private static boolean doProtosMatch(AccountRecord merged, AccountRecord other) {
return Arrays.equals(merged.toProto().encode(), other.toProto().encode()); return Arrays.equals(merged.encode(), other.encode());
} }
} }

View file

@ -17,7 +17,8 @@ import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.api.push.ServiceId.PNI; import org.whispersystems.signalservice.api.push.ServiceId.PNI;
import org.whispersystems.signalservice.api.storage.SignalContactRecord; 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 org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
import java.sql.Connection; import java.sql.Connection;
@ -27,6 +28,11 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.regex.Pattern; 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> { public class ContactRecordProcessor extends DefaultStorageRecordProcessor<SignalContactRecord> {
private static final Logger logger = LoggerFactory.getLogger(ContactRecordProcessor.class); 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. * - You can't have a contact record for yourself. That should be an account record.
*/ */
@Override @Override
protected boolean isInvalid(SignalContactRecord remote) { protected boolean isInvalid(SignalContactRecord remoteRecord) {
boolean hasAci = remote.getAci().isPresent() && remote.getAci().get().isValid(); final var remote = remoteRecord.getProto();
boolean hasPni = remote.getPni().isPresent() && remote.getPni().get().isValid(); 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) { if (!hasAci && !hasPni) {
logger.debug("Found a ContactRecord with neither an ACI nor a PNI -- marking as invalid."); logger.debug("Found a ContactRecord with neither an ACI nor a PNI -- marking as invalid.");
return true; return true;
} else if (selfAci != null && selfAci.equals(remote.getAci().orElse(null)) || ( } else if (selfAci != null && selfAci.equals(aci) || (
selfPni != null && selfPni.equals(remote.getPni().orElse(null)) selfPni != null && selfPni.equals(pni)
) || (selfNumber != null && selfNumber.equals(remote.getNumber().orElse(null)))) { ) || (selfNumber != null && selfNumber.equals(e164))) {
logger.debug("Found a ContactRecord for ourselves -- marking as invalid."); logger.debug("Found a ContactRecord for ourselves -- marking as invalid.");
return true; return true;
} else if (remote.getNumber().isPresent() && !isValidE164(remote.getNumber().get())) { } else if (e164 != null && !isValidE164(e164)) {
logger.debug("Found a record with an invalid E164. Marking as invalid."); logger.debug("Found a record with an invalid E164 ({}). Marking as invalid.", e164);
return true; return true;
} else { } else {
return false; return false;
@ -77,7 +87,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
@Override @Override
protected Optional<SignalContactRecord> getMatching(SignalContactRecord remote) throws SQLException { 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 recipientId = account.getRecipientStore().resolveRecipient(connection, address);
final var recipient = account.getRecipientStore().getRecipient(connection, recipientId); 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 identity = account.getIdentityKeyStore().getIdentityInfo(connection, identifier);
final var storageId = account.getRecipientStore().getStorageId(connection, recipientId); final var storageId = account.getRecipientStore().getStorageId(connection, recipientId);
return Optional.of(StorageSyncModels.localToRemoteRecord(recipient, identity, storageId.getRaw()) return Optional.of(new SignalContactRecord(storageId,
.getContact() StorageSyncModels.localToRemoteRecord(recipient, identity)));
.get());
} }
@Override @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 profileGivenName;
String profileFamilyName; String profileFamilyName;
if (remote.getProfileGivenName().isPresent() || remote.getProfileFamilyName().isPresent()) { if (!remote.givenName.isEmpty() || !remote.familyName.isEmpty()) {
profileGivenName = remote.getProfileGivenName().orElse(""); profileGivenName = remote.givenName;
profileFamilyName = remote.getProfileFamilyName().orElse(""); profileFamilyName = remote.familyName;
} else { } else {
profileGivenName = local.getProfileGivenName().orElse(""); profileGivenName = local.givenName;
profileFamilyName = local.getProfileFamilyName().orElse(""); profileFamilyName = local.familyName;
} }
IdentityState identityState; IdentityState identityState;
byte[] identityKey; ByteString identityKey;
if (remote.getIdentityKey().isPresent() && ( if (remote.identityKey.size() > 0 && (
remote.getIdentityState() != local.getIdentityState() !account.isPrimaryDevice()
|| local.getIdentityKey().isEmpty() || remote.identityState != local.identityState
|| !account.isPrimaryDevice() || local.identityKey.size() == 0
)) { )) {
identityState = remote.getIdentityState(); identityState = remote.identityState;
identityKey = remote.getIdentityKey().get(); identityKey = remote.identityKey;
} else { } else {
identityState = local.getIdentityState(); identityState = local.identityState;
identityKey = local.getIdentityKey().orElse(null); identityKey = local.identityKey.size() > 0 ? local.identityKey : ByteString.EMPTY;
} }
if (local.getAci().isPresent() if (!local.aci.isEmpty()
&& local.getIdentityKey().isPresent() && local.identityKey.size() > 0
&& remote.getIdentityKey().isPresent() && remote.identityKey.size() > 0
&& !Arrays.equals(local.getIdentityKey().get(), remote.getIdentityKey().get())) { && !local.identityKey.equals(remote.identityKey)) {
logger.debug("The local and remote identity keys do not match for {}. Enqueueing a profile fetch.", 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); final var address = getRecipientAddress(local);
jobExecutor.enqueueJob(new DownloadProfileJob(address)); jobExecutor.enqueueJob(new DownloadProfileJob(address));
} }
final var e164sMatchButPnisDont = local.getNumber().isPresent() String pni;
&& 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 e164; String e164;
if (!account.isPrimaryDevice() && (e164sMatchButPnisDont || pnisMatchButE164sDont)) { if (account.isPrimaryDevice()) {
if (e164sMatchButPnisDont) { final var e164sMatchButPnisDont = !local.e164.isEmpty()
logger.debug("Matching E164s, but the PNIs differ! Trusting our local pair."); && local.e164.equals(remote.e164)
} else if (pnisMatchButE164sDont) { && !local.pni.isEmpty()
logger.debug("Matching PNIs, but the E164s differ! Trusting our local pair."); && !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 { } else {
pni = OptionalUtil.or(remote.getPni(), local.getPni()).orElse(null); pni = firstNonEmpty(remote.pni, local.pni);
e164 = OptionalUtil.or(remote.getNumber(), local.getNumber()).orElse(null); e164 = firstNonEmpty(remote.e164, local.e164);
} }
final var unknownFields = remote.serializeUnknownFields(); final var mergedBuilder = SignalContactRecord.Companion.newBuilder(remote.unknownFields().toByteArray())
final var aci = local.getAci().isEmpty() ? remote.getAci().orElse(null) : local.getAci().get(); .aci(local.aci.isEmpty() ? remote.aci : local.aci)
final var profileKey = OptionalUtil.or(remote.getProfileKey(), local.getProfileKey()).orElse(null); .e164(e164)
final var username = OptionalUtil.or(remote.getUsername(), local.getUsername()).orElse(""); .pni(pni)
final var blocked = remote.isBlocked(); .givenName(profileGivenName)
final var profileSharing = remote.isProfileSharingEnabled(); .familyName(profileFamilyName)
final var archived = remote.isArchived(); .systemGivenName(account.isPrimaryDevice() ? local.systemGivenName : remote.systemGivenName)
final var forcedUnread = remote.isForcedUnread(); .systemFamilyName(account.isPrimaryDevice() ? local.systemFamilyName : remote.systemFamilyName)
final var muteUntil = remote.getMuteUntil(); .systemNickname(remote.systemNickname)
final var hideStory = remote.shouldHideStory(); .profileKey(firstNonEmpty(remote.profileKey, local.profileKey))
final var unregisteredTimestamp = remote.getUnregisteredTimestamp(); .username(firstNonEmpty(remote.username, local.username))
final var hidden = remote.isHidden(); .identityState(identityState)
final var systemGivenName = account.isPrimaryDevice() .identityKey(identityKey)
? local.getSystemGivenName().orElse("") .blocked(remote.blocked)
: remote.getSystemGivenName().orElse(""); .whitelisted(remote.whitelisted)
final var systemFamilyName = account.isPrimaryDevice() .archived(remote.archived)
? local.getSystemFamilyName().orElse("") .markedUnread(remote.markedUnread)
: remote.getSystemFamilyName().orElse(""); .mutedUntilTimestamp(remote.mutedUntilTimestamp)
final var systemNickname = remote.getSystemNickname().orElse(""); .hideStory(remote.hideStory)
final var nicknameGivenName = remote.getNicknameGivenName().orElse(""); .unregisteredAtTimestamp(remote.unregisteredAtTimestamp)
final var nicknameFamilyName = remote.getNicknameFamilyName().orElse(""); .hidden(remote.hidden)
final var pniSignatureVerified = remote.isPniSignatureVerified() || local.isPniSignatureVerified(); .pniSignatureVerified(remote.pniSignatureVerified || local.pniSignatureVerified)
final var note = remote.getNote().or(local::getNote).orElse(""); .nickname(remote.nickname)
.note(remote.note);
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 merged = mergedBuilder.build(); final var merged = mergedBuilder.build();
final var matchesRemote = doProtosMatch(merged, remote); final var matchesRemote = doProtosMatch(merged, remote);
if (matchesRemote) { if (matchesRemote) {
return remote; return remoteRecord;
} }
final var matchesLocal = doProtosMatch(merged, local); final var matchesLocal = doProtosMatch(merged, local);
if (matchesLocal) { if (matchesLocal) {
return local; return localRecord;
} }
return mergedBuilder.setId(KeyUtils.createRawStorageId()).build(); return new SignalContactRecord(StorageId.forContact(KeyUtils.createRawStorageId()), mergedBuilder.build());
} }
@Override @Override
@ -231,7 +220,8 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
@Override @Override
protected void updateLocal(StorageRecordUpdate<SignalContactRecord> update) throws SQLException { protected void updateLocal(StorageRecordUpdate<SignalContactRecord> update) throws SQLException {
final var contactRecord = update.newRecord(); 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 recipientId = account.getRecipientStore().resolveRecipientTrusted(connection, address);
final var recipient = account.getRecipientStore().getRecipient(connection, recipientId); 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 contactNickGivenName = contact == null ? null : contact.nickNameGivenName();
final var contactNickFamilyName = contact == null ? null : contact.nickNameFamilyName(); final var contactNickFamilyName = contact == null ? null : contact.nickNameFamilyName();
final var contactNote = contact == null ? null : contact.note(); final var contactNote = contact == null ? null : contact.note();
if (blocked != contactRecord.isBlocked() if (blocked != contactProto.blocked
|| profileShared != contactRecord.isProfileSharingEnabled() || profileShared != contactProto.whitelisted
|| archived != contactRecord.isArchived() || archived != contactProto.archived
|| hidden != contactRecord.isHidden() || hidden != contactProto.hidden
|| hideStory != contactRecord.shouldHideStory() || hideStory != contactProto.hideStory
|| muteUntil != contactRecord.getMuteUntil() || muteUntil != contactProto.mutedUntilTimestamp
|| unregisteredTimestamp != contactRecord.getUnregisteredTimestamp() || unregisteredTimestamp != contactProto.unregisteredAtTimestamp
|| !Objects.equals(contactRecord.getSystemGivenName().orElse(null), contactGivenName) || !Objects.equals(nullIfEmpty(contactProto.systemGivenName), contactGivenName)
|| !Objects.equals(contactRecord.getSystemFamilyName().orElse(null), contactFamilyName) || !Objects.equals(nullIfEmpty(contactProto.systemFamilyName), contactFamilyName)
|| !Objects.equals(contactRecord.getSystemNickname().orElse(null), contactNickName) || !Objects.equals(nullIfEmpty(contactProto.systemNickname), contactNickName)
|| !Objects.equals(contactRecord.getNicknameGivenName().orElse(null), contactNickGivenName) || !Objects.equals(nullIfEmpty(contactProto.nickname == null ? "" : contactProto.nickname.given),
|| !Objects.equals(contactRecord.getNicknameFamilyName().orElse(null), contactNickFamilyName) contactNickGivenName)
|| !Objects.equals(contactRecord.getNote().orElse(null), contactNote)) { || !Objects.equals(nullIfEmpty(contactProto.nickname == null ? "" : contactProto.nickname.family),
contactNickFamilyName)
|| !Objects.equals(nullIfEmpty(contactProto.note), contactNote)) {
logger.debug("Storing new or updated contact {}", recipientId); logger.debug("Storing new or updated contact {}", recipientId);
final var contactBuilder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); final var contactBuilder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
final var newContact = contactBuilder.withIsBlocked(contactRecord.isBlocked()) final var newContact = contactBuilder.withIsBlocked(contactProto.blocked)
.withIsProfileSharingEnabled(contactRecord.isProfileSharingEnabled()) .withIsProfileSharingEnabled(contactProto.whitelisted)
.withIsArchived(contactRecord.isArchived()) .withIsArchived(contactProto.archived)
.withIsHidden(contactRecord.isHidden()) .withIsHidden(contactProto.hidden)
.withMuteUntil(contactRecord.getMuteUntil()) .withMuteUntil(contactProto.mutedUntilTimestamp)
.withHideStory(contactRecord.shouldHideStory()) .withHideStory(contactProto.hideStory)
.withGivenName(contactRecord.getSystemGivenName().orElse(null)) .withGivenName(nullIfEmpty(contactProto.systemGivenName))
.withFamilyName(contactRecord.getSystemFamilyName().orElse(null)) .withFamilyName(nullIfEmpty(contactProto.systemFamilyName))
.withNickName(contactRecord.getSystemNickname().orElse(null)) .withNickName(nullIfEmpty(contactProto.systemNickname))
.withNickNameGivenName(contactRecord.getNicknameGivenName().orElse(null)) .withNickNameGivenName(nullIfEmpty(contactProto.givenName))
.withNickNameFamilyName(contactRecord.getNicknameFamilyName().orElse(null)) .withNickNameFamilyName(nullIfEmpty(contactProto.familyName))
.withNote(contactRecord.getNote().orElse(null)) .withNote(nullIfEmpty(contactProto.note))
.withUnregisteredTimestamp(contactRecord.getUnregisteredTimestamp() == 0 .withUnregisteredTimestamp(contactProto.unregisteredAtTimestamp);
? null
: contactRecord.getUnregisteredTimestamp());
account.getRecipientStore().storeContact(connection, recipientId, newContact.build()); account.getRecipientStore().storeContact(connection, recipientId, newContact.build());
} }
final var profile = recipient.getProfile(); final var profile = recipient.getProfile();
final var profileGivenName = profile == null ? null : profile.getGivenName(); final var profileGivenName = profile == null ? null : profile.getGivenName();
final var profileFamilyName = profile == null ? null : profile.getFamilyName(); final var profileFamilyName = profile == null ? null : profile.getFamilyName();
if (!Objects.equals(contactRecord.getProfileGivenName().orElse(null), profileGivenName) || !Objects.equals( if (!Objects.equals(nullIfEmpty(contactProto.givenName), profileGivenName) || !Objects.equals(nullIfEmpty(
contactRecord.getProfileFamilyName().orElse(null), contactProto.familyName), profileFamilyName)) {
profileFamilyName)) {
final var profileBuilder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile); final var profileBuilder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile);
final var newProfile = profileBuilder.withGivenName(contactRecord.getProfileGivenName().orElse(null)) final var newProfile = profileBuilder.withGivenName(nullIfEmpty(contactProto.givenName))
.withFamilyName(contactRecord.getProfileFamilyName().orElse(null)) .withFamilyName(nullIfEmpty(contactProto.familyName))
.build(); .build();
account.getRecipientStore().storeProfile(connection, recipientId, newProfile); account.getRecipientStore().storeProfile(connection, recipientId, newProfile);
} }
if (contactRecord.getProfileKey().isPresent()) { if (contactProto.profileKey.size() > 0) {
try { try {
logger.trace("Storing profile key {}", recipientId); 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); account.getRecipientStore().storeProfileKey(connection, recipientId, profileKey);
} catch (InvalidInputException e) { } catch (InvalidInputException e) {
logger.warn("Received invalid contact profile key from storage"); 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 { try {
logger.trace("Storing identity key {}", recipientId); logger.trace("Storing identity key {}", recipientId);
final var identityKey = new IdentityKey(contactRecord.getIdentityKey().get()); final var identityKey = new IdentityKey(contactProto.identityKey.toByteArray());
account.getIdentityKeyStore() account.getIdentityKeyStore().saveIdentity(connection, address.aci().get(), identityKey);
.saveIdentity(connection, contactRecord.getAci().orElse(null), identityKey);
final var trustLevel = StorageSyncModels.remoteToLocal(contactRecord.getIdentityState()); final var trustLevel = StorageSyncModels.remoteToLocal(contactProto.identityState);
if (trustLevel != null) { if (trustLevel != null) {
account.getIdentityKeyStore() account.getIdentityKeyStore()
.setIdentityTrustLevel(connection, .setIdentityTrustLevel(connection, address.aci().get(), identityKey, trustLevel);
contactRecord.getAci().orElse(null),
identityKey,
trustLevel);
} }
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
logger.warn("Received invalid contact identity key from storage"); logger.warn("Received invalid contact identity key from storage");
} }
} }
account.getRecipientStore() account.getRecipientStore()
.storeStorageRecord(connection, recipientId, contactRecord.getId(), contactRecord.toProto().encode()); .storeStorageRecord(connection, recipientId, contactRecord.getId(), contactProto.encode());
} }
private static RecipientAddress getRecipientAddress(final SignalContactRecord contactRecord) { private static RecipientAddress getRecipientAddress(final ContactRecord contactRecord) {
return new RecipientAddress(contactRecord.getAci().orElse(null), return new RecipientAddress(ACI.parseOrNull(contactRecord.aci),
contactRecord.getPni().orElse(null), PNI.parseOrNull(contactRecord.pni),
contactRecord.getNumber().orElse(null), nullIfEmpty(contactRecord.e164),
contactRecord.getUsername().orElse(null)); nullIfEmpty(contactRecord.username));
} }
@Override @Override
public int compare(SignalContactRecord lhs, SignalContactRecord rhs) { public int compare(SignalContactRecord lhsRecord, SignalContactRecord rhsRecord) {
if ((lhs.getAci().isPresent() && Objects.equals(lhs.getAci(), rhs.getAci())) || ( final var lhs = lhsRecord.getProto();
lhs.getNumber().isPresent() && Objects.equals(lhs.getNumber(), rhs.getNumber()) final var rhs = rhsRecord.getProto();
) || (lhs.getPni().isPresent() && Objects.equals(lhs.getPni(), rhs.getPni()))) { 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; return 0;
} else { } else {
return 1; return 1;
@ -350,7 +337,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
return E164_PATTERN.matcher(value).matches(); return E164_PATTERN.matcher(value).matches();
} }
private static boolean doProtosMatch(SignalContactRecord merged, SignalContactRecord other) { private static boolean doProtosMatch(ContactRecord merged, ContactRecord other) {
return Arrays.equals(merged.toProto().encode(), other.toProto().encode()); return Arrays.equals(merged.encode(), other.encode());
} }
} }

View file

@ -8,6 +8,8 @@ import org.asamk.signal.manager.util.KeyUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record; 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.Connection;
import java.sql.SQLException; import java.sql.SQLException;
@ -36,7 +38,7 @@ public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor<
@Override @Override
protected boolean isInvalid(SignalGroupV1Record remote) throws SQLException { protected boolean isInvalid(SignalGroupV1Record remote) throws SQLException {
try { try {
final var id = GroupId.unknownVersion(remote.getGroupId()); final var id = GroupId.unknownVersion(remote.getProto().id.toByteArray());
if (!(id instanceof GroupIdV1)) { if (!(id instanceof GroupIdV1)) {
return true; return true;
} }
@ -56,7 +58,7 @@ public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor<
@Override @Override
protected Optional<SignalGroupV1Record> getMatching(SignalGroupV1Record remote) throws SQLException { 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); final var group = account.getGroupStore().getGroup(connection, id);
if (group == null) { if (group == null) {
@ -64,39 +66,35 @@ public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor<
} }
final var storageId = account.getGroupStore().getGroupStorageId(connection, id); 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 @Override
protected SignalGroupV1Record merge(SignalGroupV1Record remote, SignalGroupV1Record local) { protected SignalGroupV1Record merge(SignalGroupV1Record remoteRecord, SignalGroupV1Record localRecord) {
final var unknownFields = remote.serializeUnknownFields(); final var remote = remoteRecord.getProto();
final var blocked = remote.isBlocked(); final var local = localRecord.getProto();
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(), final var mergedBuilder = SignalGroupV1Record.Companion.newBuilder(remote.unknownFields().toByteArray())
remote.getGroupId(), .id(remote.id)
unknownFields).setBlocked(blocked) .blocked(remote.blocked)
.setProfileSharingEnabled(profileSharing) .whitelisted(remote.whitelisted)
.setForcedUnread(forcedUnread) .markedUnread(remote.markedUnread)
.setMuteUntil(muteUntil) .mutedUntilTimestamp(remote.mutedUntilTimestamp)
.setArchived(archived); .archived(remote.archived);
final var merged = mergedBuilder.build(); final var merged = mergedBuilder.build();
final var matchesRemote = doProtosMatch(merged, remote); final var matchesRemote = doProtosMatch(merged, remote);
if (matchesRemote) { if (matchesRemote) {
return remote; return remoteRecord;
} }
final var matchesLocal = doProtosMatch(merged, local); final var matchesLocal = doProtosMatch(merged, local);
if (matchesLocal) { if (matchesLocal) {
return local; return localRecord;
} }
return mergedBuilder.setId(KeyUtils.createRawStorageId()).build(); return new SignalGroupV1Record(StorageId.forGroupV1(KeyUtils.createRawStorageId()), mergedBuilder.build());
} }
@Override @Override
@ -110,30 +108,28 @@ public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor<
@Override @Override
protected void updateLocal(StorageRecordUpdate<SignalGroupV1Record> update) throws SQLException { protected void updateLocal(StorageRecordUpdate<SignalGroupV1Record> update) throws SQLException {
final var groupV1Record = update.newRecord(); 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); final var group = account.getGroupStore().getOrCreateGroupV1(connection, groupIdV1);
if (group != null) { if (group != null) {
group.setBlocked(groupV1Record.isBlocked()); group.setBlocked(groupV1Proto.blocked);
account.getGroupStore().updateGroup(connection, group); account.getGroupStore().updateGroup(connection, group);
account.getGroupStore() account.getGroupStore()
.storeStorageRecord(connection, .storeStorageRecord(connection, group.getGroupId(), groupV1Record.getId(), groupV1Proto.encode());
group.getGroupId(),
groupV1Record.getId(),
groupV1Record.toProto().encode());
} }
} }
@Override @Override
public int compare(SignalGroupV1Record lhs, SignalGroupV1Record rhs) { public int compare(SignalGroupV1Record lhs, SignalGroupV1Record rhs) {
if (Arrays.equals(lhs.getGroupId(), rhs.getGroupId())) { if (lhs.getProto().id.equals(rhs.getProto().id)) {
return 0; return 0;
} else { } else {
return 1; return 1;
} }
} }
private static boolean doProtosMatch(SignalGroupV1Record merged, SignalGroupV1Record other) { private static boolean doProtosMatch(GroupV1Record merged, GroupV1Record other) {
return Arrays.equals(merged.toProto().encode(), other.toProto().encode()); return Arrays.equals(merged.encode(), other.encode());
} }
} }

View file

@ -3,16 +3,22 @@ package org.asamk.signal.manager.syncStorage;
import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.groups.GroupUtils;
import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.util.KeyUtils; 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.signal.libsignal.zkgroup.groups.GroupMasterKey;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record; 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.Connection;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Optional; import java.util.Optional;
import okio.ByteString;
public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<SignalGroupV2Record> { public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<SignalGroupV2Record> {
private static final Logger logger = LoggerFactory.getLogger(GroupV2RecordProcessor.class); private static final Logger logger = LoggerFactory.getLogger(GroupV2RecordProcessor.class);
@ -26,12 +32,12 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
@Override @Override
protected boolean isInvalid(SignalGroupV2Record remote) { protected boolean isInvalid(SignalGroupV2Record remote) {
return remote.getMasterKeyBytes().length != GroupMasterKey.SIZE; return remote.getProto().masterKey.size() != GroupMasterKey.SIZE;
} }
@Override @Override
protected Optional<SignalGroupV2Record> getMatching(SignalGroupV2Record remote) throws SQLException { 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); final var group = account.getGroupStore().getGroup(connection, id);
if (group == null) { if (group == null) {
@ -39,44 +45,37 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
} }
final var storageId = account.getGroupStore().getGroupStorageId(connection, id); 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 @Override
protected SignalGroupV2Record merge(SignalGroupV2Record remote, SignalGroupV2Record local) { protected SignalGroupV2Record merge(SignalGroupV2Record remoteRecord, SignalGroupV2Record localRecord) {
final var unknownFields = remote.serializeUnknownFields(); final var remote = remoteRecord.getProto();
final var blocked = remote.isBlocked(); final var local = localRecord.getProto();
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(), final var mergedBuilder = SignalGroupV2Record.Companion.newBuilder(remote.unknownFields().toByteArray())
remote.getMasterKeyBytes(), .masterKey(remote.masterKey)
unknownFields).setBlocked(blocked) .blocked(remote.blocked)
.setProfileSharingEnabled(profileSharing) .whitelisted(remote.whitelisted)
.setArchived(archived) .archived(remote.archived)
.setForcedUnread(forcedUnread) .markedUnread(remote.markedUnread)
.setMuteUntil(muteUntil) .mutedUntilTimestamp(remote.mutedUntilTimestamp)
.setNotifyForMentionsWhenMuted(notifyForMentionsWhenMuted) .dontNotifyForMentionsIfMuted(remote.dontNotifyForMentionsIfMuted)
.setHideStory(hideStory) .hideStory(remote.hideStory)
.setStorySendMode(storySendMode); .storySendMode(remote.storySendMode);
final var merged = mergedBuilder.build(); final var merged = mergedBuilder.build();
final var matchesRemote = doProtosMatch(merged, remote); final var matchesRemote = doProtosMatch(merged, remote);
if (matchesRemote) { if (matchesRemote) {
return remote; return remoteRecord;
} }
final var matchesLocal = doProtosMatch(merged, local); final var matchesLocal = doProtosMatch(merged, local);
if (matchesLocal) { if (matchesLocal) {
return local; return localRecord;
} }
return mergedBuilder.setId(KeyUtils.createRawStorageId()).build(); return new SignalGroupV2Record(StorageId.forGroupV2(KeyUtils.createRawStorageId()), mergedBuilder.build());
} }
@Override @Override
@ -88,29 +87,36 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
@Override @Override
protected void updateLocal(StorageRecordUpdate<SignalGroupV2Record> update) throws SQLException { protected void updateLocal(StorageRecordUpdate<SignalGroupV2Record> update) throws SQLException {
final var groupV2Record = update.newRecord(); 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); final var group = account.getGroupStore().getGroupOrPartialMigrate(connection, groupMasterKey);
group.setBlocked(groupV2Record.isBlocked()); group.setBlocked(groupV2Proto.blocked);
group.setProfileSharingEnabled(groupV2Record.isProfileSharingEnabled()); group.setProfileSharingEnabled(groupV2Proto.whitelisted);
account.getGroupStore().updateGroup(connection, group); account.getGroupStore().updateGroup(connection, group);
account.getGroupStore() account.getGroupStore()
.storeStorageRecord(connection, .storeStorageRecord(connection, group.getGroupId(), groupV2Record.getId(), groupV2Proto.encode());
group.getGroupId(), }
groupV2Record.getId(),
groupV2Record.toProto().encode()); @NotNull
private static GroupMasterKey getGroupMasterKeyOrThrow(final ByteString masterKey) {
try {
return new GroupMasterKey(masterKey.toByteArray());
} catch (InvalidInputException e) {
throw new AssertionError(e);
}
} }
@Override @Override
public int compare(SignalGroupV2Record lhs, SignalGroupV2Record rhs) { public int compare(SignalGroupV2Record lhs, SignalGroupV2Record rhs) {
if (Arrays.equals(lhs.getMasterKeyBytes(), rhs.getMasterKeyBytes())) { if (lhs.getProto().masterKey.equals(rhs.getProto().masterKey)) {
return 0; return 0;
} else { } else {
return 1; return 1;
} }
} }
private static boolean doProtosMatch(SignalGroupV2Record merged, SignalGroupV2Record other) { private static boolean doProtosMatch(GroupV2Record merged, GroupV2Record other) {
return Arrays.equals(merged.toProto().encode(), other.toProto().encode()); return Arrays.equals(merged.encode(), other.encode());
} }
} }

View 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.groups.GroupInfoV2;
import org.asamk.signal.manager.storage.identities.IdentityInfo; import org.asamk.signal.manager.storage.identities.IdentityInfo;
import org.asamk.signal.manager.storage.recipients.Recipient; 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.push.UsernameLinkComponents;
import org.whispersystems.signalservice.api.storage.SignalAccountRecord; import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
import org.whispersystems.signalservice.api.storage.SignalContactRecord; import org.whispersystems.signalservice.api.storage.SignalContactRecord;
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record; import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record; 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.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord; import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord.UsernameLink; 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.ContactRecord.IdentityState;
import org.whispersystems.signalservice.internal.storage.protos.GroupV1Record;
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record;
import java.util.Optional; import java.util.Optional;
import okio.ByteString; import okio.ByteString;
import static org.signal.core.util.StringExtensionsKt.emptyIfNull;
public final class StorageSyncModels { public final class StorageSyncModels {
private StorageSyncModels() { private StorageSyncModels() {
@ -42,104 +48,97 @@ public final class StorageSyncModels {
}; };
} }
public static SignalStorageRecord localToRemoteRecord( public static AccountRecord localToRemoteRecord(
ConfigurationStore configStore, ConfigurationStore configStore,
Recipient self, Recipient self,
UsernameLinkComponents usernameLinkComponents, UsernameLinkComponents usernameLinkComponents
byte[] rawStorageId
) { ) {
final var builder = new SignalAccountRecord.Builder(rawStorageId, self.getStorageRecord()); final var builder = SignalAccountRecord.Companion.newBuilder(self.getStorageRecord());
if (self.getProfileKey() != null) { if (self.getProfileKey() != null) {
builder.setProfileKey(self.getProfileKey().serialize()); builder.profileKey(ByteString.of(self.getProfileKey().serialize()));
} }
if (self.getProfile() != null) { if (self.getProfile() != null) {
builder.setGivenName(self.getProfile().getGivenName()) builder.givenName(emptyIfNull(self.getProfile().getGivenName()))
.setFamilyName(self.getProfile().getFamilyName()) .familyName(emptyIfNull(self.getProfile().getFamilyName()))
.setAvatarUrlPath(self.getProfile().getAvatarUrlPath()); .avatarUrlPath(emptyIfNull(self.getProfile().getAvatarUrlPath()));
} }
builder.setTypingIndicatorsEnabled(Optional.ofNullable(configStore.getTypingIndicators()).orElse(true)) builder.typingIndicators(Optional.ofNullable(configStore.getTypingIndicators()).orElse(true))
.setReadReceiptsEnabled(Optional.ofNullable(configStore.getReadReceipts()).orElse(true)) .readReceipts(Optional.ofNullable(configStore.getReadReceipts()).orElse(true))
.setSealedSenderIndicatorsEnabled(Optional.ofNullable(configStore.getUnidentifiedDeliveryIndicators()) .sealedSenderIndicators(Optional.ofNullable(configStore.getUnidentifiedDeliveryIndicators())
.orElse(true)) .orElse(true))
.setLinkPreviewsEnabled(Optional.ofNullable(configStore.getLinkPreviews()).orElse(true)) .linkPreviews(Optional.ofNullable(configStore.getLinkPreviews()).orElse(true))
.setUnlistedPhoneNumber(Optional.ofNullable(configStore.getPhoneNumberUnlisted()).orElse(false)) .unlistedPhoneNumber(Optional.ofNullable(configStore.getPhoneNumberUnlisted()).orElse(false))
.setPhoneNumberSharingMode(Optional.ofNullable(configStore.getPhoneNumberSharingMode()) .phoneNumberSharingMode(Optional.ofNullable(configStore.getPhoneNumberSharingMode())
.map(StorageSyncModels::localToRemote) .map(StorageSyncModels::localToRemote)
.orElse(AccountRecord.PhoneNumberSharingMode.UNKNOWN)) .orElse(AccountRecord.PhoneNumberSharingMode.UNKNOWN))
.setE164(self.getAddress().number().orElse("")) .e164(self.getAddress().number().orElse(""))
.setUsername(self.getAddress().username().orElse(null)); .username(self.getAddress().username().orElse(""));
if (usernameLinkComponents != null) { if (usernameLinkComponents != null) {
final var linkColor = configStore.getUsernameLinkColor(); 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())) .serverId(UuidUtil.toByteString(usernameLinkComponents.getServerId()))
.color(linkColor == null ? UsernameLink.Color.UNKNOWN : UsernameLink.Color.valueOf(linkColor)) .color(linkColor == null ? UsernameLink.Color.UNKNOWN : UsernameLink.Color.valueOf(linkColor))
.build()); .build());
} }
return SignalStorageRecord.forAccount(builder.build()); return builder.build();
} }
public static SignalStorageRecord localToRemoteRecord( public static ContactRecord localToRemoteRecord(Recipient recipient, IdentityInfo identity) {
Recipient recipient,
IdentityInfo identity,
byte[] rawStorageId
) {
final var address = recipient.getAddress(); final var address = recipient.getAddress();
final var builder = new SignalContactRecord.Builder(rawStorageId, final var builder = SignalContactRecord.Companion.newBuilder(recipient.getStorageRecord())
address.aci().orElse(null), .aci(address.aci().map(ACI::toString).orElse(""))
recipient.getStorageRecord()).setE164(address.number().orElse(null)) .e164(address.number().orElse(""))
.setPni(address.pni().orElse(null)) .pni(address.pni().map(PNI::toString).orElse(""))
.setUsername(address.username().orElse(null)) .username(address.username().orElse(""))
.setProfileKey(recipient.getProfileKey() == null ? null : recipient.getProfileKey().serialize()); .profileKey(recipient.getProfileKey() == null
? ByteString.EMPTY
: ByteString.of(recipient.getProfileKey().serialize()));
if (recipient.getProfile() != null) { if (recipient.getProfile() != null) {
builder.setProfileGivenName(recipient.getProfile().getGivenName()) builder.givenName(emptyIfNull(recipient.getProfile().getGivenName()))
.setProfileFamilyName(recipient.getProfile().getFamilyName()); .familyName(emptyIfNull(recipient.getProfile().getFamilyName()));
} }
if (recipient.getContact() != null) { if (recipient.getContact() != null) {
builder.setSystemGivenName(recipient.getContact().givenName()) builder.systemGivenName(emptyIfNull(recipient.getContact().givenName()))
.setSystemFamilyName(recipient.getContact().familyName()) .systemFamilyName(emptyIfNull(recipient.getContact().familyName()))
.setSystemNickname(recipient.getContact().nickName()) .systemNickname(emptyIfNull(recipient.getContact().nickName()))
.setNicknameGivenName(recipient.getContact().nickNameGivenName() == null .nickname(new ContactRecord.Name.Builder().given(emptyIfNull(recipient.getContact()
? "" .nickNameGivenName()))
: recipient.getContact().nickNameGivenName()) .family(emptyIfNull(recipient.getContact().nickNameFamilyName()))
.setNicknameFamilyName(recipient.getContact().nickNameFamilyName() == null .build())
? "" .note(emptyIfNull(recipient.getContact().note()))
: recipient.getContact().nickNameFamilyName()) .blocked(recipient.getContact().isBlocked())
.setNote(recipient.getContact().note()) .whitelisted(recipient.getContact().isProfileSharingEnabled())
.setBlocked(recipient.getContact().isBlocked()) .mutedUntilTimestamp(recipient.getContact().muteUntil())
.setProfileSharingEnabled(recipient.getContact().isProfileSharingEnabled()) .hideStory(recipient.getContact().hideStory())
.setMuteUntil(recipient.getContact().muteUntil()) .unregisteredAtTimestamp(recipient.getContact().unregisteredTimestamp() == null
.setHideStory(recipient.getContact().hideStory())
.setUnregisteredTimestamp(recipient.getContact().unregisteredTimestamp() == null
? 0 ? 0
: recipient.getContact().unregisteredTimestamp()) : recipient.getContact().unregisteredTimestamp())
.setArchived(recipient.getContact().isArchived()) .archived(recipient.getContact().isArchived())
.setHidden(recipient.getContact().isHidden()); .hidden(recipient.getContact().isHidden());
} }
if (identity != null) { if (identity != null) {
builder.setIdentityKey(identity.getIdentityKey().serialize()) builder.identityKey(ByteString.of(identity.getIdentityKey().serialize()))
.setIdentityState(localToRemote(identity.getTrustLevel())); .identityState(localToRemote(identity.getTrustLevel()));
} }
return SignalStorageRecord.forContact(builder.build()); return builder.build();
} }
public static SignalStorageRecord localToRemoteRecord(GroupInfoV1 group, byte[] rawStorageId) { public static GroupV1Record localToRemoteRecord(GroupInfoV1 group) {
final var builder = new SignalGroupV1Record.Builder(rawStorageId, final var builder = SignalGroupV1Record.Companion.newBuilder(group.getStorageRecord());
group.getGroupId().serialize(), builder.id(ByteString.of(group.getGroupId().serialize()));
group.getStorageRecord()); builder.blocked(group.isBlocked());
builder.setBlocked(group.isBlocked()); builder.archived(group.archived);
builder.setArchived(group.archived); builder.whitelisted(true);
builder.setProfileSharingEnabled(true); return builder.build();
return SignalStorageRecord.forGroupV1(builder.build());
} }
public static SignalStorageRecord localToRemoteRecord(GroupInfoV2 group, byte[] rawStorageId) { public static GroupV2Record localToRemoteRecord(GroupInfoV2 group) {
final var builder = new SignalGroupV2Record.Builder(rawStorageId, final var builder = SignalGroupV2Record.Companion.newBuilder(group.getStorageRecord());
group.getMasterKey(), builder.masterKey(ByteString.of(group.getMasterKey().serialize()));
group.getStorageRecord()); builder.blocked(group.isBlocked());
builder.setBlocked(group.isBlocked()); builder.whitelisted(group.isProfileSharingEnabled());
builder.setProfileSharingEnabled(group.isProfileSharingEnabled()); return builder.build();
return SignalStorageRecord.forGroupV2(builder.build());
} }
public static TrustLevel remoteToLocal(IdentityState identityState) { public static TrustLevel remoteToLocal(IdentityState identityState) {

View file

@ -5,6 +5,8 @@ import org.signal.core.util.Base64;
import org.signal.core.util.SetUtil; import org.signal.core.util.SetUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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.SignalStorageManifest;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord; import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import org.whispersystems.signalservice.api.storage.StorageId; import org.whispersystems.signalservice.api.storage.StorageId;
@ -32,9 +34,7 @@ public final class StorageSyncValidations {
validateManifestAndInserts(result.manifest(), result.inserts(), self); validateManifestAndInserts(result.manifest(), result.inserts(), self);
if (!result.deletes().isEmpty()) { if (!result.deletes().isEmpty()) {
Set<String> allSetEncoded = result.manifest() Set<String> allSetEncoded = result.manifest().storageIds.stream()
.getStorageIds()
.stream()
.map(StorageId::getRaw) .map(StorageId::getRaw)
.map(Base64::encodeWithPadding) .map(Base64::encodeWithPadding)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
@ -47,13 +47,13 @@ public final class StorageSyncValidations {
} }
} }
if (previousManifest.getVersion() == 0) { if (previousManifest.version == 0) {
logger.debug( logger.debug(
"Previous manifest is empty, not bothering with additional validations around the diffs between the two manifests."); "Previous manifest is empty, not bothering with additional validations around the diffs between the two manifests.");
return; return;
} }
if (result.manifest().getVersion() != previousManifest.getVersion() + 1) { if (result.manifest().version != previousManifest.version + 1) {
throw new IncorrectManifestVersionError(); throw new IncorrectManifestVersionError();
} }
@ -63,13 +63,10 @@ public final class StorageSyncValidations {
return; return;
} }
Set<ByteBuffer> previousIds = previousManifest.getStorageIds() Set<ByteBuffer> previousIds = previousManifest.storageIds.stream()
.stream()
.map(id -> ByteBuffer.wrap(id.getRaw())) .map(id -> ByteBuffer.wrap(id.getRaw()))
.collect(Collectors.toSet()); .collect(Collectors.toSet());
Set<ByteBuffer> newIds = result.manifest() Set<ByteBuffer> newIds = result.manifest().storageIds.stream()
.getStorageIds()
.stream()
.map(id -> ByteBuffer.wrap(id.getRaw())) .map(id -> ByteBuffer.wrap(id.getRaw()))
.collect(Collectors.toSet()); .collect(Collectors.toSet());
@ -83,12 +80,12 @@ public final class StorageSyncValidations {
Set<ByteBuffer> declaredDeletes = result.deletes().stream().map(ByteBuffer::wrap).collect(Collectors.toSet()); Set<ByteBuffer> declaredDeletes = result.deletes().stream().map(ByteBuffer::wrap).collect(Collectors.toSet());
if (declaredInserts.size() > manifestInserts.size()) { if (declaredInserts.size() > manifestInserts.size()) {
logger.debug("DeclaredInserts: " + declaredInserts.size() + ", ManifestInserts: " + manifestInserts.size()); logger.debug("DeclaredInserts: {}, ManifestInserts: {}", declaredInserts.size(), manifestInserts.size());
throw new MoreInsertsThanExpectedError(); throw new MoreInsertsThanExpectedError();
} }
if (declaredInserts.size() < manifestInserts.size()) { if (declaredInserts.size() < manifestInserts.size()) {
logger.debug("DeclaredInserts: " + declaredInserts.size() + ", ManifestInserts: " + manifestInserts.size()); logger.debug("DeclaredInserts: {}, ManifestInserts: {}", declaredInserts.size(), manifestInserts.size());
throw new LessInsertsThanExpectedError(); throw new LessInsertsThanExpectedError();
} }
@ -97,12 +94,12 @@ public final class StorageSyncValidations {
} }
if (declaredDeletes.size() > manifestDeletes.size()) { if (declaredDeletes.size() > manifestDeletes.size()) {
logger.debug("DeclaredDeletes: " + declaredDeletes.size() + ", ManifestDeletes: " + manifestDeletes.size()); logger.debug("DeclaredDeletes: {}, ManifestDeletes: {}", declaredDeletes.size(), manifestDeletes.size());
throw new MoreDeletesThanExpectedError(); throw new MoreDeletesThanExpectedError();
} }
if (declaredDeletes.size() < manifestDeletes.size()) { if (declaredDeletes.size() < manifestDeletes.size()) {
logger.debug("DeclaredDeletes: " + declaredDeletes.size() + ", ManifestDeletes: " + manifestDeletes.size()); logger.debug("DeclaredDeletes: {}, ManifestDeletes: {}", declaredDeletes.size(), manifestDeletes.size());
throw new LessDeletesThanExpectedError(); throw new LessDeletesThanExpectedError();
} }
@ -125,7 +122,7 @@ public final class StorageSyncValidations {
RecipientAddress self RecipientAddress self
) { ) {
int accountCount = 0; int accountCount = 0;
for (StorageId id : manifest.getStorageIds()) { for (StorageId id : manifest.storageIds) {
accountCount += id.getType() == ManifestRecord.Identifier.Type.ACCOUNT.getValue() ? 1 : 0; accountCount += id.getType() == ManifestRecord.Identifier.Type.ACCOUNT.getValue() ? 1 : 0;
} }
@ -137,11 +134,11 @@ public final class StorageSyncValidations {
throw new MissingAccountError(); 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<StorageId> insertSet = inserts.stream().map(SignalStorageRecord::getId).collect(Collectors.toSet());
Set<ByteBuffer> rawIdSet = allSet.stream().map(id -> ByteBuffer.wrap(id.getRaw())).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(); throw new DuplicateStorageIdError();
} }
@ -166,6 +163,11 @@ public final class StorageSyncValidations {
throw new DuplicateDistributionListIdError(); 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(); throw new DuplicateRawIdAcrossTypesError();
} }
@ -182,18 +184,18 @@ public final class StorageSyncValidations {
throw new UnknownInsertError(); throw new UnknownInsertError();
} }
if (insert.getContact().isPresent()) { if (insert.getProto().contact != null) {
final var contact = insert.getContact().get(); final var contact = insert.getProto().contact;
final var aci = contact.getAci(); final var aci = ACI.parseOrNull(contact.aci);
final var pni = contact.getPni(); final var pni = PNI.parseOrNull(contact.pni);
final var number = contact.getNumber(); final var number = contact.e164.isEmpty() ? null : contact.e164;
final var username = contact.getUsername(); final var username = contact.username.isEmpty() ? null : contact.username;
final var address = new RecipientAddress(aci, pni, number, username); final var address = new RecipientAddress(aci, pni, number, username);
if (self.matches(address)) { if (self.matches(address)) {
throw new SelfAddedAsContactError(); 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!"); 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 DuplicateDistributionListIdError extends Error {}
private static final class DuplicateCallLinkError extends Error {}
private static final class DuplicateInsertInWriteError extends Error {} private static final class DuplicateInsertInWriteError extends Error {}
private static final class InsertNotPresentInFullIdSetError extends Error {} private static final class InsertNotPresentInFullIdSetError extends Error {}

View file

@ -21,8 +21,8 @@ public record WriteOperationResult(
} else { } else {
return String.format(Locale.ROOT, return String.format(Locale.ROOT,
"ManifestVersion: %d, Total Keys: %d, Inserts: %d, Deletes: %d", "ManifestVersion: %d, Total Keys: %d, Inserts: %d, Deletes: %d",
manifest.getVersion(), manifest.version,
manifest.getStorageIds().size(), manifest.storageIds.size(),
inserts.size(), inserts.size(),
deletes.size()); deletes.size());
} }

View file

@ -14,6 +14,7 @@ import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.api.account.PreKeyCollection; import org.whispersystems.signalservice.api.account.PreKeyCollection;
import org.whispersystems.signalservice.api.backup.MediaRootBackupKey;
import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.api.kbs.MasterKey;
import java.security.SecureRandom; import java.security.SecureRandom;
@ -112,6 +113,10 @@ public class KeyUtils {
return MasterKey.createNew(secureRandom); return MasterKey.createNew(secureRandom);
} }
public static MediaRootBackupKey createMediaRootBackupKey() {
return new MediaRootBackupKey(getSecretBytes(32));
}
public static byte[] createRawStorageId() { public static byte[] createRawStorageId() {
return getSecretBytes(16); return getSecretBytes(16);
} }

View file

@ -184,7 +184,7 @@ public class NumberVerificationUtils {
TokenNotAcceptedException _e) { TokenNotAcceptedException _e) {
throw new CaptchaRequiredException("Captcha not accepted"); throw new CaptchaRequiredException("Captcha not accepted");
} catch (NonSuccessfulResponseCodeException e) { } catch (NonSuccessfulResponseCodeException e) {
if (e.getCode() == 400) { if (e.code == 400) {
throw new CaptchaRequiredException("Captcha has invalid format"); throw new CaptchaRequiredException("Captcha has invalid format");
} }
throw e; throw e;

View file

@ -84,6 +84,9 @@ public class ProfileUtils {
if (encryptedProfile.getCapabilities().isStorage()) { if (encryptedProfile.getCapabilities().isStorage()) {
capabilities.add(Profile.Capability.storage); capabilities.add(Profile.Capability.storage);
} }
if (encryptedProfile.getCapabilities().isStorageServiceEncryptionV2()) {
capabilities.add(Profile.Capability.storageServiceEncryptionV2Capability);
}
return capabilities; return capabilities;
} }

View file

@ -18,6 +18,7 @@ import java.io.InputStream;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@ -30,6 +31,8 @@ import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import java.util.stream.StreamSupport; import java.util.stream.StreamSupport;
import okio.ByteString;
public class Utils { public class Utils {
private static final Logger logger = LoggerFactory.getLogger(Utils.class); private static final Logger logger = LoggerFactory.getLogger(Utils.class);
@ -157,4 +160,46 @@ public class Utils {
} }
return response.successOrThrow(); 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;
}
} }