Update libsignal-service

Support for storage encryption v2 and account entropy pool

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

View file

@ -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());
}
}

View file

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

View file

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

View file

@ -29,7 +29,8 @@ public class ServiceConfig {
public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) {
final var deleteSync = !isPrimaryDevice;
return new AccountAttributes.Capabilities(true, deleteSync, true);
final var storageEncryptionV2 = !isPrimaryDevice;
return new AccountAttributes.Capabilities(true, deleteSync, true, storageEncryptionV2);
}
public static ServiceEnvironmentConfig getServiceEnvironmentConfig(

View file

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

View file

@ -3,7 +3,6 @@ package org.asamk.signal.manager.helper;
import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.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());
}

View file

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

View file

@ -31,6 +31,7 @@ import org.asamk.signal.manager.internal.SignalDependencies;
import org.asamk.signal.manager.jobs.RetrieveStickerPackJob;
import org.asamk.signal.manager.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());
}
}

View file

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

View file

@ -336,13 +336,6 @@ public final class ProfileHelper {
final var profile = account.getProfileStore().getProfile(recipientId);
if (recipientId.equals(account.getSelfRecipientId())) {
final var isUnrestricted = encryptedProfile.isUnrestrictedUnidentifiedAccess();
if (account.isUnrestrictedUnidentifiedAccess() != isUnrestricted) {
account.setUnrestrictedUnidentifiedAccess(isUnrestricted);
}
}
Profile newProfile = null;
if (profileKey.isPresent()) {
logger.trace("Decrypting profile");
@ -358,6 +351,18 @@ public final class ProfileHelper {
.build();
}
if (recipientId.equals(account.getSelfRecipientId())) {
final var isUnrestricted = encryptedProfile.isUnrestrictedUnidentifiedAccess();
if (account.isUnrestrictedUnidentifiedAccess() != isUnrestricted) {
account.setUnrestrictedUnidentifiedAccess(isUnrestricted);
}
if (account.isPrimaryDevice() && profile != null && newProfile.getCapabilities()
.contains(Profile.Capability.storageServiceEncryptionV2Capability) && !profile.getCapabilities()
.contains(Profile.Capability.storageServiceEncryptionV2Capability)) {
context.getJobExecutor().enqueueJob(new SyncStorageJob(true));
}
}
try {
logger.trace("Storing identity");
final var identityKey = new IdentityKey(Base64.getDecoder().decode(encryptedProfile.getIdentityKey()));

View file

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

View file

@ -2,6 +2,7 @@ package org.asamk.signal.manager.helper;
import org.asamk.signal.manager.api.GroupIdV1;
import org.asamk.signal.manager.api.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());
}
}

View file

@ -247,10 +247,14 @@ public class SyncHelper {
}
public SendMessageResult sendBlockedList() {
var addresses = new ArrayList<SignalServiceAddress>();
var addresses = new ArrayList<BlockedListMessage.Individual>();
for (var record : account.getContactStore().getContacts()) {
if (record.second().isBlocked()) {
addresses.add(context.getRecipientHelper().resolveSignalServiceAddress(record.first()));
final var address = account.getRecipientAddressResolver().resolveRecipientAddress(record.first());
if (address.aci().isPresent() || address.number().isPresent()) {
addresses.add(new BlockedListMessage.Individual(address.aci().orElse(null),
address.number().orElse(null)));
}
}
}
var groupIds = new ArrayList<byte[]>();
@ -276,8 +280,10 @@ public class SyncHelper {
}
public SendMessageResult sendKeysMessage() {
var keysMessage = new KeysMessage(Optional.ofNullable(account.getOrCreateStorageKey()),
Optional.ofNullable(account.getOrCreatePinMasterKey()));
var keysMessage = new KeysMessage(account.getOrCreateStorageKey(),
account.getOrCreatePinMasterKey(),
account.getOrCreateAccountEntropyPool(),
account.getOrCreateMediaRootBackupKey());
return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forKeys(keysMessage));
}
@ -405,7 +411,7 @@ public class SyncHelper {
builder.withMessageExpirationTimeVersion(c.getExpirationTimerVersion().get());
} else {
logger.debug(
"[ContactSync] {} was synced with an old expiration timer. Ignoring. Received: {} Current: ${}",
"[ContactSync] {} was synced with an old expiration timer. Ignoring. Received: {} Current: {}",
recipientId,
c.getExpirationTimerVersion(),
contact == null ? 1 : contact.messageExpirationTimeVersion());

View file

@ -15,10 +15,13 @@ import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.api.groupsv2.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()),

View file

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

View file

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

View file

@ -12,8 +12,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
import org.whispersystems.signalservice.api.util.OptionalUtil;
import org.whispersystems.signalservice.api.storage.StorageId;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
import org.whispersystems.signalservice.internal.storage.protos.OptionalBool;
import java.sql.Connection;
@ -21,6 +22,9 @@ import java.sql.SQLException;
import java.util.Arrays;
import java.util.Optional;
import static org.asamk.signal.manager.util.Utils.firstNonEmpty;
import static org.asamk.signal.manager.util.Utils.firstNonNull;
/**
* Processes {@link SignalAccountRecord}s.
*/
@ -43,10 +47,10 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
final var selfRecipientId = account.getSelfRecipientId();
final var recipient = account.getRecipientStore().getRecipient(connection, selfRecipientId);
final var storageId = account.getRecipientStore().getSelfStorageId(connection);
this.localAccountRecord = StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(),
recipient,
account.getUsernameLink(),
storageId.getRaw()).getAccount().get();
this.localAccountRecord = new SignalAccountRecord(storageId,
StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(),
recipient,
account.getUsernameLink()));
}
@Override
@ -60,99 +64,73 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
}
@Override
protected SignalAccountRecord merge(SignalAccountRecord remote, SignalAccountRecord local) {
protected SignalAccountRecord merge(SignalAccountRecord remoteRecord, SignalAccountRecord localRecord) {
final var remote = remoteRecord.getProto();
final var local = localRecord.getProto();
String givenName;
String familyName;
if (remote.getGivenName().isPresent() || remote.getFamilyName().isPresent()) {
givenName = remote.getGivenName().orElse("");
familyName = remote.getFamilyName().orElse("");
if (!remote.givenName.isEmpty() || !remote.familyName.isEmpty()) {
givenName = remote.givenName;
familyName = remote.familyName;
} else {
givenName = local.getGivenName().orElse("");
familyName = local.getFamilyName().orElse("");
givenName = local.givenName;
familyName = local.familyName;
}
final var payments = remote.getPayments().getEntropy().isPresent() ? remote.getPayments() : local.getPayments();
final var subscriber = remote.getSubscriber().getId().isPresent()
? remote.getSubscriber()
: local.getSubscriber();
final var storyViewReceiptsState = remote.getStoryViewReceiptsState() == OptionalBool.UNSET
? local.getStoryViewReceiptsState()
: remote.getStoryViewReceiptsState();
final var unknownFields = remote.serializeUnknownFields();
final var avatarUrlPath = OptionalUtil.or(remote.getAvatarUrlPath(), local.getAvatarUrlPath()).orElse("");
final var profileKey = OptionalUtil.or(remote.getProfileKey(), local.getProfileKey()).orElse(null);
final var noteToSelfArchived = remote.isNoteToSelfArchived();
final var noteToSelfForcedUnread = remote.isNoteToSelfForcedUnread();
final var readReceipts = remote.isReadReceiptsEnabled();
final var typingIndicators = remote.isTypingIndicatorsEnabled();
final var sealedSenderIndicators = remote.isSealedSenderIndicatorsEnabled();
final var linkPreviews = remote.isLinkPreviewsEnabled();
final var unlisted = remote.isPhoneNumberUnlisted();
final var pinnedConversations = remote.getPinnedConversations();
final var phoneNumberSharingMode = remote.getPhoneNumberSharingMode();
final var preferContactAvatars = remote.isPreferContactAvatars();
final var universalExpireTimer = remote.getUniversalExpireTimer();
final var e164 = account.isPrimaryDevice() ? local.getE164() : remote.getE164();
final var defaultReactions = !remote.getDefaultReactions().isEmpty()
? remote.getDefaultReactions()
: local.getDefaultReactions();
final var displayBadgesOnProfile = remote.isDisplayBadgesOnProfile();
final var subscriptionManuallyCancelled = remote.isSubscriptionManuallyCancelled();
final var keepMutedChatsArchived = remote.isKeepMutedChatsArchived();
final var hasSetMyStoriesPrivacy = remote.hasSetMyStoriesPrivacy();
final var hasViewedOnboardingStory = remote.hasViewedOnboardingStory() || local.hasViewedOnboardingStory();
final var storiesDisabled = remote.isStoriesDisabled();
final var hasSeenGroupStoryEducation = remote.hasSeenGroupStoryEducationSheet()
|| local.hasSeenGroupStoryEducationSheet();
boolean hasSeenUsernameOnboarding = remote.hasCompletedUsernameOnboarding()
|| local.hasCompletedUsernameOnboarding();
final var username = remote.getUsername();
final var usernameLink = remote.getUsernameLink();
final var mergedBuilder = new SignalAccountRecord.Builder(remote.getId().getRaw(), unknownFields).setGivenName(
givenName)
.setFamilyName(familyName)
.setAvatarUrlPath(avatarUrlPath)
.setProfileKey(profileKey)
.setNoteToSelfArchived(noteToSelfArchived)
.setNoteToSelfForcedUnread(noteToSelfForcedUnread)
.setReadReceiptsEnabled(readReceipts)
.setTypingIndicatorsEnabled(typingIndicators)
.setSealedSenderIndicatorsEnabled(sealedSenderIndicators)
.setLinkPreviewsEnabled(linkPreviews)
.setUnlistedPhoneNumber(unlisted)
.setPhoneNumberSharingMode(phoneNumberSharingMode)
.setPinnedConversations(pinnedConversations)
.setPreferContactAvatars(preferContactAvatars)
.setPayments(payments.isEnabled(), payments.getEntropy().orElse(null))
.setUniversalExpireTimer(universalExpireTimer)
.setDefaultReactions(defaultReactions)
.setSubscriber(subscriber)
.setDisplayBadgesOnProfile(displayBadgesOnProfile)
.setSubscriptionManuallyCancelled(subscriptionManuallyCancelled)
.setKeepMutedChatsArchived(keepMutedChatsArchived)
.setHasSetMyStoriesPrivacy(hasSetMyStoriesPrivacy)
.setHasViewedOnboardingStory(hasViewedOnboardingStory)
.setStoriesDisabled(storiesDisabled)
.setHasSeenGroupStoryEducationSheet(hasSeenGroupStoryEducation)
.setHasCompletedUsernameOnboarding(hasSeenUsernameOnboarding)
.setStoryViewReceiptsState(storyViewReceiptsState)
.setUsername(username)
.setUsernameLink(usernameLink)
.setE164(e164);
final var mergedBuilder = SignalAccountRecord.Companion.newBuilder(remote.unknownFields().toByteArray())
.givenName(givenName)
.familyName(familyName)
.avatarUrlPath(firstNonEmpty(remote.avatarUrlPath, local.avatarUrlPath))
.profileKey(firstNonEmpty(remote.profileKey, local.profileKey))
.noteToSelfArchived(remote.noteToSelfArchived)
.noteToSelfMarkedUnread(remote.noteToSelfMarkedUnread)
.readReceipts(remote.readReceipts)
.typingIndicators(remote.typingIndicators)
.sealedSenderIndicators(remote.sealedSenderIndicators)
.linkPreviews(remote.linkPreviews)
.unlistedPhoneNumber(remote.unlistedPhoneNumber)
.phoneNumberSharingMode(remote.phoneNumberSharingMode)
.pinnedConversations(remote.pinnedConversations)
.preferContactAvatars(remote.preferContactAvatars)
.universalExpireTimer(remote.universalExpireTimer)
.preferredReactionEmoji(firstNonEmpty(remote.preferredReactionEmoji, local.preferredReactionEmoji))
.subscriberId(firstNonEmpty(remote.subscriberId, local.subscriberId))
.subscriberCurrencyCode(firstNonEmpty(remote.subscriberCurrencyCode, local.subscriberCurrencyCode))
.backupsSubscriberId(firstNonEmpty(remote.backupsSubscriberId, local.backupsSubscriberId))
.backupsSubscriberCurrencyCode(firstNonEmpty(remote.backupsSubscriberCurrencyCode,
local.backupsSubscriberCurrencyCode))
.displayBadgesOnProfile(remote.displayBadgesOnProfile)
.subscriptionManuallyCancelled(remote.subscriptionManuallyCancelled)
.keepMutedChatsArchived(remote.keepMutedChatsArchived)
.hasSetMyStoriesPrivacy(remote.hasSetMyStoriesPrivacy)
.hasViewedOnboardingStory(remote.hasViewedOnboardingStory || local.hasViewedOnboardingStory)
.storiesDisabled(remote.storiesDisabled)
.hasSeenGroupStoryEducationSheet(remote.hasSeenGroupStoryEducationSheet
|| local.hasSeenGroupStoryEducationSheet)
.hasCompletedUsernameOnboarding(remote.hasCompletedUsernameOnboarding
|| local.hasCompletedUsernameOnboarding)
.storyViewReceiptsEnabled(remote.storyViewReceiptsEnabled == OptionalBool.UNSET
? local.storyViewReceiptsEnabled
: remote.storyViewReceiptsEnabled)
.username(remote.username)
.usernameLink(remote.usernameLink)
.e164(account.isPrimaryDevice() ? local.e164 : remote.e164);
if (firstNonNull(remote.payments, local.payments) != null) {
mergedBuilder.payments(firstNonNull(remote.payments, local.payments));
}
final var merged = mergedBuilder.build();
final var matchesRemote = doProtosMatch(merged, remote);
if (matchesRemote) {
return remote;
return remoteRecord;
}
final var matchesLocal = doProtosMatch(merged, local);
if (matchesLocal) {
return local;
return localRecord;
}
return mergedBuilder.setId(KeyUtils.createRawStorageId()).build();
return new SignalAccountRecord(StorageId.forAccount(KeyUtils.createRawStorageId()), mergedBuilder.build());
}
@Override
@ -164,56 +142,55 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
@Override
protected void updateLocal(StorageRecordUpdate<SignalAccountRecord> update) throws SQLException {
final var accountRecord = update.newRecord();
final var accountProto = accountRecord.getProto();
if (!accountRecord.getE164().equals(account.getNumber())) {
if (!accountProto.e164.equals(account.getNumber())) {
jobExecutor.enqueueJob(new CheckWhoAmIJob());
}
account.getConfigurationStore().setReadReceipts(connection, accountRecord.isReadReceiptsEnabled());
account.getConfigurationStore().setTypingIndicators(connection, accountRecord.isTypingIndicatorsEnabled());
account.getConfigurationStore().setReadReceipts(connection, accountProto.readReceipts);
account.getConfigurationStore().setTypingIndicators(connection, accountProto.typingIndicators);
account.getConfigurationStore()
.setUnidentifiedDeliveryIndicators(connection, accountRecord.isSealedSenderIndicatorsEnabled());
account.getConfigurationStore().setLinkPreviews(connection, accountRecord.isLinkPreviewsEnabled());
.setUnidentifiedDeliveryIndicators(connection, accountProto.sealedSenderIndicators);
account.getConfigurationStore().setLinkPreviews(connection, accountProto.linkPreviews);
account.getConfigurationStore()
.setPhoneNumberSharingMode(connection,
StorageSyncModels.remoteToLocal(accountRecord.getPhoneNumberSharingMode()));
account.getConfigurationStore().setPhoneNumberUnlisted(connection, accountRecord.isPhoneNumberUnlisted());
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());
}
}

View file

@ -17,7 +17,8 @@ import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.api.push.ServiceId.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());
}
}

View file

@ -8,6 +8,8 @@ import org.asamk.signal.manager.util.KeyUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
import org.whispersystems.signalservice.api.storage.StorageId;
import org.whispersystems.signalservice.internal.storage.protos.GroupV1Record;
import java.sql.Connection;
import java.sql.SQLException;
@ -36,7 +38,7 @@ public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor<
@Override
protected boolean isInvalid(SignalGroupV1Record remote) throws SQLException {
try {
final var id = GroupId.unknownVersion(remote.getGroupId());
final var id = GroupId.unknownVersion(remote.getProto().id.toByteArray());
if (!(id instanceof GroupIdV1)) {
return true;
}
@ -56,7 +58,7 @@ public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor<
@Override
protected Optional<SignalGroupV1Record> getMatching(SignalGroupV1Record remote) throws SQLException {
final var id = GroupId.v1(remote.getGroupId());
final var id = GroupId.v1(remote.getProto().id.toByteArray());
final var group = account.getGroupStore().getGroup(connection, id);
if (group == null) {
@ -64,39 +66,35 @@ public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor<
}
final var storageId = account.getGroupStore().getGroupStorageId(connection, id);
return Optional.of(StorageSyncModels.localToRemoteRecord(group, storageId.getRaw()).getGroupV1().get());
return Optional.of(new SignalGroupV1Record(storageId, StorageSyncModels.localToRemoteRecord(group)));
}
@Override
protected SignalGroupV1Record merge(SignalGroupV1Record remote, SignalGroupV1Record local) {
final var unknownFields = remote.serializeUnknownFields();
final var blocked = remote.isBlocked();
final var profileSharing = remote.isProfileSharingEnabled();
final var archived = remote.isArchived();
final var forcedUnread = remote.isForcedUnread();
final var muteUntil = remote.getMuteUntil();
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());
}
}

View file

@ -3,16 +3,22 @@ package org.asamk.signal.manager.syncStorage;
import org.asamk.signal.manager.groups.GroupUtils;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.util.KeyUtils;
import org.jetbrains.annotations.NotNull;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
import org.whispersystems.signalservice.api.storage.StorageId;
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Optional;
import okio.ByteString;
public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<SignalGroupV2Record> {
private static final Logger logger = LoggerFactory.getLogger(GroupV2RecordProcessor.class);
@ -26,12 +32,12 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
@Override
protected boolean isInvalid(SignalGroupV2Record remote) {
return remote.getMasterKeyBytes().length != GroupMasterKey.SIZE;
return remote.getProto().masterKey.size() != GroupMasterKey.SIZE;
}
@Override
protected Optional<SignalGroupV2Record> getMatching(SignalGroupV2Record remote) throws SQLException {
final var id = GroupUtils.getGroupIdV2(remote.getMasterKeyOrThrow());
final var id = GroupUtils.getGroupIdV2(getGroupMasterKeyOrThrow(remote.getProto().masterKey));
final var group = account.getGroupStore().getGroup(connection, id);
if (group == null) {
@ -39,44 +45,37 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
}
final var storageId = account.getGroupStore().getGroupStorageId(connection, id);
return Optional.of(StorageSyncModels.localToRemoteRecord(group, storageId.getRaw()).getGroupV2().get());
return Optional.of(new SignalGroupV2Record(storageId, StorageSyncModels.localToRemoteRecord(group)));
}
@Override
protected SignalGroupV2Record merge(SignalGroupV2Record remote, SignalGroupV2Record local) {
final var unknownFields = remote.serializeUnknownFields();
final var blocked = remote.isBlocked();
final var profileSharing = remote.isProfileSharingEnabled();
final var archived = remote.isArchived();
final var forcedUnread = remote.isForcedUnread();
final var muteUntil = remote.getMuteUntil();
final var notifyForMentionsWhenMuted = remote.notifyForMentionsWhenMuted();
final var hideStory = remote.shouldHideStory();
final var storySendMode = remote.getStorySendMode();
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());
}
}

View file

@ -7,21 +7,27 @@ import org.asamk.signal.manager.storage.groups.GroupInfoV1;
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
import org.asamk.signal.manager.storage.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) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -18,6 +18,7 @@ import java.io.InputStream;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
@ -30,6 +31,8 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import okio.ByteString;
public class Utils {
private static final Logger logger = LoggerFactory.getLogger(Utils.class);
@ -157,4 +160,46 @@ public class Utils {
}
return response.successOrThrow();
}
public static ByteString firstNonEmpty(ByteString... strings) {
for (final var s : strings) {
if (s.size() > 0) {
return s;
}
}
return ByteString.EMPTY;
}
@SafeVarargs
public static <T> List<T> firstNonEmpty(List<T>... values) {
for (final var s : values) {
if (!s.isEmpty()) {
return s;
}
}
return List.of();
}
public static String firstNonEmpty(String... strings) {
for (final var s : strings) {
if (!s.isEmpty()) {
return s;
}
}
return "";
}
@SafeVarargs
public static <T> T firstNonNull(T... values) {
for (final var v : values) {
if (v != null) {
return v;
}
}
return null;
}
public static String nullIfEmpty(String string) {
return string == null || string.isEmpty() ? null : string;
}
}