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",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"com.squareup.wire.Message",
|
||||
"methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"com.squareup.wire.ProtoAdapter"
|
||||
},
|
||||
{
|
||||
"name":"com.squareup.wire.internal.ImmutableList",
|
||||
"allDeclaredFields":true,
|
||||
|
@ -209,9 +216,14 @@
|
|||
{
|
||||
"name":"java.io.FilePermission"
|
||||
},
|
||||
{
|
||||
"name":"java.io.OutputStream"
|
||||
},
|
||||
{
|
||||
"name":"java.io.Serializable",
|
||||
"allDeclaredMethods":true
|
||||
"allDeclaredFields":true,
|
||||
"allDeclaredMethods":true,
|
||||
"allDeclaredClasses":true
|
||||
},
|
||||
{
|
||||
"name":"java.lang.Boolean",
|
||||
|
@ -577,6 +589,9 @@
|
|||
{
|
||||
"name":"kotlin.String"
|
||||
},
|
||||
{
|
||||
"name":"kotlin.Unit"
|
||||
},
|
||||
{
|
||||
"name":"kotlin.collections.AbstractCollection",
|
||||
"allDeclaredFields":true,
|
||||
|
@ -629,6 +644,9 @@
|
|||
{
|
||||
"name":"long[]"
|
||||
},
|
||||
{
|
||||
"name":"okio.BufferedSink"
|
||||
},
|
||||
{
|
||||
"name":"okio.ByteString"
|
||||
},
|
||||
|
@ -1025,7 +1043,7 @@
|
|||
"allDeclaredFields":true,
|
||||
"allDeclaredMethods":true,
|
||||
"allDeclaredConstructors":true,
|
||||
"methods":[{"name":"groupId","parameterTypes":[] }, {"name":"type","parameterTypes":[] }]
|
||||
"methods":[{"name":"groupId","parameterTypes":[] }, {"name":"groupName","parameterTypes":[] }, {"name":"revision","parameterTypes":[] }, {"name":"type","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.asamk.signal.json.JsonMention",
|
||||
|
@ -1247,7 +1265,7 @@
|
|||
"allDeclaredFields":true,
|
||||
"queryAllDeclaredMethods":true,
|
||||
"queryAllDeclaredConstructors":true,
|
||||
"methods":[{"name":"<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",
|
||||
|
@ -2257,7 +2275,7 @@
|
|||
"allDeclaredFields":true,
|
||||
"allDeclaredMethods":true,
|
||||
"allDeclaredConstructors":true,
|
||||
"methods":[{"name":"getAnnouncementGroup","parameterTypes":[] }, {"name":"getChangeNumber","parameterTypes":[] }, {"name":"getDeleteSync","parameterTypes":[] }, {"name":"getGiftBadges","parameterTypes":[] }, {"name":"getPaymentActivation","parameterTypes":[] }, {"name":"getPni","parameterTypes":[] }, {"name":"getSenderKey","parameterTypes":[] }, {"name":"getStorage","parameterTypes":[] }, {"name":"getStories","parameterTypes":[] }, {"name":"getVersionedExpirationTimer","parameterTypes":[] }]
|
||||
"methods":[{"name":"getAnnouncementGroup","parameterTypes":[] }, {"name":"getChangeNumber","parameterTypes":[] }, {"name":"getDeleteSync","parameterTypes":[] }, {"name":"getGiftBadges","parameterTypes":[] }, {"name":"getPaymentActivation","parameterTypes":[] }, {"name":"getPni","parameterTypes":[] }, {"name":"getSenderKey","parameterTypes":[] }, {"name":"getStorage","parameterTypes":[] }, {"name":"getStorageServiceEncryptionV2","parameterTypes":[] }, {"name":"getStories","parameterTypes":[] }, {"name":"getVersionedExpirationTimer","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest",
|
||||
|
@ -2284,6 +2302,13 @@
|
|||
{
|
||||
"name":"org.whispersystems.signalservice.api.groupsv2.TemporalCredential[]"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse",
|
||||
"allDeclaredFields":true,
|
||||
"queryAllDeclaredMethods":true,
|
||||
"queryAllDeclaredConstructors":true,
|
||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"<init>","parameterTypes":["java.lang.String","java.lang.String","int","kotlin.jvm.internal.DefaultConstructorMarker"] }]
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.api.messages.calls.HangupMessage",
|
||||
"allDeclaredFields":true,
|
||||
|
@ -2891,7 +2916,17 @@
|
|||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord",
|
||||
"allDeclaredFields":true
|
||||
"allDeclaredFields":true,
|
||||
"methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$Builder"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$Companion"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$PhoneNumberSharingMode"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$PinnedConversation",
|
||||
|
@ -2907,7 +2942,17 @@
|
|||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord",
|
||||
"allDeclaredFields":true
|
||||
"allDeclaredFields":true,
|
||||
"methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord$Builder"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord$Companion"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord$IdentityState"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord$Name",
|
||||
|
@ -2915,11 +2960,28 @@
|
|||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV1Record",
|
||||
"allDeclaredFields":true
|
||||
"allDeclaredFields":true,
|
||||
"methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV1Record$Builder"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV1Record$Companion"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record",
|
||||
"allDeclaredFields":true
|
||||
"allDeclaredFields":true,
|
||||
"methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record$Builder"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record$Companion"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record$StorySendMode"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.ManifestRecord",
|
||||
|
@ -2929,6 +2991,9 @@
|
|||
"name":"org.whispersystems.signalservice.internal.storage.protos.ManifestRecord$Identifier",
|
||||
"fields":[{"name":"raw_"}, {"name":"type_"}]
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.OptionalBool"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.Payments",
|
||||
"allDeclaredFields":true
|
||||
|
|
|
@ -10,7 +10,7 @@ slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
|
|||
slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" }
|
||||
logback = "ch.qos.logback:logback-classic:1.5.12"
|
||||
|
||||
signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_110"
|
||||
signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_111"
|
||||
sqlite = "org.xerial:sqlite-jdbc:3.47.0.0"
|
||||
hikari = "com.zaxxer:HikariCP:6.2.1"
|
||||
junit-jupiter = "org.junit.jupiter:junit-jupiter:5.11.3"
|
||||
|
|
|
@ -611,11 +611,12 @@ public record MessageEnvelope(
|
|||
RecipientResolver recipientResolver,
|
||||
RecipientAddressResolver addressResolver
|
||||
) {
|
||||
return new Blocked(blockedListMessage.getAddresses()
|
||||
.stream()
|
||||
.map(d -> addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(d))
|
||||
.toApiRecipientAddress())
|
||||
.toList(), blockedListMessage.getGroupIds().stream().map(GroupId::unknownVersion).toList());
|
||||
return new Blocked(blockedListMessage.individuals.stream()
|
||||
.map(d -> new RecipientAddress(d.getAci() == null ? null : d.getAci().toString(),
|
||||
null,
|
||||
d.getE164(),
|
||||
null))
|
||||
.toList(), blockedListMessage.groupIds.stream().map(GroupId::unknownVersion).toList());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -161,7 +161,8 @@ public class Profile {
|
|||
}
|
||||
|
||||
public enum Capability {
|
||||
storage;
|
||||
storage,
|
||||
storageServiceEncryptionV2Capability;
|
||||
|
||||
public static Capability valueOfOrNull(String value) {
|
||||
try {
|
||||
|
|
|
@ -51,7 +51,7 @@ class LiveConfig {
|
|||
private static final byte[] backupServerPublicParams = Base64.getDecoder()
|
||||
.decode("AJwNSU55fsFCbgaxGRD11wO1juAs8Yr5GF8FPlGzzvdJJIKH5/4CC7ZJSOe3yL2vturVaRU2Cx0n751Vt8wkj1bozK3CBV1UokxV09GWf+hdVImLGjXGYLLhnI1J2TWEe7iWHyb553EEnRb5oxr9n3lUbNAJuRmFM7hrr0Al0F0wrDD4S8lo2mGaXe0MJCOM166F8oYRQqpFeEHfiLnxA1O8ZLh7vMdv4g9jI5phpRBTsJ5IjiJrWeP0zdIGHEssUeprDZ9OUJ14m0v61eYJMKsf59Bn+mAT2a7YfB+Don9O");
|
||||
|
||||
private static Environment LIBSIGNAL_NET_ENV = Environment.PRODUCTION;
|
||||
private static final Environment LIBSIGNAL_NET_ENV = Environment.PRODUCTION;
|
||||
|
||||
static SignalServiceConfiguration createDefaultServiceConfiguration(
|
||||
final List<Interceptor> interceptors
|
||||
|
@ -71,7 +71,8 @@ class LiveConfig {
|
|||
proxy,
|
||||
zkGroupServerPublicParams,
|
||||
genericServerPublicParams,
|
||||
backupServerPublicParams);
|
||||
backupServerPublicParams,
|
||||
false);
|
||||
}
|
||||
|
||||
static ECPublicKey getUnidentifiedSenderTrustRoot() {
|
||||
|
|
|
@ -29,7 +29,8 @@ public class ServiceConfig {
|
|||
|
||||
public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) {
|
||||
final var deleteSync = !isPrimaryDevice;
|
||||
return new AccountAttributes.Capabilities(true, deleteSync, true);
|
||||
final var storageEncryptionV2 = !isPrimaryDevice;
|
||||
return new AccountAttributes.Capabilities(true, deleteSync, true, storageEncryptionV2);
|
||||
}
|
||||
|
||||
public static ServiceEnvironmentConfig getServiceEnvironmentConfig(
|
||||
|
|
|
@ -51,7 +51,7 @@ class StagingConfig {
|
|||
private static final byte[] backupServerPublicParams = Base64.getDecoder()
|
||||
.decode("AHYrGb9IfugAAJiPKp+mdXUx+OL9zBolPYHYQz6GI1gWjpEu5me3zVNSvmYY4zWboZHif+HG1sDHSuvwFd0QszSwuSF4X4kRP3fJREdTZ5MCR0n55zUppTwfHRW2S4sdQ0JGz7YDQIJCufYSKh0pGNEHL6hv79Agrdnr4momr3oXdnkpVBIp3HWAQ6IbXQVSG18X36GaicI1vdT0UFmTwU2KTneluC2eyL9c5ff8PcmiS+YcLzh0OKYQXB5ZfQ06d6DiINvDQLy75zcfUOniLAj0lGJiHxGczin/RXisKSR8");
|
||||
|
||||
private static Network.Environment LIBSIGNAL_NET_ENV = Network.Environment.STAGING;
|
||||
private static final Network.Environment LIBSIGNAL_NET_ENV = Network.Environment.STAGING;
|
||||
|
||||
static SignalServiceConfiguration createDefaultServiceConfiguration(
|
||||
final List<Interceptor> interceptors
|
||||
|
@ -71,7 +71,8 @@ class StagingConfig {
|
|||
proxy,
|
||||
zkGroupServerPublicParams,
|
||||
genericServerPublicParams,
|
||||
backupServerPublicParams);
|
||||
backupServerPublicParams,
|
||||
false);
|
||||
}
|
||||
|
||||
static ECPublicKey getUnidentifiedSenderTrustRoot() {
|
||||
|
|
|
@ -3,7 +3,6 @@ package org.asamk.signal.manager.helper;
|
|||
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
||||
import org.asamk.signal.manager.api.DeviceLinkUrl;
|
||||
import org.asamk.signal.manager.api.IncorrectPinException;
|
||||
import org.asamk.signal.manager.api.InvalidDeviceLinkException;
|
||||
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
|
||||
import org.asamk.signal.manager.api.PinLockedException;
|
||||
import org.asamk.signal.manager.api.RateLimitException;
|
||||
|
@ -27,6 +26,7 @@ import org.slf4j.Logger;
|
|||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||
|
@ -56,6 +56,7 @@ import java.util.concurrent.TimeUnit;
|
|||
import okio.ByteString;
|
||||
|
||||
import static org.asamk.signal.manager.config.ServiceConfig.PREKEY_MAXIMUM_ID;
|
||||
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
||||
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
|
||||
|
||||
public class AccountHelper {
|
||||
|
@ -289,12 +290,11 @@ public class AccountHelper {
|
|||
(sessionId1, verificationCode1, registrationLock) -> {
|
||||
final var registrationApi = dependencies.getRegistrationApi();
|
||||
try {
|
||||
Utils.handleResponseException(registrationApi.verifyAccount(sessionId1, verificationCode1));
|
||||
handleResponseException(registrationApi.verifyAccount(sessionId1, verificationCode1));
|
||||
} catch (AlreadyVerifiedException e) {
|
||||
// Already verified so can continue changing number
|
||||
}
|
||||
return Utils.handleResponseException(registrationApi.changeNumber(new ChangePhoneNumberRequest(
|
||||
sessionId1,
|
||||
return handleResponseException(registrationApi.changeNumber(new ChangePhoneNumberRequest(sessionId1,
|
||||
null,
|
||||
newNumber,
|
||||
registrationLock,
|
||||
|
@ -482,26 +482,28 @@ public class AccountHelper {
|
|||
dependencies.getAccountManager().setAccountAttributes(account.getAccountAttributes(null));
|
||||
}
|
||||
|
||||
public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, InvalidDeviceLinkException, org.asamk.signal.manager.api.DeviceLimitExceededException {
|
||||
String verificationCode;
|
||||
public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, org.asamk.signal.manager.api.DeviceLimitExceededException {
|
||||
final var linkDeviceApi = dependencies.getLinkDeviceApi();
|
||||
final LinkedDeviceVerificationCodeResponse verificationCode;
|
||||
try {
|
||||
verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode();
|
||||
verificationCode = handleResponseException(linkDeviceApi.getDeviceVerificationCode());
|
||||
} catch (DeviceLimitExceededException e) {
|
||||
throw new org.asamk.signal.manager.api.DeviceLimitExceededException("Too many linked devices", e);
|
||||
}
|
||||
|
||||
try {
|
||||
dependencies.getAccountManager()
|
||||
.addDevice(deviceLinkInfo.deviceIdentifier(),
|
||||
deviceLinkInfo.deviceKey(),
|
||||
account.getAciIdentityKeyPair(),
|
||||
account.getPniIdentityKeyPair(),
|
||||
account.getProfileKey(),
|
||||
account.getOrCreatePinMasterKey(),
|
||||
verificationCode);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidDeviceLinkException("Invalid device link", e);
|
||||
}
|
||||
handleResponseException(dependencies.getLinkDeviceApi()
|
||||
.linkDevice(account.getNumber(),
|
||||
account.getAci(),
|
||||
account.getPni(),
|
||||
deviceLinkInfo.deviceIdentifier(),
|
||||
deviceLinkInfo.deviceKey(),
|
||||
account.getAciIdentityKeyPair(),
|
||||
account.getPniIdentityKeyPair(),
|
||||
account.getProfileKey(),
|
||||
account.getOrCreatePinMasterKey(),
|
||||
account.getOrCreateMediaRootBackupKey(),
|
||||
verificationCode.getVerificationCode(),
|
||||
null));
|
||||
account.setMultiDevice(true);
|
||||
context.getJobExecutor().enqueueJob(new SyncStorageJob());
|
||||
}
|
||||
|
|
|
@ -82,7 +82,7 @@ class GroupV2Helper {
|
|||
final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams);
|
||||
return dependencies.getGroupsV2Api().getGroup(groupSecretParams, groupsV2AuthorizationString);
|
||||
} catch (NonSuccessfulResponseCodeException e) {
|
||||
if (e.getCode() == 403) {
|
||||
if (e.code == 403) {
|
||||
throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
|
||||
}
|
||||
logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage());
|
||||
|
@ -119,7 +119,7 @@ class GroupV2Helper {
|
|||
false,
|
||||
sendEndorsementsExpirationMs);
|
||||
} catch (NonSuccessfulResponseCodeException e) {
|
||||
if (e.getCode() == 403) {
|
||||
if (e.code == 403) {
|
||||
throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
|
||||
}
|
||||
logger.warn("Failed to retrieve Group V2 history, ignoring: {}", e.getMessage());
|
||||
|
|
|
@ -31,6 +31,7 @@ import org.asamk.signal.manager.internal.SignalDependencies;
|
|||
import org.asamk.signal.manager.jobs.RetrieveStickerPackJob;
|
||||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||
import org.asamk.signal.manager.storage.stickers.StickerPack;
|
||||
import org.signal.libsignal.metadata.ProtocolInvalidKeyException;
|
||||
|
@ -525,12 +526,12 @@ public final class IncomingMessageHandler {
|
|||
}
|
||||
if (syncMessage.getBlockedList().isPresent()) {
|
||||
final var blockedListMessage = syncMessage.getBlockedList().get();
|
||||
for (var address : blockedListMessage.getAddresses()) {
|
||||
context.getContactHelper()
|
||||
.setContactBlocked(account.getRecipientResolver().resolveRecipient(address), true);
|
||||
for (var individual : blockedListMessage.individuals) {
|
||||
final var address = new RecipientAddress(individual.getAci(), individual.getE164());
|
||||
final var recipientId = account.getRecipientResolver().resolveRecipient(address);
|
||||
context.getContactHelper().setContactBlocked(recipientId, true);
|
||||
}
|
||||
for (var groupId : blockedListMessage.getGroupIds()
|
||||
.stream()
|
||||
for (var groupId : blockedListMessage.groupIds.stream()
|
||||
.map(GroupId::unknownVersion)
|
||||
.collect(Collectors.toSet())) {
|
||||
try {
|
||||
|
@ -585,14 +586,22 @@ public final class IncomingMessageHandler {
|
|||
}
|
||||
if (syncMessage.getKeys().isPresent()) {
|
||||
final var keysMessage = syncMessage.getKeys().get();
|
||||
if (keysMessage.getStorageService().isPresent()) {
|
||||
final var storageKey = keysMessage.getStorageService().get();
|
||||
if (keysMessage.getAccountEntropyPool() != null) {
|
||||
final var aep = keysMessage.getAccountEntropyPool();
|
||||
account.setAccountEntropyPool(aep);
|
||||
actions.add(SyncStorageDataAction.create());
|
||||
} else if (keysMessage.getMaster() != null) {
|
||||
final var masterKey = keysMessage.getMaster();
|
||||
account.setMasterKey(masterKey);
|
||||
actions.add(SyncStorageDataAction.create());
|
||||
} else if (keysMessage.getStorageService() != null) {
|
||||
final var storageKey = keysMessage.getStorageService();
|
||||
account.setStorageKey(storageKey);
|
||||
actions.add(SyncStorageDataAction.create());
|
||||
}
|
||||
if (keysMessage.getMaster().isPresent()) {
|
||||
final var masterKey = keysMessage.getMaster().get();
|
||||
account.setMasterKey(masterKey);
|
||||
if (keysMessage.getMediaRootBackupKey() != null) {
|
||||
final var mrb = keysMessage.getMediaRootBackupKey();
|
||||
account.setMediaRootBackupKey(mrb);
|
||||
actions.add(SyncStorageDataAction.create());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -172,7 +172,7 @@ public class PreKeyHelper {
|
|||
// This can happen when the primary device has changed phone number
|
||||
logger.warn("Failed to updated pre keys: {}", e.getMessage());
|
||||
} catch (NonSuccessfulResponseCodeException e) {
|
||||
if (serviceIdType != ServiceIdType.PNI || e.getCode() != 422) {
|
||||
if (serviceIdType != ServiceIdType.PNI || e.code != 422) {
|
||||
throw e;
|
||||
}
|
||||
logger.warn("Failed to set PNI pre keys, ignoring for now. Account needs to be reregistered to fix this.");
|
||||
|
|
|
@ -336,13 +336,6 @@ public final class ProfileHelper {
|
|||
|
||||
final var profile = account.getProfileStore().getProfile(recipientId);
|
||||
|
||||
if (recipientId.equals(account.getSelfRecipientId())) {
|
||||
final var isUnrestricted = encryptedProfile.isUnrestrictedUnidentifiedAccess();
|
||||
if (account.isUnrestrictedUnidentifiedAccess() != isUnrestricted) {
|
||||
account.setUnrestrictedUnidentifiedAccess(isUnrestricted);
|
||||
}
|
||||
}
|
||||
|
||||
Profile newProfile = null;
|
||||
if (profileKey.isPresent()) {
|
||||
logger.trace("Decrypting profile");
|
||||
|
@ -358,6 +351,18 @@ public final class ProfileHelper {
|
|||
.build();
|
||||
}
|
||||
|
||||
if (recipientId.equals(account.getSelfRecipientId())) {
|
||||
final var isUnrestricted = encryptedProfile.isUnrestrictedUnidentifiedAccess();
|
||||
if (account.isUnrestrictedUnidentifiedAccess() != isUnrestricted) {
|
||||
account.setUnrestrictedUnidentifiedAccess(isUnrestricted);
|
||||
}
|
||||
if (account.isPrimaryDevice() && profile != null && newProfile.getCapabilities()
|
||||
.contains(Profile.Capability.storageServiceEncryptionV2Capability) && !profile.getCapabilities()
|
||||
.contains(Profile.Capability.storageServiceEncryptionV2Capability)) {
|
||||
context.getJobExecutor().enqueueJob(new SyncStorageJob(true));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
logger.trace("Storing identity");
|
||||
final var identityKey = new IdentityKey(Base64.getDecoder().decode(encryptedProfile.getIdentityKey()));
|
||||
|
|
|
@ -239,7 +239,6 @@ public class RecipientHelper {
|
|||
newNumbers,
|
||||
account.getRecipientStore().getServiceIdToProfileKeyMap(),
|
||||
token,
|
||||
dependencies.getServiceEnvironmentConfig().cdsiMrenclave(),
|
||||
null,
|
||||
dependencies.getLibSignalNetwork(),
|
||||
newToken -> {
|
||||
|
|
|
@ -2,6 +2,7 @@ package org.asamk.signal.manager.helper;
|
|||
|
||||
import org.asamk.signal.manager.api.GroupIdV1;
|
||||
import org.asamk.signal.manager.api.GroupIdV2;
|
||||
import org.asamk.signal.manager.api.Profile;
|
||||
import org.asamk.signal.manager.internal.SignalDependencies;
|
||||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||
|
@ -17,11 +18,17 @@ import org.signal.core.util.SetUtil;
|
|||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.storage.RecordIkm;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
||||
import org.whispersystems.signalservice.api.storage.StorageRecordConvertersKt;
|
||||
import org.whispersystems.signalservice.api.storage.StorageServiceRepository;
|
||||
import org.whispersystems.signalservice.api.storage.StorageServiceRepository.ManifestIfDifferentVersionResult;
|
||||
import org.whispersystems.signalservice.api.storage.StorageServiceRepository.WriteStorageRecordsResult;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.StorageRecord;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.Connection;
|
||||
|
@ -32,9 +39,10 @@ import java.util.Collection;
|
|||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
||||
|
||||
public class StorageHelper {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(StorageHelper.class);
|
||||
|
@ -54,7 +62,7 @@ public class StorageHelper {
|
|||
}
|
||||
|
||||
public void syncDataWithStorage() throws IOException {
|
||||
final var storageKey = account.getOrCreateStorageKey();
|
||||
var storageKey = account.getOrCreateStorageKey();
|
||||
if (storageKey == null) {
|
||||
if (!account.isPrimaryDevice()) {
|
||||
logger.debug("Storage key unknown, requesting from primary device.");
|
||||
|
@ -65,52 +73,76 @@ public class StorageHelper {
|
|||
|
||||
logger.trace("Reading manifest from remote storage");
|
||||
final var localManifestVersion = account.getStorageManifestVersion();
|
||||
final var localManifest = account.getStorageManifest().orElse(SignalStorageManifest.EMPTY);
|
||||
SignalStorageManifest remoteManifest;
|
||||
try {
|
||||
remoteManifest = dependencies.getAccountManager()
|
||||
.getStorageManifestIfDifferentVersion(storageKey, localManifestVersion)
|
||||
.orElse(localManifest);
|
||||
} catch (InvalidKeyException e) {
|
||||
logger.warn("Manifest couldn't be decrypted.");
|
||||
if (account.isPrimaryDevice()) {
|
||||
try {
|
||||
forcePushToStorage(storageKey);
|
||||
} catch (RetryLaterException rle) {
|
||||
// TODO retry later
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
logger.trace("Manifest versions: local {}, remote {}", localManifestVersion, remoteManifest.getVersion());
|
||||
final var localManifest = account.getStorageManifest().orElse(SignalStorageManifest.Companion.getEMPTY());
|
||||
final var storageServiceRepository = dependencies.getStorageServiceRepository();
|
||||
final var result = storageServiceRepository.getStorageManifestIfDifferentVersion(storageKey,
|
||||
localManifestVersion);
|
||||
|
||||
var needsForcePush = false;
|
||||
if (remoteManifest.getVersion() > localManifestVersion) {
|
||||
logger.trace("Remote version was newer, reading records.");
|
||||
needsForcePush = readDataFromStorage(storageKey, localManifest, remoteManifest);
|
||||
} else if (remoteManifest.getVersion() < localManifest.getVersion()) {
|
||||
logger.debug("Remote storage manifest version was older. User might have switched accounts.");
|
||||
}
|
||||
logger.trace("Done reading data from remote storage");
|
||||
final var remoteManifest = switch (result) {
|
||||
case ManifestIfDifferentVersionResult.DifferentVersion diff -> {
|
||||
final var manifest = diff.getManifest();
|
||||
storeManifestLocally(manifest);
|
||||
yield manifest;
|
||||
}
|
||||
case ManifestIfDifferentVersionResult.DecryptionError ignore -> {
|
||||
logger.warn("Manifest couldn't be decrypted.");
|
||||
if (account.isPrimaryDevice()) {
|
||||
needsForcePush = true;
|
||||
} else {
|
||||
context.getSyncHelper().requestSyncKeys();
|
||||
}
|
||||
yield null;
|
||||
}
|
||||
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) {
|
||||
storeManifestLocally(remoteManifest);
|
||||
}
|
||||
if (remoteManifest != null) {
|
||||
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");
|
||||
account.getRecipientStore().setMissingStorageIds();
|
||||
account.getGroupStore().setMissingStorageIds();
|
||||
|
||||
var needsMultiDeviceSync = false;
|
||||
try {
|
||||
needsMultiDeviceSync = writeToStorage(storageKey, remoteManifest, needsForcePush);
|
||||
} catch (RetryLaterException e) {
|
||||
// TODO retry later
|
||||
return;
|
||||
|
||||
if (account.needsStorageKeyMigration()) {
|
||||
logger.debug("Storage needs force push due to new account entropy pool");
|
||||
// Set new aep and reset previous master key and storage key
|
||||
account.setAccountEntropyPool(account.getOrCreateAccountEntropyPool());
|
||||
storageKey = account.getOrCreateStorageKey();
|
||||
context.getSyncHelper().sendKeysMessage();
|
||||
needsForcePush = true;
|
||||
} else if (remoteManifest == null) {
|
||||
if (account.isPrimaryDevice()) {
|
||||
needsForcePush = true;
|
||||
}
|
||||
} else if (remoteManifest.recordIkm == null && account.getSelfRecipientProfile()
|
||||
.getCapabilities()
|
||||
.contains(Profile.Capability.storageServiceEncryptionV2Capability)) {
|
||||
logger.debug("The SSRE2 capability is supported, but no recordIkm is set! Force pushing.");
|
||||
needsForcePush = true;
|
||||
} else {
|
||||
try {
|
||||
needsMultiDeviceSync = writeToStorage(storageKey, remoteManifest, needsForcePush);
|
||||
} catch (RetryLaterException e) {
|
||||
// TODO retry later
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsForcePush) {
|
||||
|
@ -131,6 +163,23 @@ public class StorageHelper {
|
|||
logger.debug("Done syncing data with remote storage");
|
||||
}
|
||||
|
||||
public void forcePushToStorage() throws IOException {
|
||||
if (!account.isPrimaryDevice()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final var storageKey = account.getOrCreateStorageKey();
|
||||
if (storageKey == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
forcePushToStorage(storageKey);
|
||||
} catch (RetryLaterException e) {
|
||||
// TODO retry later
|
||||
}
|
||||
}
|
||||
|
||||
private boolean readDataFromStorage(
|
||||
final StorageKey storageKey,
|
||||
final SignalStorageManifest localManifest,
|
||||
|
@ -140,14 +189,14 @@ public class StorageHelper {
|
|||
try (final var connection = account.getAccountDatabase().getConnection()) {
|
||||
connection.setAutoCommit(false);
|
||||
|
||||
var idDifference = findIdDifference(remoteManifest.getStorageIds(), localManifest.getStorageIds());
|
||||
var idDifference = findIdDifference(remoteManifest.storageIds, localManifest.storageIds);
|
||||
|
||||
if (idDifference.hasTypeMismatches() && account.isPrimaryDevice()) {
|
||||
logger.debug("Found type mismatches in the ID sets! Scheduling a force push after this sync completes.");
|
||||
needsForcePush = true;
|
||||
}
|
||||
|
||||
logger.debug("Pre-Merge ID Difference :: " + idDifference);
|
||||
logger.debug("Pre-Merge ID Difference :: {}", idDifference);
|
||||
|
||||
if (!idDifference.localOnlyIds().isEmpty()) {
|
||||
final var updated = account.getRecipientStore()
|
||||
|
@ -161,15 +210,15 @@ public class StorageHelper {
|
|||
}
|
||||
|
||||
if (!idDifference.isEmpty()) {
|
||||
final var remoteOnlyRecords = getSignalStorageRecords(storageKey, idDifference.remoteOnlyIds());
|
||||
final var remoteOnlyRecords = getSignalStorageRecords(storageKey,
|
||||
remoteManifest,
|
||||
idDifference.remoteOnlyIds());
|
||||
|
||||
if (remoteOnlyRecords.size() != idDifference.remoteOnlyIds().size()) {
|
||||
logger.debug("Could not find all remote-only records! Requested: "
|
||||
+ idDifference.remoteOnlyIds()
|
||||
.size()
|
||||
+ ", Found: "
|
||||
+ remoteOnlyRecords.size()
|
||||
+ ". These stragglers should naturally get deleted during the sync.");
|
||||
logger.debug(
|
||||
"Could not find all remote-only records! Requested: {}, Found: {}. These stragglers should naturally get deleted during the sync.",
|
||||
idDifference.remoteOnlyIds().size(),
|
||||
remoteOnlyRecords.size());
|
||||
}
|
||||
|
||||
final var unknownInserts = processKnownRecords(connection, remoteOnlyRecords);
|
||||
|
@ -194,18 +243,21 @@ public class StorageHelper {
|
|||
return needsForcePush;
|
||||
}
|
||||
|
||||
private void readRecordsWithPreviouslyUnknownTypes(final StorageKey storageKey) throws IOException {
|
||||
private void readRecordsWithPreviouslyUnknownTypes(
|
||||
final StorageKey storageKey,
|
||||
final SignalStorageManifest remoteManifest
|
||||
) throws IOException {
|
||||
try (final var connection = account.getAccountDatabase().getConnection()) {
|
||||
connection.setAutoCommit(false);
|
||||
final var knownUnknownIds = account.getUnknownStorageIdStore()
|
||||
.getUnknownStorageIds(connection, KNOWN_TYPES);
|
||||
|
||||
if (!knownUnknownIds.isEmpty()) {
|
||||
logger.debug("We have " + knownUnknownIds.size() + " unknown records that we can now process.");
|
||||
logger.debug("We have {} unknown records that we can now process.", knownUnknownIds.size());
|
||||
|
||||
final var remote = getSignalStorageRecords(storageKey, knownUnknownIds);
|
||||
final var remote = getSignalStorageRecords(storageKey, remoteManifest, knownUnknownIds);
|
||||
|
||||
logger.debug("Found " + remote.size() + " of the known-unknowns remotely.");
|
||||
logger.debug("Found {} of the known-unknowns remotely.", remote.size());
|
||||
|
||||
processKnownRecords(connection, remote);
|
||||
account.getUnknownStorageIdStore()
|
||||
|
@ -227,15 +279,16 @@ public class StorageHelper {
|
|||
connection.setAutoCommit(false);
|
||||
|
||||
final var localStorageIds = getAllLocalStorageIds(connection);
|
||||
final var idDifference = findIdDifference(remoteManifest.getStorageIds(), localStorageIds);
|
||||
logger.debug("ID Difference :: " + idDifference);
|
||||
final var idDifference = findIdDifference(remoteManifest.storageIds, localStorageIds);
|
||||
logger.debug("ID Difference :: {}", idDifference);
|
||||
|
||||
final var remoteDeletes = idDifference.remoteOnlyIds().stream().map(StorageId::getRaw).toList();
|
||||
final var remoteInserts = buildLocalStorageRecords(connection, idDifference.localOnlyIds());
|
||||
// TODO check if local storage record proto matches remote, then reset to remote storage_id
|
||||
|
||||
remoteWriteOperation = new WriteOperationResult(new SignalStorageManifest(remoteManifest.getVersion() + 1,
|
||||
remoteWriteOperation = new WriteOperationResult(new SignalStorageManifest(remoteManifest.version + 1,
|
||||
account.getDeviceId(),
|
||||
remoteManifest.recordIkm,
|
||||
localStorageIds), remoteInserts, remoteDeletes);
|
||||
|
||||
connection.commit();
|
||||
|
@ -244,39 +297,37 @@ public class StorageHelper {
|
|||
}
|
||||
|
||||
if (remoteWriteOperation.isEmpty()) {
|
||||
logger.debug("No remote writes needed. Still at version: " + remoteManifest.getVersion());
|
||||
logger.debug("No remote writes needed. Still at version: {}", remoteManifest.version);
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.debug("We have something to write remotely.");
|
||||
logger.debug("WriteOperationResult :: " + remoteWriteOperation);
|
||||
logger.debug("WriteOperationResult :: {}", remoteWriteOperation);
|
||||
|
||||
StorageSyncValidations.validate(remoteWriteOperation,
|
||||
remoteManifest,
|
||||
needsForcePush,
|
||||
account.getSelfRecipientAddress());
|
||||
|
||||
final Optional<SignalStorageManifest> conflict;
|
||||
try {
|
||||
conflict = dependencies.getAccountManager()
|
||||
.writeStorageRecords(storageKey,
|
||||
remoteWriteOperation.manifest(),
|
||||
remoteWriteOperation.inserts(),
|
||||
remoteWriteOperation.deletes());
|
||||
} catch (InvalidKeyException e) {
|
||||
logger.warn("Failed to decrypt conflicting storage manifest: {}", e.getMessage());
|
||||
throw new IOException(e);
|
||||
final var result = dependencies.getStorageServiceRepository()
|
||||
.writeStorageRecords(storageKey,
|
||||
remoteWriteOperation.manifest(),
|
||||
remoteWriteOperation.inserts(),
|
||||
remoteWriteOperation.deletes());
|
||||
switch (result) {
|
||||
case WriteStorageRecordsResult.ConflictError ignored -> {
|
||||
logger.debug("Hit a conflict when trying to resolve the conflict! Retrying.");
|
||||
throw new RetryLaterException();
|
||||
}
|
||||
case WriteStorageRecordsResult.NetworkError networkError -> throw networkError.getException();
|
||||
case WriteStorageRecordsResult.StatusCodeError statusCodeError -> throw statusCodeError.getException();
|
||||
case WriteStorageRecordsResult.Success ignored -> {
|
||||
logger.debug("Saved new manifest. Now at version: {}", remoteWriteOperation.manifest().version);
|
||||
storeManifestLocally(remoteWriteOperation.manifest());
|
||||
return true;
|
||||
}
|
||||
default -> throw new IllegalStateException("Unexpected value: " + result);
|
||||
}
|
||||
|
||||
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(
|
||||
|
@ -284,7 +335,8 @@ public class StorageHelper {
|
|||
) throws IOException, RetryLaterException {
|
||||
logger.debug("Force pushing local state to remote storage");
|
||||
|
||||
final var currentVersion = dependencies.getAccountManager().getStorageManifestVersion();
|
||||
final var currentVersion = handleResponseException(dependencies.getStorageServiceRepository()
|
||||
.getManifestVersion());
|
||||
final var newVersion = currentVersion + 1;
|
||||
final var newStorageRecords = new ArrayList<SignalStorageRecord>();
|
||||
final Map<RecipientId, StorageId> newContactStorageIds;
|
||||
|
@ -302,15 +354,16 @@ public class StorageHelper {
|
|||
final var recipient = account.getRecipientStore().getRecipient(connection, recipientId);
|
||||
final var accountRecord = StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(),
|
||||
recipient,
|
||||
account.getUsernameLink(),
|
||||
storageId.getRaw());
|
||||
newStorageRecords.add(accountRecord);
|
||||
account.getUsernameLink());
|
||||
newStorageRecords.add(new SignalStorageRecord(storageId,
|
||||
new StorageRecord.Builder().account(accountRecord).build()));
|
||||
} else {
|
||||
final var recipient = account.getRecipientStore().getRecipient(connection, recipientId);
|
||||
final var address = recipient.getAddress().getIdentifier();
|
||||
final var identity = account.getIdentityKeyStore().getIdentityInfo(connection, address);
|
||||
final var record = StorageSyncModels.localToRemoteRecord(recipient, identity, storageId.getRaw());
|
||||
newStorageRecords.add(record);
|
||||
final var record = StorageSyncModels.localToRemoteRecord(recipient, identity);
|
||||
newStorageRecords.add(new SignalStorageRecord(storageId,
|
||||
new StorageRecord.Builder().contact(record).build()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -319,8 +372,9 @@ public class StorageHelper {
|
|||
for (final var groupId : groupV1Ids) {
|
||||
final var storageId = newGroupV1StorageIds.get(groupId);
|
||||
final var group = account.getGroupStore().getGroup(connection, groupId);
|
||||
final var record = StorageSyncModels.localToRemoteRecord(group, storageId.getRaw());
|
||||
newStorageRecords.add(record);
|
||||
final var record = StorageSyncModels.localToRemoteRecord(group);
|
||||
newStorageRecords.add(new SignalStorageRecord(storageId,
|
||||
new StorageRecord.Builder().groupV1(record).build()));
|
||||
}
|
||||
|
||||
final var groupV2Ids = account.getGroupStore().getGroupV2Ids(connection);
|
||||
|
@ -328,8 +382,9 @@ public class StorageHelper {
|
|||
for (final var groupId : groupV2Ids) {
|
||||
final var storageId = newGroupV2StorageIds.get(groupId);
|
||||
final var group = account.getGroupStore().getGroup(connection, groupId);
|
||||
final var record = StorageSyncModels.localToRemoteRecord(group, storageId.getRaw());
|
||||
newStorageRecords.add(record);
|
||||
final var record = StorageSyncModels.localToRemoteRecord(group);
|
||||
newStorageRecords.add(new SignalStorageRecord(storageId,
|
||||
new StorageRecord.Builder().groupV2(record).build()));
|
||||
}
|
||||
|
||||
connection.commit();
|
||||
|
@ -338,34 +393,46 @@ public class StorageHelper {
|
|||
}
|
||||
final var newStorageIds = newStorageRecords.stream().map(SignalStorageRecord::getId).toList();
|
||||
|
||||
final var manifest = new SignalStorageManifest(newVersion, account.getDeviceId(), newStorageIds);
|
||||
final RecordIkm recordIkm;
|
||||
if (account.getSelfRecipientProfile()
|
||||
.getCapabilities()
|
||||
.contains(Profile.Capability.storageServiceEncryptionV2Capability)) {
|
||||
logger.debug("Generating and including a new recordIkm.");
|
||||
recordIkm = RecordIkm.Companion.generate();
|
||||
} else {
|
||||
logger.debug("SSRE2 not yet supported. Not including recordIkm.");
|
||||
recordIkm = null;
|
||||
}
|
||||
|
||||
final var manifest = new SignalStorageManifest(newVersion, account.getDeviceId(), recordIkm, newStorageIds);
|
||||
|
||||
StorageSyncValidations.validateForcePush(manifest, newStorageRecords, account.getSelfRecipientAddress());
|
||||
|
||||
final Optional<SignalStorageManifest> conflict;
|
||||
try {
|
||||
if (newVersion > 1) {
|
||||
logger.trace("Force-pushing data. Inserting {} IDs.", newStorageRecords.size());
|
||||
conflict = dependencies.getAccountManager()
|
||||
.resetStorageRecords(storageServiceKey, manifest, newStorageRecords);
|
||||
} else {
|
||||
logger.trace("First version, normal push. Inserting {} IDs.", newStorageRecords.size());
|
||||
conflict = dependencies.getAccountManager()
|
||||
.writeStorageRecords(storageServiceKey, manifest, newStorageRecords, Collections.emptyList());
|
||||
final WriteStorageRecordsResult result;
|
||||
if (newVersion > 1) {
|
||||
logger.trace("Force-pushing data. Inserting {} IDs.", newStorageRecords.size());
|
||||
result = dependencies.getStorageServiceRepository()
|
||||
.resetAndWriteStorageRecords(storageServiceKey, manifest, newStorageRecords);
|
||||
} else {
|
||||
logger.trace("First version, normal push. Inserting {} IDs.", newStorageRecords.size());
|
||||
result = dependencies.getStorageServiceRepository()
|
||||
.writeStorageRecords(storageServiceKey, manifest, newStorageRecords, Collections.emptyList());
|
||||
}
|
||||
|
||||
switch (result) {
|
||||
case WriteStorageRecordsResult.ConflictError ignored -> {
|
||||
logger.debug("Hit a conflict. Trying again.");
|
||||
throw new RetryLaterException();
|
||||
}
|
||||
} catch (InvalidKeyException e) {
|
||||
logger.debug("Hit an invalid key exception, which likely indicates a conflict.", e);
|
||||
throw new RetryLaterException();
|
||||
case WriteStorageRecordsResult.NetworkError networkError -> throw networkError.getException();
|
||||
case WriteStorageRecordsResult.StatusCodeError statusCodeError -> throw statusCodeError.getException();
|
||||
case WriteStorageRecordsResult.Success ignored -> {
|
||||
logger.debug("Force push succeeded. Updating local manifest version to: {}", manifest.version);
|
||||
storeManifestLocally(manifest);
|
||||
}
|
||||
default -> throw new IllegalStateException("Unexpected value: " + result);
|
||||
}
|
||||
|
||||
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()) {
|
||||
connection.setAutoCommit(false);
|
||||
account.getRecipientStore().updateStorageIds(connection, newContactStorageIds);
|
||||
|
@ -405,22 +472,35 @@ public class StorageHelper {
|
|||
private void storeManifestLocally(
|
||||
final SignalStorageManifest remoteManifest
|
||||
) {
|
||||
account.setStorageManifestVersion(remoteManifest.getVersion());
|
||||
account.setStorageManifestVersion(remoteManifest.version);
|
||||
account.setStorageManifest(remoteManifest);
|
||||
}
|
||||
|
||||
private List<SignalStorageRecord> getSignalStorageRecords(
|
||||
final StorageKey storageKey,
|
||||
final SignalStorageManifest manifest,
|
||||
final List<StorageId> storageIds
|
||||
) throws IOException {
|
||||
List<SignalStorageRecord> records;
|
||||
try {
|
||||
records = dependencies.getAccountManager().readStorageRecords(storageKey, storageIds);
|
||||
} catch (InvalidKeyException e) {
|
||||
logger.warn("Failed to read storage records, ignoring.");
|
||||
return List.of();
|
||||
}
|
||||
return records;
|
||||
final var result = dependencies.getStorageServiceRepository()
|
||||
.readStorageRecords(storageKey, manifest.recordIkm, storageIds);
|
||||
return switch (result) {
|
||||
case StorageServiceRepository.StorageRecordResult.DecryptionError decryptionError -> {
|
||||
if (decryptionError.getException() instanceof InvalidKeyException) {
|
||||
logger.warn("Failed to read storage records, ignoring.");
|
||||
yield List.of();
|
||||
} else if (decryptionError.getException() instanceof IOException ioe) {
|
||||
throw ioe;
|
||||
} else {
|
||||
throw new IOException(decryptionError.getException());
|
||||
}
|
||||
}
|
||||
case StorageServiceRepository.StorageRecordResult.NetworkError networkError ->
|
||||
throw networkError.getException();
|
||||
case StorageServiceRepository.StorageRecordResult.StatusCodeError statusCodeError ->
|
||||
throw statusCodeError.getException();
|
||||
case StorageServiceRepository.StorageRecordResult.Success success -> success.getRecords();
|
||||
default -> throw new IllegalStateException("Unexpected value: " + result);
|
||||
};
|
||||
}
|
||||
|
||||
private List<StorageId> getAllLocalStorageIds(final Connection connection) throws SQLException {
|
||||
|
@ -436,12 +516,10 @@ public class StorageHelper {
|
|||
final Connection connection,
|
||||
final List<StorageId> storageIds
|
||||
) throws SQLException {
|
||||
final var records = new ArrayList<SignalStorageRecord>();
|
||||
final var records = new ArrayList<SignalStorageRecord>(storageIds.size());
|
||||
for (final var storageId : storageIds) {
|
||||
final var record = buildLocalStorageRecord(connection, storageId);
|
||||
if (record != null) {
|
||||
records.add(record);
|
||||
}
|
||||
records.add(record);
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
@ -455,25 +533,31 @@ public class StorageHelper {
|
|||
final var recipient = account.getRecipientStore().getRecipient(connection, storageId);
|
||||
final var address = recipient.getAddress().getIdentifier();
|
||||
final var identity = account.getIdentityKeyStore().getIdentityInfo(connection, address);
|
||||
yield StorageSyncModels.localToRemoteRecord(recipient, identity, storageId.getRaw());
|
||||
final var record = StorageSyncModels.localToRemoteRecord(recipient, identity);
|
||||
yield new SignalStorageRecord(storageId, new StorageRecord.Builder().contact(record).build());
|
||||
}
|
||||
case ManifestRecord.Identifier.Type.GROUPV1 -> {
|
||||
final var groupV1 = account.getGroupStore().getGroupV1(connection, storageId);
|
||||
yield StorageSyncModels.localToRemoteRecord(groupV1, storageId.getRaw());
|
||||
final var record = StorageSyncModels.localToRemoteRecord(groupV1);
|
||||
yield new SignalStorageRecord(storageId, new StorageRecord.Builder().groupV1(record).build());
|
||||
}
|
||||
case ManifestRecord.Identifier.Type.GROUPV2 -> {
|
||||
final var groupV2 = account.getGroupStore().getGroupV2(connection, storageId);
|
||||
yield StorageSyncModels.localToRemoteRecord(groupV2, storageId.getRaw());
|
||||
final var record = StorageSyncModels.localToRemoteRecord(groupV2);
|
||||
yield new SignalStorageRecord(storageId, new StorageRecord.Builder().groupV2(record).build());
|
||||
}
|
||||
case ManifestRecord.Identifier.Type.ACCOUNT -> {
|
||||
final var selfRecipient = account.getRecipientStore()
|
||||
.getRecipient(connection, account.getSelfRecipientId());
|
||||
yield StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(),
|
||||
|
||||
final var record = StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(),
|
||||
selfRecipient,
|
||||
account.getUsernameLink(),
|
||||
storageId.getRaw());
|
||||
account.getUsernameLink());
|
||||
yield new SignalStorageRecord(storageId, new StorageRecord.Builder().account(record).build());
|
||||
}
|
||||
case null, default -> {
|
||||
throw new AssertionError("Got unknown local storage record type: " + storageId);
|
||||
}
|
||||
case null, default -> throw new AssertionError("Got unknown local storage record type: " + storageId);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -537,13 +621,24 @@ public class StorageHelper {
|
|||
final var groupV2RecordProcessor = new GroupV2RecordProcessor(account, connection);
|
||||
|
||||
for (final var record : records) {
|
||||
logger.debug("Reading record of type {}", record.getType());
|
||||
switch (ManifestRecord.Identifier.Type.fromValue(record.getType())) {
|
||||
case ACCOUNT -> accountRecordProcessor.process(record.getAccount().get());
|
||||
case GROUPV1 -> groupV1RecordProcessor.process(record.getGroupV1().get());
|
||||
case GROUPV2 -> groupV2RecordProcessor.process(record.getGroupV2().get());
|
||||
case CONTACT -> contactRecordProcessor.process(record.getContact().get());
|
||||
case null, default -> unknownRecords.add(record.getId());
|
||||
if (record.getProto().account != null) {
|
||||
logger.debug("Reading record {} of type account", record.getId());
|
||||
accountRecordProcessor.process(StorageRecordConvertersKt.toSignalAccountRecord(record.getProto().account,
|
||||
record.getId()));
|
||||
} else if (record.getProto().groupV1 != null) {
|
||||
logger.debug("Reading record {} of type groupV1", record.getId());
|
||||
groupV1RecordProcessor.process(StorageRecordConvertersKt.toSignalGroupV1Record(record.getProto().groupV1,
|
||||
record.getId()));
|
||||
} else if (record.getProto().groupV2 != null) {
|
||||
logger.debug("Reading record {} of type groupV2", record.getId());
|
||||
groupV2RecordProcessor.process(StorageRecordConvertersKt.toSignalGroupV2Record(record.getProto().groupV2,
|
||||
record.getId()));
|
||||
} else if (record.getProto().contact != null) {
|
||||
logger.debug("Reading record {} of type contact", record.getId());
|
||||
contactRecordProcessor.process(StorageRecordConvertersKt.toSignalContactRecord(record.getProto().contact,
|
||||
record.getId()));
|
||||
} else {
|
||||
unknownRecords.add(record.getId());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -247,10 +247,14 @@ public class SyncHelper {
|
|||
}
|
||||
|
||||
public SendMessageResult sendBlockedList() {
|
||||
var addresses = new ArrayList<SignalServiceAddress>();
|
||||
var addresses = new ArrayList<BlockedListMessage.Individual>();
|
||||
for (var record : account.getContactStore().getContacts()) {
|
||||
if (record.second().isBlocked()) {
|
||||
addresses.add(context.getRecipientHelper().resolveSignalServiceAddress(record.first()));
|
||||
final var address = account.getRecipientAddressResolver().resolveRecipientAddress(record.first());
|
||||
if (address.aci().isPresent() || address.number().isPresent()) {
|
||||
addresses.add(new BlockedListMessage.Individual(address.aci().orElse(null),
|
||||
address.number().orElse(null)));
|
||||
}
|
||||
}
|
||||
}
|
||||
var groupIds = new ArrayList<byte[]>();
|
||||
|
@ -276,8 +280,10 @@ public class SyncHelper {
|
|||
}
|
||||
|
||||
public SendMessageResult sendKeysMessage() {
|
||||
var keysMessage = new KeysMessage(Optional.ofNullable(account.getOrCreateStorageKey()),
|
||||
Optional.ofNullable(account.getOrCreatePinMasterKey()));
|
||||
var keysMessage = new KeysMessage(account.getOrCreateStorageKey(),
|
||||
account.getOrCreatePinMasterKey(),
|
||||
account.getOrCreateAccountEntropyPool(),
|
||||
account.getOrCreateMediaRootBackupKey());
|
||||
return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forKeys(keysMessage));
|
||||
}
|
||||
|
||||
|
@ -405,7 +411,7 @@ public class SyncHelper {
|
|||
builder.withMessageExpirationTimeVersion(c.getExpirationTimerVersion().get());
|
||||
} else {
|
||||
logger.debug(
|
||||
"[ContactSync] {} was synced with an old expiration timer. Ignoring. Received: {} Current: ${}",
|
||||
"[ContactSync] {} was synced with an old expiration timer. Ignoring. Received: {} Current: {}",
|
||||
recipientId,
|
||||
c.getExpirationTimerVersion(),
|
||||
contact == null ? 1 : contact.messageExpirationTimeVersion());
|
||||
|
|
|
@ -15,10 +15,13 @@ import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
|
|||
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||
import org.whispersystems.signalservice.api.link.LinkDeviceApi;
|
||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.registration.RegistrationApi;
|
||||
import org.whispersystems.signalservice.api.services.ProfileService;
|
||||
import org.whispersystems.signalservice.api.storage.StorageServiceApi;
|
||||
import org.whispersystems.signalservice.api.storage.StorageServiceRepository;
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery;
|
||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||
import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
|
||||
|
@ -49,6 +52,8 @@ public class SignalDependencies {
|
|||
private SignalServiceAccountManager accountManager;
|
||||
private GroupsV2Api groupsV2Api;
|
||||
private RegistrationApi registrationApi;
|
||||
private LinkDeviceApi linkDeviceApi;
|
||||
private StorageServiceApi storageServiceApi;
|
||||
private GroupsV2Operations groupsV2Operations;
|
||||
private ClientZkOperations clientZkOperations;
|
||||
|
||||
|
@ -155,6 +160,19 @@ public class SignalDependencies {
|
|||
return getOrCreate(() -> registrationApi, () -> registrationApi = getAccountManager().getRegistrationApi());
|
||||
}
|
||||
|
||||
public LinkDeviceApi getLinkDeviceApi() {
|
||||
return getOrCreate(() -> linkDeviceApi, () -> linkDeviceApi = new LinkDeviceApi(getPushServiceSocket()));
|
||||
}
|
||||
|
||||
private StorageServiceApi getStorageServiceApi() {
|
||||
return getOrCreate(() -> storageServiceApi,
|
||||
() -> storageServiceApi = new StorageServiceApi(getPushServiceSocket()));
|
||||
}
|
||||
|
||||
public StorageServiceRepository getStorageServiceRepository() {
|
||||
return new StorageServiceRepository(getStorageServiceApi());
|
||||
}
|
||||
|
||||
public GroupsV2Operations getGroupsV2Operations() {
|
||||
return getOrCreate(() -> groupsV2Operations,
|
||||
() -> groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.signalServiceConfiguration()),
|
||||
|
|
|
@ -8,13 +8,27 @@ import java.io.IOException;
|
|||
|
||||
public class SyncStorageJob implements Job {
|
||||
|
||||
private final boolean forcePush;
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SyncStorageJob.class);
|
||||
|
||||
public SyncStorageJob() {
|
||||
this.forcePush = false;
|
||||
}
|
||||
|
||||
public SyncStorageJob(final boolean forcePush) {
|
||||
this.forcePush = forcePush;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(Context context) {
|
||||
logger.trace("Running storage sync job");
|
||||
try {
|
||||
context.getStorageHelper().syncDataWithStorage();
|
||||
if (forcePush) {
|
||||
context.getStorageHelper().forcePushToStorage();
|
||||
} else {
|
||||
context.getStorageHelper().syncDataWithStorage();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to sync storage data", e);
|
||||
}
|
||||
|
|
|
@ -65,10 +65,12 @@ import org.signal.libsignal.zkgroup.InvalidInputException;
|
|||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.AccountEntropyPool;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountDataStore;
|
||||
import org.whispersystems.signalservice.api.SignalServiceDataStore;
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes;
|
||||
import org.whispersystems.signalservice.api.account.PreKeyCollection;
|
||||
import org.whispersystems.signalservice.api.backup.MediaRootBackupKey;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
|
@ -138,6 +140,8 @@ public class SignalAccount implements Closeable {
|
|||
private String registrationLockPin;
|
||||
private MasterKey pinMasterKey;
|
||||
private StorageKey storageKey;
|
||||
private AccountEntropyPool accountEntropyPool;
|
||||
private MediaRootBackupKey mediaRootBackupKey;
|
||||
private ProfileKey profileKey;
|
||||
|
||||
private Settings settings;
|
||||
|
@ -305,6 +309,7 @@ public class SignalAccount implements Closeable {
|
|||
this.isMultiDevice = true;
|
||||
setLastReceiveTimestamp(0L);
|
||||
this.pinMasterKey = masterKey;
|
||||
this.accountEntropyPool = null;
|
||||
getKeyValueStore().storeEntry(storageManifestVersion, -1L);
|
||||
this.setStorageManifest(null);
|
||||
this.storageKey = null;
|
||||
|
@ -339,6 +344,7 @@ public class SignalAccount implements Closeable {
|
|||
final PreKeyCollection pniPreKeys
|
||||
) {
|
||||
this.pinMasterKey = masterKey;
|
||||
this.accountEntropyPool = null;
|
||||
getKeyValueStore().storeEntry(storageManifestVersion, -1L);
|
||||
this.setStorageManifest(null);
|
||||
this.storageKey = null;
|
||||
|
@ -499,6 +505,12 @@ public class SignalAccount implements Closeable {
|
|||
if (storage.storageKey != null) {
|
||||
storageKey = new StorageKey(base64.decode(storage.storageKey));
|
||||
}
|
||||
if (storage.accountEntropyPool != null) {
|
||||
accountEntropyPool = new AccountEntropyPool(storage.accountEntropyPool);
|
||||
}
|
||||
if (storage.mediaRootBackupKey != null) {
|
||||
mediaRootBackupKey = new MediaRootBackupKey(base64.decode(storage.mediaRootBackupKey));
|
||||
}
|
||||
if (storage.profileKey != null) {
|
||||
try {
|
||||
profileKey = new ProfileKey(base64.decode(storage.profileKey));
|
||||
|
@ -981,6 +993,8 @@ public class SignalAccount implements Closeable {
|
|||
registrationLockPin,
|
||||
pinMasterKey == null ? null : base64.encodeToString(pinMasterKey.serialize()),
|
||||
storageKey == null ? null : base64.encodeToString(storageKey.serialize()),
|
||||
accountEntropyPool == null ? null : accountEntropyPool.getValue(),
|
||||
mediaRootBackupKey == null ? null : base64.encodeToString(mediaRootBackupKey.getValue()),
|
||||
profileKey == null ? null : base64.encodeToString(profileKey.serialize()),
|
||||
usernameLink == null ? null : base64.encodeToString(usernameLink.getEntropy()),
|
||||
usernameLink == null ? null : usernameLink.getServerId().toString());
|
||||
|
@ -1442,6 +1456,10 @@ public class SignalAccount implements Closeable {
|
|||
return selfRecipientId;
|
||||
}
|
||||
|
||||
public Profile getSelfRecipientProfile() {
|
||||
return recipientStore.getProfile(selfRecipientId);
|
||||
}
|
||||
|
||||
public String getSessionId(final String forNumber) {
|
||||
final var keyValueStore = getKeyValueStore();
|
||||
final var sessionNumber = keyValueStore.getEntry(verificationSessionNumber);
|
||||
|
@ -1512,31 +1530,50 @@ public class SignalAccount implements Closeable {
|
|||
public MasterKey getPinBackedMasterKey() {
|
||||
if (registrationLockPin == null) {
|
||||
return null;
|
||||
} else if (!isPrimaryDevice()) {
|
||||
return getMasterKey();
|
||||
}
|
||||
return pinMasterKey;
|
||||
return getOrCreatePinMasterKey();
|
||||
}
|
||||
|
||||
public MasterKey getOrCreatePinMasterKey() {
|
||||
if (pinMasterKey == null) {
|
||||
pinMasterKey = KeyUtils.createMasterKey();
|
||||
save();
|
||||
final var key = getMasterKey();
|
||||
if (key != null) {
|
||||
return key;
|
||||
}
|
||||
|
||||
pinMasterKey = KeyUtils.createMasterKey();
|
||||
save();
|
||||
return pinMasterKey;
|
||||
}
|
||||
|
||||
private MasterKey getMasterKey() {
|
||||
if (pinMasterKey != null) {
|
||||
return pinMasterKey;
|
||||
} else if (accountEntropyPool != null) {
|
||||
return accountEntropyPool.deriveMasterKey();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setMasterKey(MasterKey masterKey) {
|
||||
if (isPrimaryDevice()) {
|
||||
return;
|
||||
}
|
||||
this.pinMasterKey = masterKey;
|
||||
if (masterKey != null) {
|
||||
this.storageKey = null;
|
||||
}
|
||||
save();
|
||||
}
|
||||
|
||||
public StorageKey getOrCreateStorageKey() {
|
||||
if (pinMasterKey != null) {
|
||||
return pinMasterKey.deriveStorageServiceKey();
|
||||
} else if (storageKey != null) {
|
||||
if (storageKey != null) {
|
||||
return storageKey;
|
||||
} else if (pinMasterKey != null) {
|
||||
return pinMasterKey.deriveStorageServiceKey();
|
||||
} else if (accountEntropyPool != null) {
|
||||
return accountEntropyPool.deriveMasterKey().deriveStorageServiceKey();
|
||||
} else if (!isPrimaryDevice() || !isMultiDevice()) {
|
||||
// Only upload storage, if a pin master key already exists or linked devices exist
|
||||
return null;
|
||||
|
@ -1553,6 +1590,40 @@ public class SignalAccount implements Closeable {
|
|||
save();
|
||||
}
|
||||
|
||||
public AccountEntropyPool getOrCreateAccountEntropyPool() {
|
||||
if (accountEntropyPool == null) {
|
||||
accountEntropyPool = AccountEntropyPool.Companion.generate();
|
||||
save();
|
||||
}
|
||||
return accountEntropyPool;
|
||||
}
|
||||
|
||||
public void setAccountEntropyPool(final AccountEntropyPool accountEntropyPool) {
|
||||
this.accountEntropyPool = accountEntropyPool;
|
||||
if (accountEntropyPool != null) {
|
||||
this.storageKey = null;
|
||||
this.pinMasterKey = null;
|
||||
}
|
||||
save();
|
||||
}
|
||||
|
||||
public boolean needsStorageKeyMigration() {
|
||||
return isPrimaryDevice() && (storageKey != null || pinMasterKey != null);
|
||||
}
|
||||
|
||||
public MediaRootBackupKey getOrCreateMediaRootBackupKey() {
|
||||
if (mediaRootBackupKey == null) {
|
||||
mediaRootBackupKey = KeyUtils.createMediaRootBackupKey();
|
||||
save();
|
||||
}
|
||||
return mediaRootBackupKey;
|
||||
}
|
||||
|
||||
public void setMediaRootBackupKey(final MediaRootBackupKey mediaRootBackupKey) {
|
||||
this.mediaRootBackupKey = mediaRootBackupKey;
|
||||
save();
|
||||
}
|
||||
|
||||
public String getRecoveryPassword() {
|
||||
final var masterKey = getPinBackedMasterKey();
|
||||
if (masterKey == null) {
|
||||
|
@ -1575,7 +1646,7 @@ public class SignalAccount implements Closeable {
|
|||
return Optional.empty();
|
||||
}
|
||||
try (var inputStream = new FileInputStream(storageManifestFile)) {
|
||||
return Optional.of(SignalStorageManifest.deserialize(inputStream.readAllBytes()));
|
||||
return Optional.of(SignalStorageManifest.Companion.deserialize(inputStream.readAllBytes()));
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to read local storage manifest.", e);
|
||||
return Optional.empty();
|
||||
|
@ -1882,6 +1953,8 @@ public class SignalAccount implements Closeable {
|
|||
String registrationLockPin,
|
||||
String pinMasterKey,
|
||||
String storageKey,
|
||||
String accountEntropyPool,
|
||||
String mediaRootBackupKey,
|
||||
String profileKey,
|
||||
String usernameLinkEntropy,
|
||||
String usernameLinkServerId
|
||||
|
|
|
@ -12,8 +12,9 @@ import org.slf4j.Logger;
|
|||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
|
||||
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
||||
import org.whispersystems.signalservice.api.util.OptionalUtil;
|
||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.OptionalBool;
|
||||
|
||||
import java.sql.Connection;
|
||||
|
@ -21,6 +22,9 @@ import java.sql.SQLException;
|
|||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.asamk.signal.manager.util.Utils.firstNonEmpty;
|
||||
import static org.asamk.signal.manager.util.Utils.firstNonNull;
|
||||
|
||||
/**
|
||||
* Processes {@link SignalAccountRecord}s.
|
||||
*/
|
||||
|
@ -43,10 +47,10 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||
final var selfRecipientId = account.getSelfRecipientId();
|
||||
final var recipient = account.getRecipientStore().getRecipient(connection, selfRecipientId);
|
||||
final var storageId = account.getRecipientStore().getSelfStorageId(connection);
|
||||
this.localAccountRecord = StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(),
|
||||
recipient,
|
||||
account.getUsernameLink(),
|
||||
storageId.getRaw()).getAccount().get();
|
||||
this.localAccountRecord = new SignalAccountRecord(storageId,
|
||||
StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(),
|
||||
recipient,
|
||||
account.getUsernameLink()));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -60,99 +64,73 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||
}
|
||||
|
||||
@Override
|
||||
protected SignalAccountRecord merge(SignalAccountRecord remote, SignalAccountRecord local) {
|
||||
protected SignalAccountRecord merge(SignalAccountRecord remoteRecord, SignalAccountRecord localRecord) {
|
||||
final var remote = remoteRecord.getProto();
|
||||
final var local = localRecord.getProto();
|
||||
String givenName;
|
||||
String familyName;
|
||||
if (remote.getGivenName().isPresent() || remote.getFamilyName().isPresent()) {
|
||||
givenName = remote.getGivenName().orElse("");
|
||||
familyName = remote.getFamilyName().orElse("");
|
||||
if (!remote.givenName.isEmpty() || !remote.familyName.isEmpty()) {
|
||||
givenName = remote.givenName;
|
||||
familyName = remote.familyName;
|
||||
} else {
|
||||
givenName = local.getGivenName().orElse("");
|
||||
familyName = local.getFamilyName().orElse("");
|
||||
givenName = local.givenName;
|
||||
familyName = local.familyName;
|
||||
}
|
||||
|
||||
final var payments = remote.getPayments().getEntropy().isPresent() ? remote.getPayments() : local.getPayments();
|
||||
final var subscriber = remote.getSubscriber().getId().isPresent()
|
||||
? remote.getSubscriber()
|
||||
: local.getSubscriber();
|
||||
final var storyViewReceiptsState = remote.getStoryViewReceiptsState() == OptionalBool.UNSET
|
||||
? local.getStoryViewReceiptsState()
|
||||
: remote.getStoryViewReceiptsState();
|
||||
final var unknownFields = remote.serializeUnknownFields();
|
||||
final var avatarUrlPath = OptionalUtil.or(remote.getAvatarUrlPath(), local.getAvatarUrlPath()).orElse("");
|
||||
final var profileKey = OptionalUtil.or(remote.getProfileKey(), local.getProfileKey()).orElse(null);
|
||||
final var noteToSelfArchived = remote.isNoteToSelfArchived();
|
||||
final var noteToSelfForcedUnread = remote.isNoteToSelfForcedUnread();
|
||||
final var readReceipts = remote.isReadReceiptsEnabled();
|
||||
final var typingIndicators = remote.isTypingIndicatorsEnabled();
|
||||
final var sealedSenderIndicators = remote.isSealedSenderIndicatorsEnabled();
|
||||
final var linkPreviews = remote.isLinkPreviewsEnabled();
|
||||
final var unlisted = remote.isPhoneNumberUnlisted();
|
||||
final var pinnedConversations = remote.getPinnedConversations();
|
||||
final var phoneNumberSharingMode = remote.getPhoneNumberSharingMode();
|
||||
final var preferContactAvatars = remote.isPreferContactAvatars();
|
||||
final var universalExpireTimer = remote.getUniversalExpireTimer();
|
||||
final var e164 = account.isPrimaryDevice() ? local.getE164() : remote.getE164();
|
||||
final var defaultReactions = !remote.getDefaultReactions().isEmpty()
|
||||
? remote.getDefaultReactions()
|
||||
: local.getDefaultReactions();
|
||||
final var displayBadgesOnProfile = remote.isDisplayBadgesOnProfile();
|
||||
final var subscriptionManuallyCancelled = remote.isSubscriptionManuallyCancelled();
|
||||
final var keepMutedChatsArchived = remote.isKeepMutedChatsArchived();
|
||||
final var hasSetMyStoriesPrivacy = remote.hasSetMyStoriesPrivacy();
|
||||
final var hasViewedOnboardingStory = remote.hasViewedOnboardingStory() || local.hasViewedOnboardingStory();
|
||||
final var storiesDisabled = remote.isStoriesDisabled();
|
||||
final var hasSeenGroupStoryEducation = remote.hasSeenGroupStoryEducationSheet()
|
||||
|| local.hasSeenGroupStoryEducationSheet();
|
||||
boolean hasSeenUsernameOnboarding = remote.hasCompletedUsernameOnboarding()
|
||||
|| local.hasCompletedUsernameOnboarding();
|
||||
final var username = remote.getUsername();
|
||||
final var usernameLink = remote.getUsernameLink();
|
||||
|
||||
final var mergedBuilder = new SignalAccountRecord.Builder(remote.getId().getRaw(), unknownFields).setGivenName(
|
||||
givenName)
|
||||
.setFamilyName(familyName)
|
||||
.setAvatarUrlPath(avatarUrlPath)
|
||||
.setProfileKey(profileKey)
|
||||
.setNoteToSelfArchived(noteToSelfArchived)
|
||||
.setNoteToSelfForcedUnread(noteToSelfForcedUnread)
|
||||
.setReadReceiptsEnabled(readReceipts)
|
||||
.setTypingIndicatorsEnabled(typingIndicators)
|
||||
.setSealedSenderIndicatorsEnabled(sealedSenderIndicators)
|
||||
.setLinkPreviewsEnabled(linkPreviews)
|
||||
.setUnlistedPhoneNumber(unlisted)
|
||||
.setPhoneNumberSharingMode(phoneNumberSharingMode)
|
||||
.setPinnedConversations(pinnedConversations)
|
||||
.setPreferContactAvatars(preferContactAvatars)
|
||||
.setPayments(payments.isEnabled(), payments.getEntropy().orElse(null))
|
||||
.setUniversalExpireTimer(universalExpireTimer)
|
||||
.setDefaultReactions(defaultReactions)
|
||||
.setSubscriber(subscriber)
|
||||
.setDisplayBadgesOnProfile(displayBadgesOnProfile)
|
||||
.setSubscriptionManuallyCancelled(subscriptionManuallyCancelled)
|
||||
.setKeepMutedChatsArchived(keepMutedChatsArchived)
|
||||
.setHasSetMyStoriesPrivacy(hasSetMyStoriesPrivacy)
|
||||
.setHasViewedOnboardingStory(hasViewedOnboardingStory)
|
||||
.setStoriesDisabled(storiesDisabled)
|
||||
.setHasSeenGroupStoryEducationSheet(hasSeenGroupStoryEducation)
|
||||
.setHasCompletedUsernameOnboarding(hasSeenUsernameOnboarding)
|
||||
.setStoryViewReceiptsState(storyViewReceiptsState)
|
||||
.setUsername(username)
|
||||
.setUsernameLink(usernameLink)
|
||||
.setE164(e164);
|
||||
final var mergedBuilder = SignalAccountRecord.Companion.newBuilder(remote.unknownFields().toByteArray())
|
||||
.givenName(givenName)
|
||||
.familyName(familyName)
|
||||
.avatarUrlPath(firstNonEmpty(remote.avatarUrlPath, local.avatarUrlPath))
|
||||
.profileKey(firstNonEmpty(remote.profileKey, local.profileKey))
|
||||
.noteToSelfArchived(remote.noteToSelfArchived)
|
||||
.noteToSelfMarkedUnread(remote.noteToSelfMarkedUnread)
|
||||
.readReceipts(remote.readReceipts)
|
||||
.typingIndicators(remote.typingIndicators)
|
||||
.sealedSenderIndicators(remote.sealedSenderIndicators)
|
||||
.linkPreviews(remote.linkPreviews)
|
||||
.unlistedPhoneNumber(remote.unlistedPhoneNumber)
|
||||
.phoneNumberSharingMode(remote.phoneNumberSharingMode)
|
||||
.pinnedConversations(remote.pinnedConversations)
|
||||
.preferContactAvatars(remote.preferContactAvatars)
|
||||
.universalExpireTimer(remote.universalExpireTimer)
|
||||
.preferredReactionEmoji(firstNonEmpty(remote.preferredReactionEmoji, local.preferredReactionEmoji))
|
||||
.subscriberId(firstNonEmpty(remote.subscriberId, local.subscriberId))
|
||||
.subscriberCurrencyCode(firstNonEmpty(remote.subscriberCurrencyCode, local.subscriberCurrencyCode))
|
||||
.backupsSubscriberId(firstNonEmpty(remote.backupsSubscriberId, local.backupsSubscriberId))
|
||||
.backupsSubscriberCurrencyCode(firstNonEmpty(remote.backupsSubscriberCurrencyCode,
|
||||
local.backupsSubscriberCurrencyCode))
|
||||
.displayBadgesOnProfile(remote.displayBadgesOnProfile)
|
||||
.subscriptionManuallyCancelled(remote.subscriptionManuallyCancelled)
|
||||
.keepMutedChatsArchived(remote.keepMutedChatsArchived)
|
||||
.hasSetMyStoriesPrivacy(remote.hasSetMyStoriesPrivacy)
|
||||
.hasViewedOnboardingStory(remote.hasViewedOnboardingStory || local.hasViewedOnboardingStory)
|
||||
.storiesDisabled(remote.storiesDisabled)
|
||||
.hasSeenGroupStoryEducationSheet(remote.hasSeenGroupStoryEducationSheet
|
||||
|| local.hasSeenGroupStoryEducationSheet)
|
||||
.hasCompletedUsernameOnboarding(remote.hasCompletedUsernameOnboarding
|
||||
|| local.hasCompletedUsernameOnboarding)
|
||||
.storyViewReceiptsEnabled(remote.storyViewReceiptsEnabled == OptionalBool.UNSET
|
||||
? local.storyViewReceiptsEnabled
|
||||
: remote.storyViewReceiptsEnabled)
|
||||
.username(remote.username)
|
||||
.usernameLink(remote.usernameLink)
|
||||
.e164(account.isPrimaryDevice() ? local.e164 : remote.e164);
|
||||
if (firstNonNull(remote.payments, local.payments) != null) {
|
||||
mergedBuilder.payments(firstNonNull(remote.payments, local.payments));
|
||||
}
|
||||
final var merged = mergedBuilder.build();
|
||||
|
||||
final var matchesRemote = doProtosMatch(merged, remote);
|
||||
if (matchesRemote) {
|
||||
return remote;
|
||||
return remoteRecord;
|
||||
}
|
||||
|
||||
final var matchesLocal = doProtosMatch(merged, local);
|
||||
if (matchesLocal) {
|
||||
return local;
|
||||
return localRecord;
|
||||
}
|
||||
|
||||
return mergedBuilder.setId(KeyUtils.createRawStorageId()).build();
|
||||
return new SignalAccountRecord(StorageId.forAccount(KeyUtils.createRawStorageId()), mergedBuilder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -164,56 +142,55 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||
@Override
|
||||
protected void updateLocal(StorageRecordUpdate<SignalAccountRecord> update) throws SQLException {
|
||||
final var accountRecord = update.newRecord();
|
||||
final var accountProto = accountRecord.getProto();
|
||||
|
||||
if (!accountRecord.getE164().equals(account.getNumber())) {
|
||||
if (!accountProto.e164.equals(account.getNumber())) {
|
||||
jobExecutor.enqueueJob(new CheckWhoAmIJob());
|
||||
}
|
||||
|
||||
account.getConfigurationStore().setReadReceipts(connection, accountRecord.isReadReceiptsEnabled());
|
||||
account.getConfigurationStore().setTypingIndicators(connection, accountRecord.isTypingIndicatorsEnabled());
|
||||
account.getConfigurationStore().setReadReceipts(connection, accountProto.readReceipts);
|
||||
account.getConfigurationStore().setTypingIndicators(connection, accountProto.typingIndicators);
|
||||
account.getConfigurationStore()
|
||||
.setUnidentifiedDeliveryIndicators(connection, accountRecord.isSealedSenderIndicatorsEnabled());
|
||||
account.getConfigurationStore().setLinkPreviews(connection, accountRecord.isLinkPreviewsEnabled());
|
||||
.setUnidentifiedDeliveryIndicators(connection, accountProto.sealedSenderIndicators);
|
||||
account.getConfigurationStore().setLinkPreviews(connection, accountProto.linkPreviews);
|
||||
account.getConfigurationStore()
|
||||
.setPhoneNumberSharingMode(connection,
|
||||
StorageSyncModels.remoteToLocal(accountRecord.getPhoneNumberSharingMode()));
|
||||
account.getConfigurationStore().setPhoneNumberUnlisted(connection, accountRecord.isPhoneNumberUnlisted());
|
||||
StorageSyncModels.remoteToLocal(accountProto.phoneNumberSharingMode));
|
||||
account.getConfigurationStore().setPhoneNumberUnlisted(connection, accountProto.unlistedPhoneNumber);
|
||||
|
||||
account.setUsername(accountRecord.getUsername() != null && !accountRecord.getUsername().isEmpty()
|
||||
? accountRecord.getUsername()
|
||||
: null);
|
||||
if (accountRecord.getUsernameLink() != null) {
|
||||
final var usernameLink = accountRecord.getUsernameLink();
|
||||
account.setUsername(!accountProto.username.isEmpty() ? accountProto.username : null);
|
||||
if (accountProto.usernameLink != null) {
|
||||
final var usernameLink = accountProto.usernameLink;
|
||||
account.setUsernameLink(new UsernameLinkComponents(usernameLink.entropy.toByteArray(),
|
||||
UuidUtil.parseOrThrow(usernameLink.serverId.toByteArray())));
|
||||
account.getConfigurationStore().setUsernameLinkColor(connection, usernameLink.color.name());
|
||||
}
|
||||
|
||||
if (accountRecord.getProfileKey().isPresent()) {
|
||||
if (accountProto.profileKey.size() > 0) {
|
||||
ProfileKey profileKey;
|
||||
try {
|
||||
profileKey = new ProfileKey(accountRecord.getProfileKey().get());
|
||||
profileKey = new ProfileKey(accountProto.profileKey.toByteArray());
|
||||
} catch (InvalidInputException e) {
|
||||
logger.debug("Received invalid profile key from storage");
|
||||
profileKey = null;
|
||||
}
|
||||
if (profileKey != null) {
|
||||
account.setProfileKey(profileKey);
|
||||
final var avatarPath = accountRecord.getAvatarUrlPath().orElse(null);
|
||||
final var avatarPath = accountProto.avatarUrlPath.isEmpty() ? null : accountProto.avatarUrlPath;
|
||||
jobExecutor.enqueueJob(new DownloadProfileAvatarJob(avatarPath));
|
||||
}
|
||||
}
|
||||
|
||||
final var profile = account.getRecipientStore().getProfile(connection, account.getSelfRecipientId());
|
||||
final var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile);
|
||||
builder.withGivenName(accountRecord.getGivenName().orElse(null));
|
||||
builder.withFamilyName(accountRecord.getFamilyName().orElse(null));
|
||||
builder.withGivenName(accountProto.givenName);
|
||||
builder.withFamilyName(accountProto.familyName);
|
||||
account.getRecipientStore().storeProfile(connection, account.getSelfRecipientId(), builder.build());
|
||||
account.getRecipientStore()
|
||||
.storeStorageRecord(connection,
|
||||
account.getSelfRecipientId(),
|
||||
accountRecord.getId(),
|
||||
accountRecord.toProto().encode());
|
||||
accountProto.encode());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -221,7 +198,7 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||
return 0;
|
||||
}
|
||||
|
||||
private static boolean doProtosMatch(SignalAccountRecord merged, SignalAccountRecord other) {
|
||||
return Arrays.equals(merged.toProto().encode(), other.toProto().encode());
|
||||
private static boolean doProtosMatch(AccountRecord merged, AccountRecord other) {
|
||||
return Arrays.equals(merged.encode(), other.encode());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,8 @@ import org.slf4j.LoggerFactory;
|
|||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||
import org.whispersystems.signalservice.api.util.OptionalUtil;
|
||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
|
||||
|
||||
import java.sql.Connection;
|
||||
|
@ -27,6 +28,11 @@ import java.util.Objects;
|
|||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
import static org.asamk.signal.manager.util.Utils.firstNonEmpty;
|
||||
import static org.asamk.signal.manager.util.Utils.nullIfEmpty;
|
||||
|
||||
public class ContactRecordProcessor extends DefaultStorageRecordProcessor<SignalContactRecord> {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ContactRecordProcessor.class);
|
||||
|
@ -55,20 +61,24 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||
* - You can't have a contact record for yourself. That should be an account record.
|
||||
*/
|
||||
@Override
|
||||
protected boolean isInvalid(SignalContactRecord remote) {
|
||||
boolean hasAci = remote.getAci().isPresent() && remote.getAci().get().isValid();
|
||||
boolean hasPni = remote.getPni().isPresent() && remote.getPni().get().isValid();
|
||||
protected boolean isInvalid(SignalContactRecord remoteRecord) {
|
||||
final var remote = remoteRecord.getProto();
|
||||
final var aci = ACI.parseOrNull(remote.aci);
|
||||
final var pni = PNI.parseOrNull(remote.pni);
|
||||
final var e164 = nullIfEmpty(remote.e164);
|
||||
boolean hasAci = aci != null && aci.isValid();
|
||||
boolean hasPni = pni != null && pni.isValid();
|
||||
|
||||
if (!hasAci && !hasPni) {
|
||||
logger.debug("Found a ContactRecord with neither an ACI nor a PNI -- marking as invalid.");
|
||||
return true;
|
||||
} else if (selfAci != null && selfAci.equals(remote.getAci().orElse(null)) || (
|
||||
selfPni != null && selfPni.equals(remote.getPni().orElse(null))
|
||||
) || (selfNumber != null && selfNumber.equals(remote.getNumber().orElse(null)))) {
|
||||
} else if (selfAci != null && selfAci.equals(aci) || (
|
||||
selfPni != null && selfPni.equals(pni)
|
||||
) || (selfNumber != null && selfNumber.equals(e164))) {
|
||||
logger.debug("Found a ContactRecord for ourselves -- marking as invalid.");
|
||||
return true;
|
||||
} else if (remote.getNumber().isPresent() && !isValidE164(remote.getNumber().get())) {
|
||||
logger.debug("Found a record with an invalid E164. Marking as invalid.");
|
||||
} else if (e164 != null && !isValidE164(e164)) {
|
||||
logger.debug("Found a record with an invalid E164 ({}). Marking as invalid.", e164);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
@ -77,7 +87,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||
|
||||
@Override
|
||||
protected Optional<SignalContactRecord> getMatching(SignalContactRecord remote) throws SQLException {
|
||||
final var address = getRecipientAddress(remote);
|
||||
final var address = getRecipientAddress(remote.getProto());
|
||||
final var recipientId = account.getRecipientStore().resolveRecipient(connection, address);
|
||||
final var recipient = account.getRecipientStore().getRecipient(connection, recipientId);
|
||||
|
||||
|
@ -85,141 +95,120 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||
final var identity = account.getIdentityKeyStore().getIdentityInfo(connection, identifier);
|
||||
final var storageId = account.getRecipientStore().getStorageId(connection, recipientId);
|
||||
|
||||
return Optional.of(StorageSyncModels.localToRemoteRecord(recipient, identity, storageId.getRaw())
|
||||
.getContact()
|
||||
.get());
|
||||
return Optional.of(new SignalContactRecord(storageId,
|
||||
StorageSyncModels.localToRemoteRecord(recipient, identity)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SignalContactRecord merge(SignalContactRecord remote, SignalContactRecord local) {
|
||||
protected SignalContactRecord merge(SignalContactRecord remoteRecord, SignalContactRecord localRecord) {
|
||||
final var remote = remoteRecord.getProto();
|
||||
final var local = localRecord.getProto();
|
||||
|
||||
String profileGivenName;
|
||||
String profileFamilyName;
|
||||
if (remote.getProfileGivenName().isPresent() || remote.getProfileFamilyName().isPresent()) {
|
||||
profileGivenName = remote.getProfileGivenName().orElse("");
|
||||
profileFamilyName = remote.getProfileFamilyName().orElse("");
|
||||
if (!remote.givenName.isEmpty() || !remote.familyName.isEmpty()) {
|
||||
profileGivenName = remote.givenName;
|
||||
profileFamilyName = remote.familyName;
|
||||
} else {
|
||||
profileGivenName = local.getProfileGivenName().orElse("");
|
||||
profileFamilyName = local.getProfileFamilyName().orElse("");
|
||||
profileGivenName = local.givenName;
|
||||
profileFamilyName = local.familyName;
|
||||
}
|
||||
|
||||
IdentityState identityState;
|
||||
byte[] identityKey;
|
||||
if (remote.getIdentityKey().isPresent() && (
|
||||
remote.getIdentityState() != local.getIdentityState()
|
||||
|| local.getIdentityKey().isEmpty()
|
||||
|| !account.isPrimaryDevice()
|
||||
ByteString identityKey;
|
||||
if (remote.identityKey.size() > 0 && (
|
||||
!account.isPrimaryDevice()
|
||||
|| remote.identityState != local.identityState
|
||||
|| local.identityKey.size() == 0
|
||||
|
||||
)) {
|
||||
identityState = remote.getIdentityState();
|
||||
identityKey = remote.getIdentityKey().get();
|
||||
identityState = remote.identityState;
|
||||
identityKey = remote.identityKey;
|
||||
} else {
|
||||
identityState = local.getIdentityState();
|
||||
identityKey = local.getIdentityKey().orElse(null);
|
||||
identityState = local.identityState;
|
||||
identityKey = local.identityKey.size() > 0 ? local.identityKey : ByteString.EMPTY;
|
||||
}
|
||||
|
||||
if (local.getAci().isPresent()
|
||||
&& local.getIdentityKey().isPresent()
|
||||
&& remote.getIdentityKey().isPresent()
|
||||
&& !Arrays.equals(local.getIdentityKey().get(), remote.getIdentityKey().get())) {
|
||||
if (!local.aci.isEmpty()
|
||||
&& local.identityKey.size() > 0
|
||||
&& remote.identityKey.size() > 0
|
||||
&& !local.identityKey.equals(remote.identityKey)) {
|
||||
logger.debug("The local and remote identity keys do not match for {}. Enqueueing a profile fetch.",
|
||||
local.getAci().orElse(null));
|
||||
local.aci);
|
||||
final var address = getRecipientAddress(local);
|
||||
jobExecutor.enqueueJob(new DownloadProfileJob(address));
|
||||
}
|
||||
|
||||
final var e164sMatchButPnisDont = local.getNumber().isPresent()
|
||||
&& local.getNumber()
|
||||
.get()
|
||||
.equals(remote.getNumber().orElse(null))
|
||||
&& local.getPni().isPresent()
|
||||
&& remote.getPni().isPresent()
|
||||
&& !local.getPni().get().equals(remote.getPni().get());
|
||||
|
||||
final var pnisMatchButE164sDont = local.getPni().isPresent()
|
||||
&& local.getPni()
|
||||
.get()
|
||||
.equals(remote.getPni().orElse(null))
|
||||
&& local.getNumber().isPresent()
|
||||
&& remote.getNumber().isPresent()
|
||||
&& !local.getNumber().get().equals(remote.getNumber().get());
|
||||
|
||||
PNI pni;
|
||||
String pni;
|
||||
String e164;
|
||||
if (!account.isPrimaryDevice() && (e164sMatchButPnisDont || pnisMatchButE164sDont)) {
|
||||
if (e164sMatchButPnisDont) {
|
||||
logger.debug("Matching E164s, but the PNIs differ! Trusting our local pair.");
|
||||
} else if (pnisMatchButE164sDont) {
|
||||
logger.debug("Matching PNIs, but the E164s differ! Trusting our local pair.");
|
||||
if (account.isPrimaryDevice()) {
|
||||
final var e164sMatchButPnisDont = !local.e164.isEmpty()
|
||||
&& local.e164.equals(remote.e164)
|
||||
&& !local.pni.isEmpty()
|
||||
&& !remote.pni.isEmpty()
|
||||
&& !local.pni.equals(remote.pni);
|
||||
|
||||
final var pnisMatchButE164sDont = !local.pni.isEmpty()
|
||||
&& local.pni.equals(remote.pni)
|
||||
&& !local.e164.isEmpty()
|
||||
&& !remote.e164.isEmpty()
|
||||
&& !local.e164.equals(remote.e164);
|
||||
|
||||
if (e164sMatchButPnisDont || pnisMatchButE164sDont) {
|
||||
if (e164sMatchButPnisDont) {
|
||||
logger.debug("Matching E164s, but the PNIs differ! Trusting our local pair.");
|
||||
} else if (pnisMatchButE164sDont) {
|
||||
logger.debug("Matching PNIs, but the E164s differ! Trusting our local pair.");
|
||||
}
|
||||
jobExecutor.enqueueJob(new RefreshRecipientsJob());
|
||||
pni = local.pni;
|
||||
e164 = local.e164;
|
||||
} else {
|
||||
pni = firstNonEmpty(remote.pni, local.pni);
|
||||
e164 = firstNonEmpty(remote.e164, local.e164);
|
||||
}
|
||||
jobExecutor.enqueueJob(new RefreshRecipientsJob());
|
||||
pni = local.getPni().get();
|
||||
e164 = local.getNumber().get();
|
||||
} else {
|
||||
pni = OptionalUtil.or(remote.getPni(), local.getPni()).orElse(null);
|
||||
e164 = OptionalUtil.or(remote.getNumber(), local.getNumber()).orElse(null);
|
||||
pni = firstNonEmpty(remote.pni, local.pni);
|
||||
e164 = firstNonEmpty(remote.e164, local.e164);
|
||||
}
|
||||
|
||||
final var unknownFields = remote.serializeUnknownFields();
|
||||
final var aci = local.getAci().isEmpty() ? remote.getAci().orElse(null) : local.getAci().get();
|
||||
final var profileKey = OptionalUtil.or(remote.getProfileKey(), local.getProfileKey()).orElse(null);
|
||||
final var username = OptionalUtil.or(remote.getUsername(), local.getUsername()).orElse("");
|
||||
final var blocked = remote.isBlocked();
|
||||
final var profileSharing = remote.isProfileSharingEnabled();
|
||||
final var archived = remote.isArchived();
|
||||
final var forcedUnread = remote.isForcedUnread();
|
||||
final var muteUntil = remote.getMuteUntil();
|
||||
final var hideStory = remote.shouldHideStory();
|
||||
final var unregisteredTimestamp = remote.getUnregisteredTimestamp();
|
||||
final var hidden = remote.isHidden();
|
||||
final var systemGivenName = account.isPrimaryDevice()
|
||||
? local.getSystemGivenName().orElse("")
|
||||
: remote.getSystemGivenName().orElse("");
|
||||
final var systemFamilyName = account.isPrimaryDevice()
|
||||
? local.getSystemFamilyName().orElse("")
|
||||
: remote.getSystemFamilyName().orElse("");
|
||||
final var systemNickname = remote.getSystemNickname().orElse("");
|
||||
final var nicknameGivenName = remote.getNicknameGivenName().orElse("");
|
||||
final var nicknameFamilyName = remote.getNicknameFamilyName().orElse("");
|
||||
final var pniSignatureVerified = remote.isPniSignatureVerified() || local.isPniSignatureVerified();
|
||||
final var note = remote.getNote().or(local::getNote).orElse("");
|
||||
|
||||
final var mergedBuilder = new SignalContactRecord.Builder(remote.getId().getRaw(), aci, unknownFields).setE164(
|
||||
e164)
|
||||
.setPni(pni)
|
||||
.setProfileGivenName(profileGivenName)
|
||||
.setProfileFamilyName(profileFamilyName)
|
||||
.setSystemGivenName(systemGivenName)
|
||||
.setSystemFamilyName(systemFamilyName)
|
||||
.setSystemNickname(systemNickname)
|
||||
.setProfileKey(profileKey)
|
||||
.setUsername(username)
|
||||
.setIdentityState(identityState)
|
||||
.setIdentityKey(identityKey)
|
||||
.setBlocked(blocked)
|
||||
.setProfileSharingEnabled(profileSharing)
|
||||
.setArchived(archived)
|
||||
.setForcedUnread(forcedUnread)
|
||||
.setMuteUntil(muteUntil)
|
||||
.setHideStory(hideStory)
|
||||
.setUnregisteredTimestamp(unregisteredTimestamp)
|
||||
.setHidden(hidden)
|
||||
.setPniSignatureVerified(pniSignatureVerified)
|
||||
.setNicknameGivenName(nicknameGivenName)
|
||||
.setNicknameFamilyName(nicknameFamilyName)
|
||||
.setNote(note);
|
||||
final var mergedBuilder = SignalContactRecord.Companion.newBuilder(remote.unknownFields().toByteArray())
|
||||
.aci(local.aci.isEmpty() ? remote.aci : local.aci)
|
||||
.e164(e164)
|
||||
.pni(pni)
|
||||
.givenName(profileGivenName)
|
||||
.familyName(profileFamilyName)
|
||||
.systemGivenName(account.isPrimaryDevice() ? local.systemGivenName : remote.systemGivenName)
|
||||
.systemFamilyName(account.isPrimaryDevice() ? local.systemFamilyName : remote.systemFamilyName)
|
||||
.systemNickname(remote.systemNickname)
|
||||
.profileKey(firstNonEmpty(remote.profileKey, local.profileKey))
|
||||
.username(firstNonEmpty(remote.username, local.username))
|
||||
.identityState(identityState)
|
||||
.identityKey(identityKey)
|
||||
.blocked(remote.blocked)
|
||||
.whitelisted(remote.whitelisted)
|
||||
.archived(remote.archived)
|
||||
.markedUnread(remote.markedUnread)
|
||||
.mutedUntilTimestamp(remote.mutedUntilTimestamp)
|
||||
.hideStory(remote.hideStory)
|
||||
.unregisteredAtTimestamp(remote.unregisteredAtTimestamp)
|
||||
.hidden(remote.hidden)
|
||||
.pniSignatureVerified(remote.pniSignatureVerified || local.pniSignatureVerified)
|
||||
.nickname(remote.nickname)
|
||||
.note(remote.note);
|
||||
final var merged = mergedBuilder.build();
|
||||
|
||||
final var matchesRemote = doProtosMatch(merged, remote);
|
||||
if (matchesRemote) {
|
||||
return remote;
|
||||
return remoteRecord;
|
||||
}
|
||||
|
||||
final var matchesLocal = doProtosMatch(merged, local);
|
||||
if (matchesLocal) {
|
||||
return local;
|
||||
return localRecord;
|
||||
}
|
||||
|
||||
return mergedBuilder.setId(KeyUtils.createRawStorageId()).build();
|
||||
return new SignalContactRecord(StorageId.forContact(KeyUtils.createRawStorageId()), mergedBuilder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -231,7 +220,8 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||
@Override
|
||||
protected void updateLocal(StorageRecordUpdate<SignalContactRecord> update) throws SQLException {
|
||||
final var contactRecord = update.newRecord();
|
||||
final var address = getRecipientAddress(contactRecord);
|
||||
final var contactProto = contactRecord.getProto();
|
||||
final var address = getRecipientAddress(contactProto);
|
||||
final var recipientId = account.getRecipientStore().resolveRecipientTrusted(connection, address);
|
||||
final var recipient = account.getRecipientStore().getRecipient(connection, recipientId);
|
||||
|
||||
|
@ -251,95 +241,92 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||
final var contactNickGivenName = contact == null ? null : contact.nickNameGivenName();
|
||||
final var contactNickFamilyName = contact == null ? null : contact.nickNameFamilyName();
|
||||
final var contactNote = contact == null ? null : contact.note();
|
||||
if (blocked != contactRecord.isBlocked()
|
||||
|| profileShared != contactRecord.isProfileSharingEnabled()
|
||||
|| archived != contactRecord.isArchived()
|
||||
|| hidden != contactRecord.isHidden()
|
||||
|| hideStory != contactRecord.shouldHideStory()
|
||||
|| muteUntil != contactRecord.getMuteUntil()
|
||||
|| unregisteredTimestamp != contactRecord.getUnregisteredTimestamp()
|
||||
|| !Objects.equals(contactRecord.getSystemGivenName().orElse(null), contactGivenName)
|
||||
|| !Objects.equals(contactRecord.getSystemFamilyName().orElse(null), contactFamilyName)
|
||||
|| !Objects.equals(contactRecord.getSystemNickname().orElse(null), contactNickName)
|
||||
|| !Objects.equals(contactRecord.getNicknameGivenName().orElse(null), contactNickGivenName)
|
||||
|| !Objects.equals(contactRecord.getNicknameFamilyName().orElse(null), contactNickFamilyName)
|
||||
|| !Objects.equals(contactRecord.getNote().orElse(null), contactNote)) {
|
||||
if (blocked != contactProto.blocked
|
||||
|| profileShared != contactProto.whitelisted
|
||||
|| archived != contactProto.archived
|
||||
|| hidden != contactProto.hidden
|
||||
|| hideStory != contactProto.hideStory
|
||||
|| muteUntil != contactProto.mutedUntilTimestamp
|
||||
|| unregisteredTimestamp != contactProto.unregisteredAtTimestamp
|
||||
|| !Objects.equals(nullIfEmpty(contactProto.systemGivenName), contactGivenName)
|
||||
|| !Objects.equals(nullIfEmpty(contactProto.systemFamilyName), contactFamilyName)
|
||||
|| !Objects.equals(nullIfEmpty(contactProto.systemNickname), contactNickName)
|
||||
|| !Objects.equals(nullIfEmpty(contactProto.nickname == null ? "" : contactProto.nickname.given),
|
||||
contactNickGivenName)
|
||||
|| !Objects.equals(nullIfEmpty(contactProto.nickname == null ? "" : contactProto.nickname.family),
|
||||
contactNickFamilyName)
|
||||
|| !Objects.equals(nullIfEmpty(contactProto.note), contactNote)) {
|
||||
logger.debug("Storing new or updated contact {}", recipientId);
|
||||
final var contactBuilder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
|
||||
final var newContact = contactBuilder.withIsBlocked(contactRecord.isBlocked())
|
||||
.withIsProfileSharingEnabled(contactRecord.isProfileSharingEnabled())
|
||||
.withIsArchived(contactRecord.isArchived())
|
||||
.withIsHidden(contactRecord.isHidden())
|
||||
.withMuteUntil(contactRecord.getMuteUntil())
|
||||
.withHideStory(contactRecord.shouldHideStory())
|
||||
.withGivenName(contactRecord.getSystemGivenName().orElse(null))
|
||||
.withFamilyName(contactRecord.getSystemFamilyName().orElse(null))
|
||||
.withNickName(contactRecord.getSystemNickname().orElse(null))
|
||||
.withNickNameGivenName(contactRecord.getNicknameGivenName().orElse(null))
|
||||
.withNickNameFamilyName(contactRecord.getNicknameFamilyName().orElse(null))
|
||||
.withNote(contactRecord.getNote().orElse(null))
|
||||
.withUnregisteredTimestamp(contactRecord.getUnregisteredTimestamp() == 0
|
||||
? null
|
||||
: contactRecord.getUnregisteredTimestamp());
|
||||
final var newContact = contactBuilder.withIsBlocked(contactProto.blocked)
|
||||
.withIsProfileSharingEnabled(contactProto.whitelisted)
|
||||
.withIsArchived(contactProto.archived)
|
||||
.withIsHidden(contactProto.hidden)
|
||||
.withMuteUntil(contactProto.mutedUntilTimestamp)
|
||||
.withHideStory(contactProto.hideStory)
|
||||
.withGivenName(nullIfEmpty(contactProto.systemGivenName))
|
||||
.withFamilyName(nullIfEmpty(contactProto.systemFamilyName))
|
||||
.withNickName(nullIfEmpty(contactProto.systemNickname))
|
||||
.withNickNameGivenName(nullIfEmpty(contactProto.givenName))
|
||||
.withNickNameFamilyName(nullIfEmpty(contactProto.familyName))
|
||||
.withNote(nullIfEmpty(contactProto.note))
|
||||
.withUnregisteredTimestamp(contactProto.unregisteredAtTimestamp);
|
||||
account.getRecipientStore().storeContact(connection, recipientId, newContact.build());
|
||||
}
|
||||
|
||||
final var profile = recipient.getProfile();
|
||||
final var profileGivenName = profile == null ? null : profile.getGivenName();
|
||||
final var profileFamilyName = profile == null ? null : profile.getFamilyName();
|
||||
if (!Objects.equals(contactRecord.getProfileGivenName().orElse(null), profileGivenName) || !Objects.equals(
|
||||
contactRecord.getProfileFamilyName().orElse(null),
|
||||
profileFamilyName)) {
|
||||
if (!Objects.equals(nullIfEmpty(contactProto.givenName), profileGivenName) || !Objects.equals(nullIfEmpty(
|
||||
contactProto.familyName), profileFamilyName)) {
|
||||
final var profileBuilder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile);
|
||||
final var newProfile = profileBuilder.withGivenName(contactRecord.getProfileGivenName().orElse(null))
|
||||
.withFamilyName(contactRecord.getProfileFamilyName().orElse(null))
|
||||
final var newProfile = profileBuilder.withGivenName(nullIfEmpty(contactProto.givenName))
|
||||
.withFamilyName(nullIfEmpty(contactProto.familyName))
|
||||
.build();
|
||||
account.getRecipientStore().storeProfile(connection, recipientId, newProfile);
|
||||
}
|
||||
if (contactRecord.getProfileKey().isPresent()) {
|
||||
if (contactProto.profileKey.size() > 0) {
|
||||
try {
|
||||
logger.trace("Storing profile key {}", recipientId);
|
||||
final var profileKey = new ProfileKey(contactRecord.getProfileKey().get());
|
||||
final var profileKey = new ProfileKey(contactProto.profileKey.toByteArray());
|
||||
account.getRecipientStore().storeProfileKey(connection, recipientId, profileKey);
|
||||
} catch (InvalidInputException e) {
|
||||
logger.warn("Received invalid contact profile key from storage");
|
||||
}
|
||||
}
|
||||
if (contactRecord.getIdentityKey().isPresent() && contactRecord.getAci().isPresent()) {
|
||||
if (contactProto.identityKey.size() > 0 && address.aci().isPresent()) {
|
||||
try {
|
||||
logger.trace("Storing identity key {}", recipientId);
|
||||
final var identityKey = new IdentityKey(contactRecord.getIdentityKey().get());
|
||||
account.getIdentityKeyStore()
|
||||
.saveIdentity(connection, contactRecord.getAci().orElse(null), identityKey);
|
||||
final var identityKey = new IdentityKey(contactProto.identityKey.toByteArray());
|
||||
account.getIdentityKeyStore().saveIdentity(connection, address.aci().get(), identityKey);
|
||||
|
||||
final var trustLevel = StorageSyncModels.remoteToLocal(contactRecord.getIdentityState());
|
||||
final var trustLevel = StorageSyncModels.remoteToLocal(contactProto.identityState);
|
||||
if (trustLevel != null) {
|
||||
account.getIdentityKeyStore()
|
||||
.setIdentityTrustLevel(connection,
|
||||
contactRecord.getAci().orElse(null),
|
||||
identityKey,
|
||||
trustLevel);
|
||||
.setIdentityTrustLevel(connection, address.aci().get(), identityKey, trustLevel);
|
||||
}
|
||||
} catch (InvalidKeyException e) {
|
||||
logger.warn("Received invalid contact identity key from storage");
|
||||
}
|
||||
}
|
||||
account.getRecipientStore()
|
||||
.storeStorageRecord(connection, recipientId, contactRecord.getId(), contactRecord.toProto().encode());
|
||||
.storeStorageRecord(connection, recipientId, contactRecord.getId(), contactProto.encode());
|
||||
}
|
||||
|
||||
private static RecipientAddress getRecipientAddress(final SignalContactRecord contactRecord) {
|
||||
return new RecipientAddress(contactRecord.getAci().orElse(null),
|
||||
contactRecord.getPni().orElse(null),
|
||||
contactRecord.getNumber().orElse(null),
|
||||
contactRecord.getUsername().orElse(null));
|
||||
private static RecipientAddress getRecipientAddress(final ContactRecord contactRecord) {
|
||||
return new RecipientAddress(ACI.parseOrNull(contactRecord.aci),
|
||||
PNI.parseOrNull(contactRecord.pni),
|
||||
nullIfEmpty(contactRecord.e164),
|
||||
nullIfEmpty(contactRecord.username));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(SignalContactRecord lhs, SignalContactRecord rhs) {
|
||||
if ((lhs.getAci().isPresent() && Objects.equals(lhs.getAci(), rhs.getAci())) || (
|
||||
lhs.getNumber().isPresent() && Objects.equals(lhs.getNumber(), rhs.getNumber())
|
||||
) || (lhs.getPni().isPresent() && Objects.equals(lhs.getPni(), rhs.getPni()))) {
|
||||
public int compare(SignalContactRecord lhsRecord, SignalContactRecord rhsRecord) {
|
||||
final var lhs = lhsRecord.getProto();
|
||||
final var rhs = rhsRecord.getProto();
|
||||
if ((!lhs.aci.isEmpty() && Objects.equals(lhs.aci, rhs.aci)) || (
|
||||
!lhs.e164.isEmpty() && Objects.equals(lhs.e164, rhs.e164)
|
||||
) || (!lhs.pni.isEmpty() && Objects.equals(lhs.pni, rhs.pni))) {
|
||||
return 0;
|
||||
} else {
|
||||
return 1;
|
||||
|
@ -350,7 +337,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||
return E164_PATTERN.matcher(value).matches();
|
||||
}
|
||||
|
||||
private static boolean doProtosMatch(SignalContactRecord merged, SignalContactRecord other) {
|
||||
return Arrays.equals(merged.toProto().encode(), other.toProto().encode());
|
||||
private static boolean doProtosMatch(ContactRecord merged, ContactRecord other) {
|
||||
return Arrays.equals(merged.encode(), other.encode());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ import org.asamk.signal.manager.util.KeyUtils;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.GroupV1Record;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
|
@ -36,7 +38,7 @@ public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor<
|
|||
@Override
|
||||
protected boolean isInvalid(SignalGroupV1Record remote) throws SQLException {
|
||||
try {
|
||||
final var id = GroupId.unknownVersion(remote.getGroupId());
|
||||
final var id = GroupId.unknownVersion(remote.getProto().id.toByteArray());
|
||||
if (!(id instanceof GroupIdV1)) {
|
||||
return true;
|
||||
}
|
||||
|
@ -56,7 +58,7 @@ public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor<
|
|||
|
||||
@Override
|
||||
protected Optional<SignalGroupV1Record> getMatching(SignalGroupV1Record remote) throws SQLException {
|
||||
final var id = GroupId.v1(remote.getGroupId());
|
||||
final var id = GroupId.v1(remote.getProto().id.toByteArray());
|
||||
final var group = account.getGroupStore().getGroup(connection, id);
|
||||
|
||||
if (group == null) {
|
||||
|
@ -64,39 +66,35 @@ public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor<
|
|||
}
|
||||
|
||||
final var storageId = account.getGroupStore().getGroupStorageId(connection, id);
|
||||
return Optional.of(StorageSyncModels.localToRemoteRecord(group, storageId.getRaw()).getGroupV1().get());
|
||||
return Optional.of(new SignalGroupV1Record(storageId, StorageSyncModels.localToRemoteRecord(group)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SignalGroupV1Record merge(SignalGroupV1Record remote, SignalGroupV1Record local) {
|
||||
final var unknownFields = remote.serializeUnknownFields();
|
||||
final var blocked = remote.isBlocked();
|
||||
final var profileSharing = remote.isProfileSharingEnabled();
|
||||
final var archived = remote.isArchived();
|
||||
final var forcedUnread = remote.isForcedUnread();
|
||||
final var muteUntil = remote.getMuteUntil();
|
||||
protected SignalGroupV1Record merge(SignalGroupV1Record remoteRecord, SignalGroupV1Record localRecord) {
|
||||
final var remote = remoteRecord.getProto();
|
||||
final var local = localRecord.getProto();
|
||||
|
||||
final var mergedBuilder = new SignalGroupV1Record.Builder(remote.getId().getRaw(),
|
||||
remote.getGroupId(),
|
||||
unknownFields).setBlocked(blocked)
|
||||
.setProfileSharingEnabled(profileSharing)
|
||||
.setForcedUnread(forcedUnread)
|
||||
.setMuteUntil(muteUntil)
|
||||
.setArchived(archived);
|
||||
final var mergedBuilder = SignalGroupV1Record.Companion.newBuilder(remote.unknownFields().toByteArray())
|
||||
.id(remote.id)
|
||||
.blocked(remote.blocked)
|
||||
.whitelisted(remote.whitelisted)
|
||||
.markedUnread(remote.markedUnread)
|
||||
.mutedUntilTimestamp(remote.mutedUntilTimestamp)
|
||||
.archived(remote.archived);
|
||||
|
||||
final var merged = mergedBuilder.build();
|
||||
|
||||
final var matchesRemote = doProtosMatch(merged, remote);
|
||||
if (matchesRemote) {
|
||||
return remote;
|
||||
return remoteRecord;
|
||||
}
|
||||
|
||||
final var matchesLocal = doProtosMatch(merged, local);
|
||||
if (matchesLocal) {
|
||||
return local;
|
||||
return localRecord;
|
||||
}
|
||||
|
||||
return mergedBuilder.setId(KeyUtils.createRawStorageId()).build();
|
||||
return new SignalGroupV1Record(StorageId.forGroupV1(KeyUtils.createRawStorageId()), mergedBuilder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -110,30 +108,28 @@ public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor<
|
|||
@Override
|
||||
protected void updateLocal(StorageRecordUpdate<SignalGroupV1Record> update) throws SQLException {
|
||||
final var groupV1Record = update.newRecord();
|
||||
final var groupIdV1 = GroupId.v1(groupV1Record.getGroupId());
|
||||
final var groupV1Proto = groupV1Record.getProto();
|
||||
final var groupIdV1 = GroupId.v1(groupV1Proto.id.toByteArray());
|
||||
|
||||
final var group = account.getGroupStore().getOrCreateGroupV1(connection, groupIdV1);
|
||||
if (group != null) {
|
||||
group.setBlocked(groupV1Record.isBlocked());
|
||||
group.setBlocked(groupV1Proto.blocked);
|
||||
account.getGroupStore().updateGroup(connection, group);
|
||||
account.getGroupStore()
|
||||
.storeStorageRecord(connection,
|
||||
group.getGroupId(),
|
||||
groupV1Record.getId(),
|
||||
groupV1Record.toProto().encode());
|
||||
.storeStorageRecord(connection, group.getGroupId(), groupV1Record.getId(), groupV1Proto.encode());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(SignalGroupV1Record lhs, SignalGroupV1Record rhs) {
|
||||
if (Arrays.equals(lhs.getGroupId(), rhs.getGroupId())) {
|
||||
if (lhs.getProto().id.equals(rhs.getProto().id)) {
|
||||
return 0;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean doProtosMatch(SignalGroupV1Record merged, SignalGroupV1Record other) {
|
||||
return Arrays.equals(merged.toProto().encode(), other.toProto().encode());
|
||||
private static boolean doProtosMatch(GroupV1Record merged, GroupV1Record other) {
|
||||
return Arrays.equals(merged.encode(), other.encode());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,16 +3,22 @@ package org.asamk.signal.manager.syncStorage;
|
|||
import org.asamk.signal.manager.groups.GroupUtils;
|
||||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.util.KeyUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<SignalGroupV2Record> {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(GroupV2RecordProcessor.class);
|
||||
|
@ -26,12 +32,12 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
|
|||
|
||||
@Override
|
||||
protected boolean isInvalid(SignalGroupV2Record remote) {
|
||||
return remote.getMasterKeyBytes().length != GroupMasterKey.SIZE;
|
||||
return remote.getProto().masterKey.size() != GroupMasterKey.SIZE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<SignalGroupV2Record> getMatching(SignalGroupV2Record remote) throws SQLException {
|
||||
final var id = GroupUtils.getGroupIdV2(remote.getMasterKeyOrThrow());
|
||||
final var id = GroupUtils.getGroupIdV2(getGroupMasterKeyOrThrow(remote.getProto().masterKey));
|
||||
final var group = account.getGroupStore().getGroup(connection, id);
|
||||
|
||||
if (group == null) {
|
||||
|
@ -39,44 +45,37 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
|
|||
}
|
||||
|
||||
final var storageId = account.getGroupStore().getGroupStorageId(connection, id);
|
||||
return Optional.of(StorageSyncModels.localToRemoteRecord(group, storageId.getRaw()).getGroupV2().get());
|
||||
return Optional.of(new SignalGroupV2Record(storageId, StorageSyncModels.localToRemoteRecord(group)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SignalGroupV2Record merge(SignalGroupV2Record remote, SignalGroupV2Record local) {
|
||||
final var unknownFields = remote.serializeUnknownFields();
|
||||
final var blocked = remote.isBlocked();
|
||||
final var profileSharing = remote.isProfileSharingEnabled();
|
||||
final var archived = remote.isArchived();
|
||||
final var forcedUnread = remote.isForcedUnread();
|
||||
final var muteUntil = remote.getMuteUntil();
|
||||
final var notifyForMentionsWhenMuted = remote.notifyForMentionsWhenMuted();
|
||||
final var hideStory = remote.shouldHideStory();
|
||||
final var storySendMode = remote.getStorySendMode();
|
||||
protected SignalGroupV2Record merge(SignalGroupV2Record remoteRecord, SignalGroupV2Record localRecord) {
|
||||
final var remote = remoteRecord.getProto();
|
||||
final var local = localRecord.getProto();
|
||||
|
||||
final var mergedBuilder = new SignalGroupV2Record.Builder(remote.getId().getRaw(),
|
||||
remote.getMasterKeyBytes(),
|
||||
unknownFields).setBlocked(blocked)
|
||||
.setProfileSharingEnabled(profileSharing)
|
||||
.setArchived(archived)
|
||||
.setForcedUnread(forcedUnread)
|
||||
.setMuteUntil(muteUntil)
|
||||
.setNotifyForMentionsWhenMuted(notifyForMentionsWhenMuted)
|
||||
.setHideStory(hideStory)
|
||||
.setStorySendMode(storySendMode);
|
||||
final var mergedBuilder = SignalGroupV2Record.Companion.newBuilder(remote.unknownFields().toByteArray())
|
||||
.masterKey(remote.masterKey)
|
||||
.blocked(remote.blocked)
|
||||
.whitelisted(remote.whitelisted)
|
||||
.archived(remote.archived)
|
||||
.markedUnread(remote.markedUnread)
|
||||
.mutedUntilTimestamp(remote.mutedUntilTimestamp)
|
||||
.dontNotifyForMentionsIfMuted(remote.dontNotifyForMentionsIfMuted)
|
||||
.hideStory(remote.hideStory)
|
||||
.storySendMode(remote.storySendMode);
|
||||
final var merged = mergedBuilder.build();
|
||||
|
||||
final var matchesRemote = doProtosMatch(merged, remote);
|
||||
if (matchesRemote) {
|
||||
return remote;
|
||||
return remoteRecord;
|
||||
}
|
||||
|
||||
final var matchesLocal = doProtosMatch(merged, local);
|
||||
if (matchesLocal) {
|
||||
return local;
|
||||
return localRecord;
|
||||
}
|
||||
|
||||
return mergedBuilder.setId(KeyUtils.createRawStorageId()).build();
|
||||
return new SignalGroupV2Record(StorageId.forGroupV2(KeyUtils.createRawStorageId()), mergedBuilder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -88,29 +87,36 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
|
|||
@Override
|
||||
protected void updateLocal(StorageRecordUpdate<SignalGroupV2Record> update) throws SQLException {
|
||||
final var groupV2Record = update.newRecord();
|
||||
final var groupMasterKey = groupV2Record.getMasterKeyOrThrow();
|
||||
final var groupV2Proto = groupV2Record.getProto();
|
||||
final var groupMasterKey = getGroupMasterKeyOrThrow(groupV2Proto.masterKey);
|
||||
|
||||
final var group = account.getGroupStore().getGroupOrPartialMigrate(connection, groupMasterKey);
|
||||
group.setBlocked(groupV2Record.isBlocked());
|
||||
group.setProfileSharingEnabled(groupV2Record.isProfileSharingEnabled());
|
||||
group.setBlocked(groupV2Proto.blocked);
|
||||
group.setProfileSharingEnabled(groupV2Proto.whitelisted);
|
||||
account.getGroupStore().updateGroup(connection, group);
|
||||
account.getGroupStore()
|
||||
.storeStorageRecord(connection,
|
||||
group.getGroupId(),
|
||||
groupV2Record.getId(),
|
||||
groupV2Record.toProto().encode());
|
||||
.storeStorageRecord(connection, group.getGroupId(), groupV2Record.getId(), groupV2Proto.encode());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static GroupMasterKey getGroupMasterKeyOrThrow(final ByteString masterKey) {
|
||||
try {
|
||||
return new GroupMasterKey(masterKey.toByteArray());
|
||||
} catch (InvalidInputException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(SignalGroupV2Record lhs, SignalGroupV2Record rhs) {
|
||||
if (Arrays.equals(lhs.getMasterKeyBytes(), rhs.getMasterKeyBytes())) {
|
||||
if (lhs.getProto().masterKey.equals(rhs.getProto().masterKey)) {
|
||||
return 0;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean doProtosMatch(SignalGroupV2Record merged, SignalGroupV2Record other) {
|
||||
return Arrays.equals(merged.toProto().encode(), other.toProto().encode());
|
||||
private static boolean doProtosMatch(GroupV2Record merged, GroupV2Record other) {
|
||||
return Arrays.equals(merged.encode(), other.encode());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,21 +7,27 @@ import org.asamk.signal.manager.storage.groups.GroupInfoV1;
|
|||
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
|
||||
import org.asamk.signal.manager.storage.identities.IdentityInfo;
|
||||
import org.asamk.signal.manager.storage.recipients.Recipient;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
||||
import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
|
||||
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord.UsernameLink;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.GroupV1Record;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
import static org.signal.core.util.StringExtensionsKt.emptyIfNull;
|
||||
|
||||
public final class StorageSyncModels {
|
||||
|
||||
private StorageSyncModels() {
|
||||
|
@ -42,104 +48,97 @@ public final class StorageSyncModels {
|
|||
};
|
||||
}
|
||||
|
||||
public static SignalStorageRecord localToRemoteRecord(
|
||||
public static AccountRecord localToRemoteRecord(
|
||||
ConfigurationStore configStore,
|
||||
Recipient self,
|
||||
UsernameLinkComponents usernameLinkComponents,
|
||||
byte[] rawStorageId
|
||||
UsernameLinkComponents usernameLinkComponents
|
||||
) {
|
||||
final var builder = new SignalAccountRecord.Builder(rawStorageId, self.getStorageRecord());
|
||||
final var builder = SignalAccountRecord.Companion.newBuilder(self.getStorageRecord());
|
||||
if (self.getProfileKey() != null) {
|
||||
builder.setProfileKey(self.getProfileKey().serialize());
|
||||
builder.profileKey(ByteString.of(self.getProfileKey().serialize()));
|
||||
}
|
||||
if (self.getProfile() != null) {
|
||||
builder.setGivenName(self.getProfile().getGivenName())
|
||||
.setFamilyName(self.getProfile().getFamilyName())
|
||||
.setAvatarUrlPath(self.getProfile().getAvatarUrlPath());
|
||||
builder.givenName(emptyIfNull(self.getProfile().getGivenName()))
|
||||
.familyName(emptyIfNull(self.getProfile().getFamilyName()))
|
||||
.avatarUrlPath(emptyIfNull(self.getProfile().getAvatarUrlPath()));
|
||||
}
|
||||
builder.setTypingIndicatorsEnabled(Optional.ofNullable(configStore.getTypingIndicators()).orElse(true))
|
||||
.setReadReceiptsEnabled(Optional.ofNullable(configStore.getReadReceipts()).orElse(true))
|
||||
.setSealedSenderIndicatorsEnabled(Optional.ofNullable(configStore.getUnidentifiedDeliveryIndicators())
|
||||
builder.typingIndicators(Optional.ofNullable(configStore.getTypingIndicators()).orElse(true))
|
||||
.readReceipts(Optional.ofNullable(configStore.getReadReceipts()).orElse(true))
|
||||
.sealedSenderIndicators(Optional.ofNullable(configStore.getUnidentifiedDeliveryIndicators())
|
||||
.orElse(true))
|
||||
.setLinkPreviewsEnabled(Optional.ofNullable(configStore.getLinkPreviews()).orElse(true))
|
||||
.setUnlistedPhoneNumber(Optional.ofNullable(configStore.getPhoneNumberUnlisted()).orElse(false))
|
||||
.setPhoneNumberSharingMode(Optional.ofNullable(configStore.getPhoneNumberSharingMode())
|
||||
.linkPreviews(Optional.ofNullable(configStore.getLinkPreviews()).orElse(true))
|
||||
.unlistedPhoneNumber(Optional.ofNullable(configStore.getPhoneNumberUnlisted()).orElse(false))
|
||||
.phoneNumberSharingMode(Optional.ofNullable(configStore.getPhoneNumberSharingMode())
|
||||
.map(StorageSyncModels::localToRemote)
|
||||
.orElse(AccountRecord.PhoneNumberSharingMode.UNKNOWN))
|
||||
.setE164(self.getAddress().number().orElse(""))
|
||||
.setUsername(self.getAddress().username().orElse(null));
|
||||
.e164(self.getAddress().number().orElse(""))
|
||||
.username(self.getAddress().username().orElse(""));
|
||||
if (usernameLinkComponents != null) {
|
||||
final var linkColor = configStore.getUsernameLinkColor();
|
||||
builder.setUsernameLink(new UsernameLink.Builder().entropy(ByteString.of(usernameLinkComponents.getEntropy()))
|
||||
builder.usernameLink(new UsernameLink.Builder().entropy(ByteString.of(usernameLinkComponents.getEntropy()))
|
||||
.serverId(UuidUtil.toByteString(usernameLinkComponents.getServerId()))
|
||||
.color(linkColor == null ? UsernameLink.Color.UNKNOWN : UsernameLink.Color.valueOf(linkColor))
|
||||
.build());
|
||||
}
|
||||
|
||||
return SignalStorageRecord.forAccount(builder.build());
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static SignalStorageRecord localToRemoteRecord(
|
||||
Recipient recipient,
|
||||
IdentityInfo identity,
|
||||
byte[] rawStorageId
|
||||
) {
|
||||
public static ContactRecord localToRemoteRecord(Recipient recipient, IdentityInfo identity) {
|
||||
final var address = recipient.getAddress();
|
||||
final var builder = new SignalContactRecord.Builder(rawStorageId,
|
||||
address.aci().orElse(null),
|
||||
recipient.getStorageRecord()).setE164(address.number().orElse(null))
|
||||
.setPni(address.pni().orElse(null))
|
||||
.setUsername(address.username().orElse(null))
|
||||
.setProfileKey(recipient.getProfileKey() == null ? null : recipient.getProfileKey().serialize());
|
||||
final var builder = SignalContactRecord.Companion.newBuilder(recipient.getStorageRecord())
|
||||
.aci(address.aci().map(ACI::toString).orElse(""))
|
||||
.e164(address.number().orElse(""))
|
||||
.pni(address.pni().map(PNI::toString).orElse(""))
|
||||
.username(address.username().orElse(""))
|
||||
.profileKey(recipient.getProfileKey() == null
|
||||
? ByteString.EMPTY
|
||||
: ByteString.of(recipient.getProfileKey().serialize()));
|
||||
if (recipient.getProfile() != null) {
|
||||
builder.setProfileGivenName(recipient.getProfile().getGivenName())
|
||||
.setProfileFamilyName(recipient.getProfile().getFamilyName());
|
||||
builder.givenName(emptyIfNull(recipient.getProfile().getGivenName()))
|
||||
.familyName(emptyIfNull(recipient.getProfile().getFamilyName()));
|
||||
}
|
||||
if (recipient.getContact() != null) {
|
||||
builder.setSystemGivenName(recipient.getContact().givenName())
|
||||
.setSystemFamilyName(recipient.getContact().familyName())
|
||||
.setSystemNickname(recipient.getContact().nickName())
|
||||
.setNicknameGivenName(recipient.getContact().nickNameGivenName() == null
|
||||
? ""
|
||||
: recipient.getContact().nickNameGivenName())
|
||||
.setNicknameFamilyName(recipient.getContact().nickNameFamilyName() == null
|
||||
? ""
|
||||
: recipient.getContact().nickNameFamilyName())
|
||||
.setNote(recipient.getContact().note())
|
||||
.setBlocked(recipient.getContact().isBlocked())
|
||||
.setProfileSharingEnabled(recipient.getContact().isProfileSharingEnabled())
|
||||
.setMuteUntil(recipient.getContact().muteUntil())
|
||||
.setHideStory(recipient.getContact().hideStory())
|
||||
.setUnregisteredTimestamp(recipient.getContact().unregisteredTimestamp() == null
|
||||
builder.systemGivenName(emptyIfNull(recipient.getContact().givenName()))
|
||||
.systemFamilyName(emptyIfNull(recipient.getContact().familyName()))
|
||||
.systemNickname(emptyIfNull(recipient.getContact().nickName()))
|
||||
.nickname(new ContactRecord.Name.Builder().given(emptyIfNull(recipient.getContact()
|
||||
.nickNameGivenName()))
|
||||
.family(emptyIfNull(recipient.getContact().nickNameFamilyName()))
|
||||
.build())
|
||||
.note(emptyIfNull(recipient.getContact().note()))
|
||||
.blocked(recipient.getContact().isBlocked())
|
||||
.whitelisted(recipient.getContact().isProfileSharingEnabled())
|
||||
.mutedUntilTimestamp(recipient.getContact().muteUntil())
|
||||
.hideStory(recipient.getContact().hideStory())
|
||||
.unregisteredAtTimestamp(recipient.getContact().unregisteredTimestamp() == null
|
||||
? 0
|
||||
: recipient.getContact().unregisteredTimestamp())
|
||||
.setArchived(recipient.getContact().isArchived())
|
||||
.setHidden(recipient.getContact().isHidden());
|
||||
.archived(recipient.getContact().isArchived())
|
||||
.hidden(recipient.getContact().isHidden());
|
||||
}
|
||||
if (identity != null) {
|
||||
builder.setIdentityKey(identity.getIdentityKey().serialize())
|
||||
.setIdentityState(localToRemote(identity.getTrustLevel()));
|
||||
builder.identityKey(ByteString.of(identity.getIdentityKey().serialize()))
|
||||
.identityState(localToRemote(identity.getTrustLevel()));
|
||||
}
|
||||
return SignalStorageRecord.forContact(builder.build());
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static SignalStorageRecord localToRemoteRecord(GroupInfoV1 group, byte[] rawStorageId) {
|
||||
final var builder = new SignalGroupV1Record.Builder(rawStorageId,
|
||||
group.getGroupId().serialize(),
|
||||
group.getStorageRecord());
|
||||
builder.setBlocked(group.isBlocked());
|
||||
builder.setArchived(group.archived);
|
||||
builder.setProfileSharingEnabled(true);
|
||||
return SignalStorageRecord.forGroupV1(builder.build());
|
||||
public static GroupV1Record localToRemoteRecord(GroupInfoV1 group) {
|
||||
final var builder = SignalGroupV1Record.Companion.newBuilder(group.getStorageRecord());
|
||||
builder.id(ByteString.of(group.getGroupId().serialize()));
|
||||
builder.blocked(group.isBlocked());
|
||||
builder.archived(group.archived);
|
||||
builder.whitelisted(true);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static SignalStorageRecord localToRemoteRecord(GroupInfoV2 group, byte[] rawStorageId) {
|
||||
final var builder = new SignalGroupV2Record.Builder(rawStorageId,
|
||||
group.getMasterKey(),
|
||||
group.getStorageRecord());
|
||||
builder.setBlocked(group.isBlocked());
|
||||
builder.setProfileSharingEnabled(group.isProfileSharingEnabled());
|
||||
return SignalStorageRecord.forGroupV2(builder.build());
|
||||
public static GroupV2Record localToRemoteRecord(GroupInfoV2 group) {
|
||||
final var builder = SignalGroupV2Record.Companion.newBuilder(group.getStorageRecord());
|
||||
builder.masterKey(ByteString.of(group.getMasterKey().serialize()));
|
||||
builder.blocked(group.isBlocked());
|
||||
builder.whitelisted(group.isProfileSharingEnabled());
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static TrustLevel remoteToLocal(IdentityState identityState) {
|
||||
|
|
|
@ -5,6 +5,8 @@ import org.signal.core.util.Base64;
|
|||
import org.signal.core.util.SetUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||
|
@ -32,9 +34,7 @@ public final class StorageSyncValidations {
|
|||
validateManifestAndInserts(result.manifest(), result.inserts(), self);
|
||||
|
||||
if (!result.deletes().isEmpty()) {
|
||||
Set<String> allSetEncoded = result.manifest()
|
||||
.getStorageIds()
|
||||
.stream()
|
||||
Set<String> allSetEncoded = result.manifest().storageIds.stream()
|
||||
.map(StorageId::getRaw)
|
||||
.map(Base64::encodeWithPadding)
|
||||
.collect(Collectors.toSet());
|
||||
|
@ -47,13 +47,13 @@ public final class StorageSyncValidations {
|
|||
}
|
||||
}
|
||||
|
||||
if (previousManifest.getVersion() == 0) {
|
||||
if (previousManifest.version == 0) {
|
||||
logger.debug(
|
||||
"Previous manifest is empty, not bothering with additional validations around the diffs between the two manifests.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.manifest().getVersion() != previousManifest.getVersion() + 1) {
|
||||
if (result.manifest().version != previousManifest.version + 1) {
|
||||
throw new IncorrectManifestVersionError();
|
||||
}
|
||||
|
||||
|
@ -63,13 +63,10 @@ public final class StorageSyncValidations {
|
|||
return;
|
||||
}
|
||||
|
||||
Set<ByteBuffer> previousIds = previousManifest.getStorageIds()
|
||||
.stream()
|
||||
Set<ByteBuffer> previousIds = previousManifest.storageIds.stream()
|
||||
.map(id -> ByteBuffer.wrap(id.getRaw()))
|
||||
.collect(Collectors.toSet());
|
||||
Set<ByteBuffer> newIds = result.manifest()
|
||||
.getStorageIds()
|
||||
.stream()
|
||||
Set<ByteBuffer> newIds = result.manifest().storageIds.stream()
|
||||
.map(id -> ByteBuffer.wrap(id.getRaw()))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
|
@ -83,12 +80,12 @@ public final class StorageSyncValidations {
|
|||
Set<ByteBuffer> declaredDeletes = result.deletes().stream().map(ByteBuffer::wrap).collect(Collectors.toSet());
|
||||
|
||||
if (declaredInserts.size() > manifestInserts.size()) {
|
||||
logger.debug("DeclaredInserts: " + declaredInserts.size() + ", ManifestInserts: " + manifestInserts.size());
|
||||
logger.debug("DeclaredInserts: {}, ManifestInserts: {}", declaredInserts.size(), manifestInserts.size());
|
||||
throw new MoreInsertsThanExpectedError();
|
||||
}
|
||||
|
||||
if (declaredInserts.size() < manifestInserts.size()) {
|
||||
logger.debug("DeclaredInserts: " + declaredInserts.size() + ", ManifestInserts: " + manifestInserts.size());
|
||||
logger.debug("DeclaredInserts: {}, ManifestInserts: {}", declaredInserts.size(), manifestInserts.size());
|
||||
throw new LessInsertsThanExpectedError();
|
||||
}
|
||||
|
||||
|
@ -97,12 +94,12 @@ public final class StorageSyncValidations {
|
|||
}
|
||||
|
||||
if (declaredDeletes.size() > manifestDeletes.size()) {
|
||||
logger.debug("DeclaredDeletes: " + declaredDeletes.size() + ", ManifestDeletes: " + manifestDeletes.size());
|
||||
logger.debug("DeclaredDeletes: {}, ManifestDeletes: {}", declaredDeletes.size(), manifestDeletes.size());
|
||||
throw new MoreDeletesThanExpectedError();
|
||||
}
|
||||
|
||||
if (declaredDeletes.size() < manifestDeletes.size()) {
|
||||
logger.debug("DeclaredDeletes: " + declaredDeletes.size() + ", ManifestDeletes: " + manifestDeletes.size());
|
||||
logger.debug("DeclaredDeletes: {}, ManifestDeletes: {}", declaredDeletes.size(), manifestDeletes.size());
|
||||
throw new LessDeletesThanExpectedError();
|
||||
}
|
||||
|
||||
|
@ -125,7 +122,7 @@ public final class StorageSyncValidations {
|
|||
RecipientAddress self
|
||||
) {
|
||||
int accountCount = 0;
|
||||
for (StorageId id : manifest.getStorageIds()) {
|
||||
for (StorageId id : manifest.storageIds) {
|
||||
accountCount += id.getType() == ManifestRecord.Identifier.Type.ACCOUNT.getValue() ? 1 : 0;
|
||||
}
|
||||
|
||||
|
@ -137,11 +134,11 @@ public final class StorageSyncValidations {
|
|||
throw new MissingAccountError();
|
||||
}
|
||||
|
||||
Set<StorageId> allSet = new HashSet<>(manifest.getStorageIds());
|
||||
Set<StorageId> allSet = new HashSet<>(manifest.storageIds);
|
||||
Set<StorageId> insertSet = inserts.stream().map(SignalStorageRecord::getId).collect(Collectors.toSet());
|
||||
Set<ByteBuffer> rawIdSet = allSet.stream().map(id -> ByteBuffer.wrap(id.getRaw())).collect(Collectors.toSet());
|
||||
|
||||
if (allSet.size() != manifest.getStorageIds().size()) {
|
||||
if (allSet.size() != manifest.storageIds.size()) {
|
||||
throw new DuplicateStorageIdError();
|
||||
}
|
||||
|
||||
|
@ -166,6 +163,11 @@ public final class StorageSyncValidations {
|
|||
throw new DuplicateDistributionListIdError();
|
||||
}
|
||||
|
||||
ids = manifest.getStorageIdsByType().get(ManifestRecord.Identifier.Type.CALL_LINK.getValue());
|
||||
if (ids.size() != new HashSet<>(ids).size()) {
|
||||
throw new DuplicateCallLinkError();
|
||||
}
|
||||
|
||||
throw new DuplicateRawIdAcrossTypesError();
|
||||
}
|
||||
|
||||
|
@ -182,18 +184,18 @@ public final class StorageSyncValidations {
|
|||
throw new UnknownInsertError();
|
||||
}
|
||||
|
||||
if (insert.getContact().isPresent()) {
|
||||
final var contact = insert.getContact().get();
|
||||
final var aci = contact.getAci();
|
||||
final var pni = contact.getPni();
|
||||
final var number = contact.getNumber();
|
||||
final var username = contact.getUsername();
|
||||
if (insert.getProto().contact != null) {
|
||||
final var contact = insert.getProto().contact;
|
||||
final var aci = ACI.parseOrNull(contact.aci);
|
||||
final var pni = PNI.parseOrNull(contact.pni);
|
||||
final var number = contact.e164.isEmpty() ? null : contact.e164;
|
||||
final var username = contact.username.isEmpty() ? null : contact.username;
|
||||
final var address = new RecipientAddress(aci, pni, number, username);
|
||||
if (self.matches(address)) {
|
||||
throw new SelfAddedAsContactError();
|
||||
}
|
||||
}
|
||||
if (insert.getAccount().isPresent() && insert.getAccount().get().getProfileKey().isEmpty()) {
|
||||
if (insert.getProto().account != null && insert.getProto().account.profileKey.size() == 0) {
|
||||
logger.debug("Uploading a null profile key in our AccountRecord!");
|
||||
}
|
||||
}
|
||||
|
@ -211,6 +213,8 @@ public final class StorageSyncValidations {
|
|||
|
||||
private static final class DuplicateDistributionListIdError extends Error {}
|
||||
|
||||
private static final class DuplicateCallLinkError extends Error {}
|
||||
|
||||
private static final class DuplicateInsertInWriteError extends Error {}
|
||||
|
||||
private static final class InsertNotPresentInFullIdSetError extends Error {}
|
||||
|
|
|
@ -21,8 +21,8 @@ public record WriteOperationResult(
|
|||
} else {
|
||||
return String.format(Locale.ROOT,
|
||||
"ManifestVersion: %d, Total Keys: %d, Inserts: %d, Deletes: %d",
|
||||
manifest.getVersion(),
|
||||
manifest.getStorageIds().size(),
|
||||
manifest.version,
|
||||
manifest.storageIds.size(),
|
||||
inserts.size(),
|
||||
deletes.size());
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
|||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.whispersystems.signalservice.api.account.PreKeyCollection;
|
||||
import org.whispersystems.signalservice.api.backup.MediaRootBackupKey;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
@ -112,6 +113,10 @@ public class KeyUtils {
|
|||
return MasterKey.createNew(secureRandom);
|
||||
}
|
||||
|
||||
public static MediaRootBackupKey createMediaRootBackupKey() {
|
||||
return new MediaRootBackupKey(getSecretBytes(32));
|
||||
}
|
||||
|
||||
public static byte[] createRawStorageId() {
|
||||
return getSecretBytes(16);
|
||||
}
|
||||
|
|
|
@ -184,7 +184,7 @@ public class NumberVerificationUtils {
|
|||
TokenNotAcceptedException _e) {
|
||||
throw new CaptchaRequiredException("Captcha not accepted");
|
||||
} catch (NonSuccessfulResponseCodeException e) {
|
||||
if (e.getCode() == 400) {
|
||||
if (e.code == 400) {
|
||||
throw new CaptchaRequiredException("Captcha has invalid format");
|
||||
}
|
||||
throw e;
|
||||
|
|
|
@ -84,6 +84,9 @@ public class ProfileUtils {
|
|||
if (encryptedProfile.getCapabilities().isStorage()) {
|
||||
capabilities.add(Profile.Capability.storage);
|
||||
}
|
||||
if (encryptedProfile.getCapabilities().isStorageServiceEncryptionV2()) {
|
||||
capabilities.add(Profile.Capability.storageServiceEncryptionV2Capability);
|
||||
}
|
||||
|
||||
return capabilities;
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import java.io.InputStream;
|
|||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
@ -30,6 +31,8 @@ import java.util.stream.Collectors;
|
|||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
public class Utils {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(Utils.class);
|
||||
|
@ -157,4 +160,46 @@ public class Utils {
|
|||
}
|
||||
return response.successOrThrow();
|
||||
}
|
||||
|
||||
public static ByteString firstNonEmpty(ByteString... strings) {
|
||||
for (final var s : strings) {
|
||||
if (s.size() > 0) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
return ByteString.EMPTY;
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <T> List<T> firstNonEmpty(List<T>... values) {
|
||||
for (final var s : values) {
|
||||
if (!s.isEmpty()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
public static String firstNonEmpty(String... strings) {
|
||||
for (final var s : strings) {
|
||||
if (!s.isEmpty()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <T> T firstNonNull(T... values) {
|
||||
for (final var v : values) {
|
||||
if (v != null) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String nullIfEmpty(String string) {
|
||||
return string == null || string.isEmpty() ? null : string;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue