mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 10:30:38 +00:00
Update libsignal-service
Support for storage encryption v2 and account entropy pool Fixes #1632
This commit is contained in:
parent
f2005593ec
commit
ff6cb5262a
29 changed files with 952 additions and 639 deletions
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.");
|
||||||
|
|
|
@ -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()));
|
||||||
|
|
|
@ -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 -> {
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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()),
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 {}
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue