Update the storage service.
parent
133bd44b85
commit
6184e5f828
|
@ -30,7 +30,7 @@ public final class AppInitialization {
|
|||
TextSecurePreferences.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode());
|
||||
TextSecurePreferences.setHasSeenStickerIntroTooltip(context, true);
|
||||
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
|
||||
SignalStore.registrationValues().onNewInstall();
|
||||
SignalStore.onFirstEverAppLaunch();
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
|
||||
|
@ -39,7 +39,7 @@ public final class AppInitialization {
|
|||
|
||||
public static void onPostBackupRestore(@NonNull Context context) {
|
||||
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
|
||||
SignalStore.registrationValues().onNewInstall();
|
||||
SignalStore.onFirstEverAppLaunch();
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
|
||||
|
|
|
@ -47,6 +47,7 @@ import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
|||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.logging.AndroidLogger;
|
||||
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
@ -54,6 +55,7 @@ import org.thoughtcrime.securesms.logging.PersistentLogger;
|
|||
import org.thoughtcrime.securesms.logging.SignalUncaughtExceptionHandler;
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil;
|
||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
||||
import org.thoughtcrime.securesms.migrations.StorageServiceMigrationJob;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
|
@ -134,6 +136,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
|||
FeatureFlags.init();
|
||||
NotificationChannels.create(this);
|
||||
RefreshPreKeysJob.scheduleIfNecessary();
|
||||
StorageSyncJob.scheduleIfNecessary();
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
|
||||
|
||||
if (Build.VERSION.SDK_INT < 21) {
|
||||
|
|
|
@ -44,6 +44,7 @@ import android.text.TextUtils;
|
|||
import android.text.method.LinkMovementMethod;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.ContextMenu.ContextMenuInfo;
|
||||
|
@ -604,6 +605,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
|
|||
remoteIdentity,
|
||||
isChecked ? VerifiedStatus.VERIFIED :
|
||||
VerifiedStatus.DEFAULT));
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
|
||||
IdentityUtil.markIdentityVerified(getActivity(), recipient.get(), isChecked, false);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ import androidx.annotation.WorkerThread;
|
|||
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
|
||||
|
@ -15,8 +17,15 @@ import java.io.IOException;
|
|||
|
||||
public class DirectoryHelper {
|
||||
|
||||
private static final String TAG = Log.tag(DirectoryHelper.class);
|
||||
|
||||
@WorkerThread
|
||||
public static void refreshDirectory(@NonNull Context context, boolean notifyOfNewUsers) throws IOException {
|
||||
if (!SignalStore.storageServiceValues().hasFirstStorageSyncCompleted()) {
|
||||
Log.i(TAG, "First storage sync has not completed. Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (FeatureFlags.uuids()) {
|
||||
// TODO [greyson] Create a DirectoryHelperV2 when appropriate.
|
||||
DirectoryHelperV1.refreshDirectory(context, notifyOfNewUsers);
|
||||
|
@ -24,9 +33,7 @@ public class DirectoryHelper {
|
|||
DirectoryHelperV1.refreshDirectory(context, notifyOfNewUsers);
|
||||
}
|
||||
|
||||
if (FeatureFlags.storageService()) {
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
}
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
@ -41,7 +48,7 @@ public class DirectoryHelper {
|
|||
newRegisteredState = DirectoryHelperV1.refreshDirectoryFor(context, recipient, notifyOfNewUsers);
|
||||
}
|
||||
|
||||
if (FeatureFlags.storageService() && newRegisteredState != originalRegisteredState) {
|
||||
if (newRegisteredState != originalRegisteredState) {
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
}
|
||||
|
||||
|
|
|
@ -4,18 +4,22 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.SetUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord.IdentityState;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||
import org.whispersystems.signalservice.api.util.OptionalUtil;
|
||||
|
@ -28,11 +32,14 @@ import java.util.HashMap;
|
|||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.crypto.KeyGenerator;
|
||||
|
||||
public final class StorageSyncHelper {
|
||||
|
||||
private static final String TAG = Log.tag(StorageSyncHelper.class);
|
||||
|
@ -42,7 +49,7 @@ public final class StorageSyncHelper {
|
|||
private static KeyGenerator testKeyGenerator = null;
|
||||
|
||||
/**
|
||||
* Given the local state of pending storage mutatations, this will generate a result that will
|
||||
* Given the local state of pending storage mutations, this will generate a result that will
|
||||
* include that data that needs to be written to the storage service, as well as any changes you
|
||||
* need to write back to local storage (like storage keys that might have changed for updated
|
||||
* contacts).
|
||||
|
@ -64,17 +71,17 @@ public final class StorageSyncHelper {
|
|||
@NonNull List<RecipientSettings> deletes)
|
||||
{
|
||||
Set<ByteBuffer> completeKeys = new LinkedHashSet<>(Stream.of(currentLocalKeys).map(ByteBuffer::wrap).toList());
|
||||
Set<SignalContactRecord> contactInserts = new LinkedHashSet<>();
|
||||
Set<ByteBuffer> contactDeletes = new LinkedHashSet<>();
|
||||
Set<SignalStorageRecord> storageInserts = new LinkedHashSet<>();
|
||||
Set<ByteBuffer> storageDeletes = new LinkedHashSet<>();
|
||||
Map<RecipientId, byte[]> storageKeyUpdates = new HashMap<>();
|
||||
|
||||
for (RecipientSettings insert : inserts) {
|
||||
contactInserts.add(localToRemoteContact(insert));
|
||||
storageInserts.add(localToRemoteRecord(insert));
|
||||
}
|
||||
|
||||
for (RecipientSettings delete : deletes) {
|
||||
byte[] key = Objects.requireNonNull(delete.getStorageKey());
|
||||
contactDeletes.add(ByteBuffer.wrap(key));
|
||||
storageDeletes.add(ByteBuffer.wrap(key));
|
||||
completeKeys.remove(ByteBuffer.wrap(key));
|
||||
}
|
||||
|
||||
|
@ -82,21 +89,20 @@ public final class StorageSyncHelper {
|
|||
byte[] oldKey = Objects.requireNonNull(update.getStorageKey());
|
||||
byte[] newKey = generateKey();
|
||||
|
||||
contactInserts.add(localToRemoteContact(update, newKey));
|
||||
contactDeletes.add(ByteBuffer.wrap(oldKey));
|
||||
storageInserts.add(localToRemoteRecord(update, newKey));
|
||||
storageDeletes.add(ByteBuffer.wrap(oldKey));
|
||||
completeKeys.remove(ByteBuffer.wrap(oldKey));
|
||||
completeKeys.add(ByteBuffer.wrap(newKey));
|
||||
storageKeyUpdates.put(update.getId(), newKey);
|
||||
}
|
||||
|
||||
if (contactInserts.isEmpty() && contactDeletes.isEmpty()) {
|
||||
if (storageInserts.isEmpty() && storageDeletes.isEmpty()) {
|
||||
return Optional.absent();
|
||||
} else {
|
||||
List<SignalStorageRecord> storageInserts = Stream.of(contactInserts).map(c -> SignalStorageRecord.forContact(c.getKey(), c)).toList();
|
||||
List<byte[]> contactDeleteBytes = Stream.of(contactDeletes).map(ByteBuffer::array).toList();
|
||||
List<byte[]> contactDeleteBytes = Stream.of(storageDeletes).map(ByteBuffer::array).toList();
|
||||
List<byte[]> completeKeysBytes = Stream.of(completeKeys).map(ByteBuffer::array).toList();
|
||||
SignalStorageManifest manifest = new SignalStorageManifest(currentManifestVersion + 1, completeKeysBytes);
|
||||
WriteOperationResult writeOperationResult = new WriteOperationResult(manifest, storageInserts, contactDeleteBytes);
|
||||
WriteOperationResult writeOperationResult = new WriteOperationResult(manifest, new ArrayList<>(storageInserts), contactDeleteBytes);
|
||||
|
||||
return Optional.of(new LocalWriteResult(writeOperationResult, storageKeyUpdates));
|
||||
}
|
||||
|
@ -142,17 +148,35 @@ public final class StorageSyncHelper {
|
|||
List<SignalContactRecord> remoteOnlyContacts = Stream.of(remoteOnlyRecords).filter(r -> r.getContact().isPresent()).map(r -> r.getContact().get()).toList();
|
||||
List<SignalContactRecord> localOnlyContacts = Stream.of(localOnlyRecords).filter(r -> r.getContact().isPresent()).map(r -> r.getContact().get()).toList();
|
||||
|
||||
List<SignalGroupV1Record> remoteOnlyGroupV1 = Stream.of(remoteOnlyRecords).filter(r -> r.getGroupV1().isPresent()).map(r -> r.getGroupV1().get()).toList();
|
||||
List<SignalGroupV1Record> localOnlyGroupV1 = Stream.of(localOnlyRecords).filter(r -> r.getGroupV1().isPresent()).map(r -> r.getGroupV1().get()).toList();
|
||||
|
||||
List<SignalStorageRecord> remoteOnlyUnknowns = Stream.of(remoteOnlyRecords).filter(SignalStorageRecord::isUnknown).toList();
|
||||
List<SignalStorageRecord> localOnlyUnknowns = Stream.of(localOnlyRecords).filter(SignalStorageRecord::isUnknown).toList();
|
||||
|
||||
ContactRecordMergeResult contactMergeResult = resolveContactConflict(remoteOnlyContacts, localOnlyContacts);
|
||||
GroupV1RecordMergeResult groupV1MergeResult = resolveGroupV1Conflict(remoteOnlyGroupV1, localOnlyGroupV1);
|
||||
|
||||
Set<SignalStorageRecord> remoteInserts = new HashSet<>();
|
||||
remoteInserts.addAll(Stream.of(contactMergeResult.remoteInserts).map(SignalStorageRecord::forContact).toList());
|
||||
remoteInserts.addAll(Stream.of(groupV1MergeResult.remoteInserts).map(SignalStorageRecord::forGroupV1).toList());
|
||||
|
||||
Set<RecordUpdate> remoteUpdates = new HashSet<>();
|
||||
remoteUpdates.addAll(Stream.of(contactMergeResult.remoteUpdates)
|
||||
.map(c -> new RecordUpdate(SignalStorageRecord.forContact(c.getOld()), SignalStorageRecord.forContact(c.getNew())))
|
||||
.toList());
|
||||
remoteUpdates.addAll(Stream.of(groupV1MergeResult.remoteUpdates)
|
||||
.map(c -> new RecordUpdate(SignalStorageRecord.forGroupV1(c.getOld()), SignalStorageRecord.forGroupV1(c.getNew())))
|
||||
.toList());
|
||||
|
||||
return new MergeResult(contactMergeResult.localInserts,
|
||||
contactMergeResult.localUpdates,
|
||||
contactMergeResult.remoteInserts,
|
||||
contactMergeResult.remoteUpdates,
|
||||
groupV1MergeResult.localInserts,
|
||||
groupV1MergeResult.localUpdates,
|
||||
new LinkedHashSet<>(remoteOnlyUnknowns),
|
||||
new LinkedHashSet<>(localOnlyUnknowns));
|
||||
new LinkedHashSet<>(localOnlyUnknowns),
|
||||
remoteInserts,
|
||||
remoteUpdates);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -169,7 +193,11 @@ public final class StorageSyncHelper {
|
|||
completeKeys.add(ByteBuffer.wrap(insert.getKey()));
|
||||
}
|
||||
|
||||
for (SignalContactRecord insert : mergeResult.getRemoteContactInserts()) {
|
||||
for (SignalGroupV1Record insert : mergeResult.getLocalGroupV1Inserts()) {
|
||||
completeKeys.add(ByteBuffer.wrap(insert.getKey()));
|
||||
}
|
||||
|
||||
for (SignalStorageRecord insert : mergeResult.getRemoteInserts()) {
|
||||
completeKeys.add(ByteBuffer.wrap(insert.getKey()));
|
||||
}
|
||||
|
||||
|
@ -178,34 +206,47 @@ public final class StorageSyncHelper {
|
|||
}
|
||||
|
||||
for (ContactUpdate update : mergeResult.getLocalContactUpdates()) {
|
||||
completeKeys.remove(ByteBuffer.wrap(update.getOldContact().getKey()));
|
||||
completeKeys.add(ByteBuffer.wrap(update.getNewContact().getKey()));
|
||||
completeKeys.remove(ByteBuffer.wrap(update.getOld().getKey()));
|
||||
completeKeys.add(ByteBuffer.wrap(update.getNew().getKey()));
|
||||
}
|
||||
|
||||
for (ContactUpdate update : mergeResult.getRemoteContactUpdates()) {
|
||||
completeKeys.remove(ByteBuffer.wrap(update.getOldContact().getKey()));
|
||||
completeKeys.add(ByteBuffer.wrap(update.getNewContact().getKey()));
|
||||
for (GroupV1Update update : mergeResult.getLocalGroupV1Updates()) {
|
||||
completeKeys.remove(ByteBuffer.wrap(update.getOld().getKey()));
|
||||
completeKeys.add(ByteBuffer.wrap(update.getNew().getKey()));
|
||||
}
|
||||
|
||||
for (RecordUpdate update : mergeResult.getRemoteUpdates()) {
|
||||
completeKeys.remove(ByteBuffer.wrap(update.getOld().getKey()));
|
||||
completeKeys.add(ByteBuffer.wrap(update.getNew().getKey()));
|
||||
}
|
||||
|
||||
SignalStorageManifest manifest = new SignalStorageManifest(currentManifestVersion + 1, Stream.of(completeKeys).map(ByteBuffer::array).toList());
|
||||
|
||||
List<SignalContactRecord> contactInserts = new ArrayList<>();
|
||||
contactInserts.addAll(mergeResult.getRemoteContactInserts());
|
||||
contactInserts.addAll(Stream.of(mergeResult.getRemoteContactUpdates()).map(ContactUpdate::getNewContact).toList());
|
||||
List<SignalStorageRecord> inserts = new ArrayList<>();
|
||||
inserts.addAll(mergeResult.getRemoteInserts());
|
||||
inserts.addAll(Stream.of(mergeResult.getRemoteUpdates()).map(RecordUpdate::getNew).toList());
|
||||
|
||||
List<SignalStorageRecord> inserts = Stream.of(contactInserts).map(c -> SignalStorageRecord.forContact(c.getKey(), c)).toList();
|
||||
|
||||
List<byte[]> deletes = Stream.of(mergeResult.getRemoteContactUpdates()).map(ContactUpdate::getOldContact).map(SignalContactRecord::getKey).toList();
|
||||
List<byte[]> deletes = Stream.of(mergeResult.getRemoteUpdates()).map(RecordUpdate::getOld).map(SignalStorageRecord::getKey).toList();
|
||||
|
||||
return new WriteOperationResult(manifest, inserts, deletes);
|
||||
}
|
||||
|
||||
public static @NonNull SignalContactRecord localToRemoteContact(@NonNull RecipientSettings recipient) {
|
||||
if (recipient.getStorageKey() == null) {
|
||||
public static @NonNull SignalStorageRecord localToRemoteRecord(@NonNull RecipientSettings settings) {
|
||||
if (settings.getStorageKey() == null) {
|
||||
throw new AssertionError("Must have a storage key!");
|
||||
}
|
||||
|
||||
return localToRemoteContact(recipient, recipient.getStorageKey());
|
||||
return localToRemoteRecord(settings, settings.getStorageKey());
|
||||
}
|
||||
|
||||
public static @NonNull SignalStorageRecord localToRemoteRecord(@NonNull RecipientSettings settings, @NonNull byte[] key) {
|
||||
if (settings.getGroupType() == RecipientDatabase.GroupType.NONE) {
|
||||
return SignalStorageRecord.forContact(localToRemoteContact(settings, key));
|
||||
} else if (settings.getGroupType() == RecipientDatabase.GroupType.SIGNAL_V1) {
|
||||
return SignalStorageRecord.forGroupV1(localToRemoteGroupV1(settings, key));
|
||||
} else {
|
||||
throw new AssertionError("Unsupported type!");
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull SignalContactRecord localToRemoteContact(@NonNull RecipientSettings recipient, byte[] storageKey) {
|
||||
|
@ -215,7 +256,8 @@ public final class StorageSyncHelper {
|
|||
|
||||
return new SignalContactRecord.Builder(storageKey, new SignalServiceAddress(recipient.getUuid(), recipient.getE164()))
|
||||
.setProfileKey(recipient.getProfileKey())
|
||||
.setProfileName(recipient.getProfileName().serialize())
|
||||
.setGivenName(recipient.getProfileName().getGivenName())
|
||||
.setFamilyName(recipient.getProfileName().getFamilyName())
|
||||
.setBlocked(recipient.isBlocked())
|
||||
.setProfileSharingEnabled(recipient.isProfileSharing())
|
||||
.setIdentityKey(recipient.getIdentityKey())
|
||||
|
@ -223,6 +265,17 @@ public final class StorageSyncHelper {
|
|||
.build();
|
||||
}
|
||||
|
||||
private static @NonNull SignalGroupV1Record localToRemoteGroupV1(@NonNull RecipientSettings recipient, byte[] storageKey) {
|
||||
if (recipient.getGroupId() == null) {
|
||||
throw new AssertionError("Must have a groupId!");
|
||||
}
|
||||
|
||||
return new SignalGroupV1Record.Builder(storageKey, GroupUtil.getDecodedIdOrThrow(recipient.getGroupId()))
|
||||
.setBlocked(recipient.isBlocked())
|
||||
.setProfileSharingEnabled(recipient.isProfileSharing())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static @NonNull IdentityDatabase.VerifiedStatus remoteToLocalIdentityStatus(@NonNull IdentityState identityState) {
|
||||
switch (identityState) {
|
||||
case VERIFIED: return IdentityDatabase.VerifiedStatus.VERIFIED;
|
||||
|
@ -246,16 +299,17 @@ public final class StorageSyncHelper {
|
|||
UUID uuid = remote.getAddress().getUuid().or(local.getAddress().getUuid()).orNull();
|
||||
String e164 = remote.getAddress().getNumber().or(local.getAddress().getNumber()).orNull();
|
||||
SignalServiceAddress address = new SignalServiceAddress(uuid, e164);
|
||||
String profileName = remote.getProfileName().or(local.getProfileName()).orNull();
|
||||
String givenName = remote.getGivenName().or(local.getGivenName()).or("");
|
||||
String familyName = remote.getFamilyName().or(local.getFamilyName()).or("");
|
||||
byte[] profileKey = remote.getProfileKey().or(local.getProfileKey()).orNull();
|
||||
String username = remote.getUsername().or(local.getUsername()).orNull();
|
||||
String username = remote.getUsername().or(local.getUsername()).or("");
|
||||
IdentityState identityState = remote.getIdentityState();
|
||||
byte[] identityKey = remote.getIdentityKey().or(local.getIdentityKey()).orNull();
|
||||
String nickname = local.getNickname().orNull(); // TODO [greyson] Update this when we add real nickname support
|
||||
String nickname = local.getNickname().or(""); // TODO [greyson] Update this when we add real nickname support
|
||||
boolean blocked = remote.isBlocked();
|
||||
boolean profileSharing = remote.isProfileSharingEnabled() | local.isProfileSharingEnabled();
|
||||
boolean matchesRemote = doParamsMatchContact(remote, address, profileName, profileKey, username, identityState, identityKey, blocked, profileSharing, nickname);
|
||||
boolean matchesLocal = doParamsMatchContact(local, address, profileName, profileKey, username, identityState, identityKey, blocked, profileSharing, nickname);
|
||||
boolean profileSharing = remote.isProfileSharingEnabled() || local.isProfileSharingEnabled();
|
||||
boolean matchesRemote = doParamsMatchContact(remote, address, givenName, familyName, profileKey, username, identityState, identityKey, blocked, profileSharing, nickname);
|
||||
boolean matchesLocal = doParamsMatchContact(local, address, givenName, familyName, profileKey, username, identityState, identityKey, blocked, profileSharing, nickname);
|
||||
|
||||
if (remote.getProtoVersion() > 0) {
|
||||
Log.w(TAG, "Inbound model has version " + remote.getProtoVersion() + ", but our version is 0.");
|
||||
|
@ -267,15 +321,38 @@ public final class StorageSyncHelper {
|
|||
return local;
|
||||
} else {
|
||||
return new SignalContactRecord.Builder(generateKey(), address)
|
||||
.setProfileName(profileName)
|
||||
.setProfileKey(profileKey)
|
||||
.setUsername(username)
|
||||
.setIdentityState(identityState)
|
||||
.setIdentityKey(identityKey)
|
||||
.setBlocked(blocked)
|
||||
.setProfileSharingEnabled(profileSharing)
|
||||
.setNickname(nickname)
|
||||
.build();
|
||||
.setGivenName(givenName)
|
||||
.setFamilyName(familyName)
|
||||
.setProfileKey(profileKey)
|
||||
.setUsername(username)
|
||||
.setIdentityState(identityState)
|
||||
.setIdentityKey(identityKey)
|
||||
.setBlocked(blocked)
|
||||
.setProfileSharingEnabled(profileSharing)
|
||||
.setNickname(nickname)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static @NonNull SignalGroupV1Record mergeGroupV1(@NonNull SignalGroupV1Record remote,
|
||||
@NonNull SignalGroupV1Record local)
|
||||
{
|
||||
boolean blocked = remote.isBlocked();
|
||||
boolean profileSharing = remote.isProfileSharingEnabled() || local.isProfileSharingEnabled();
|
||||
|
||||
boolean matchesRemote = blocked == remote.isBlocked() && profileSharing == remote.isProfileSharingEnabled();
|
||||
boolean matchesLocal = blocked == local.isBlocked() && profileSharing == local.isProfileSharingEnabled();
|
||||
|
||||
if (matchesRemote) {
|
||||
return remote;
|
||||
} else if (matchesLocal) {
|
||||
return local;
|
||||
} else {
|
||||
return new SignalGroupV1Record.Builder(generateKey(), remote.getGroupId())
|
||||
.setBlocked(blocked)
|
||||
.setProfileSharingEnabled(blocked)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -294,7 +371,8 @@ public final class StorageSyncHelper {
|
|||
|
||||
private static boolean doParamsMatchContact(@NonNull SignalContactRecord contact,
|
||||
@NonNull SignalServiceAddress address,
|
||||
@Nullable String profileName,
|
||||
@Nullable String givenName,
|
||||
@Nullable String familyName,
|
||||
@Nullable byte[] profileKey,
|
||||
@Nullable String username,
|
||||
@Nullable IdentityState identityState,
|
||||
|
@ -303,15 +381,16 @@ public final class StorageSyncHelper {
|
|||
boolean profileSharing,
|
||||
@Nullable String nickname)
|
||||
{
|
||||
return Objects.equals(contact.getAddress(), address) &&
|
||||
Objects.equals(contact.getProfileName().orNull(), profileName) &&
|
||||
Arrays.equals(contact.getProfileKey().orNull(), profileKey) &&
|
||||
Objects.equals(contact.getUsername().orNull(), username) &&
|
||||
Objects.equals(contact.getIdentityState(), identityState) &&
|
||||
Arrays.equals(contact.getIdentityKey().orNull(), identityKey) &&
|
||||
contact.isBlocked() == blocked &&
|
||||
contact.isProfileSharingEnabled() == profileSharing &&
|
||||
Objects.equals(contact.getNickname().orNull(), nickname);
|
||||
return Objects.equals(contact.getAddress(), address) &&
|
||||
Objects.equals(contact.getGivenName().or(""), givenName) &&
|
||||
Objects.equals(contact.getFamilyName().or(""), familyName) &&
|
||||
Arrays.equals(contact.getProfileKey().orNull(), profileKey) &&
|
||||
Objects.equals(contact.getUsername().or(""), username) &&
|
||||
Objects.equals(contact.getIdentityState(), identityState) &&
|
||||
Arrays.equals(contact.getIdentityKey().orNull(), identityKey) &&
|
||||
contact.isBlocked() == blocked &&
|
||||
contact.isProfileSharingEnabled() == profileSharing &&
|
||||
Objects.equals(contact.getNickname().or(""), nickname);
|
||||
}
|
||||
|
||||
private static @NonNull ContactRecordMergeResult resolveContactConflict(@NonNull Collection<SignalContactRecord> remoteOnlyRecords,
|
||||
|
@ -359,6 +438,40 @@ public final class StorageSyncHelper {
|
|||
return new ContactRecordMergeResult(localInserts, localUpdates, remoteInserts, remoteUpdates);
|
||||
}
|
||||
|
||||
private static @NonNull GroupV1RecordMergeResult resolveGroupV1Conflict(@NonNull Collection<SignalGroupV1Record> remoteOnlyRecords,
|
||||
@NonNull Collection<SignalGroupV1Record> localOnlyRecords)
|
||||
{
|
||||
Map<String, SignalGroupV1Record> remoteByGroupId = Stream.of(remoteOnlyRecords).collect(Collectors.toMap(g -> GroupUtil.getEncodedId(g.getGroupId(), false), g -> g));
|
||||
Map<String, SignalGroupV1Record> localByGroupId = Stream.of(localOnlyRecords).collect(Collectors.toMap(g -> GroupUtil.getEncodedId(g.getGroupId(), false), g -> g));
|
||||
|
||||
Set<SignalGroupV1Record> localInserts = new LinkedHashSet<>(remoteOnlyRecords);
|
||||
Set<SignalGroupV1Record> remoteInserts = new LinkedHashSet<>(localOnlyRecords);
|
||||
Set<GroupV1Update> localUpdates = new LinkedHashSet<>();
|
||||
Set<GroupV1Update> remoteUpdates = new LinkedHashSet<>();
|
||||
|
||||
for (Map.Entry<String, SignalGroupV1Record> entry : remoteByGroupId.entrySet()) {
|
||||
SignalGroupV1Record remote = entry.getValue();
|
||||
SignalGroupV1Record local = localByGroupId.get(entry.getKey());
|
||||
|
||||
if (local != null) {
|
||||
SignalGroupV1Record merged = mergeGroupV1(remote, local);
|
||||
|
||||
if (!merged.equals(remote)) {
|
||||
remoteUpdates.add(new GroupV1Update(remote, merged));
|
||||
}
|
||||
|
||||
if (!merged.equals(local)) {
|
||||
localUpdates.add(new GroupV1Update(local, merged));
|
||||
}
|
||||
|
||||
localInserts.remove(remote);
|
||||
remoteInserts.remove(local);
|
||||
}
|
||||
}
|
||||
|
||||
return new GroupV1RecordMergeResult(localInserts, localUpdates, remoteInserts, remoteUpdates);
|
||||
}
|
||||
|
||||
public static final class ContactUpdate {
|
||||
private final SignalContactRecord oldContact;
|
||||
private final SignalContactRecord newContact;
|
||||
|
@ -368,13 +481,11 @@ public final class StorageSyncHelper {
|
|||
this.newContact = newContact;
|
||||
}
|
||||
|
||||
public @NonNull
|
||||
SignalContactRecord getOldContact() {
|
||||
public @NonNull SignalContactRecord getOld() {
|
||||
return oldContact;
|
||||
}
|
||||
|
||||
public @NonNull
|
||||
SignalContactRecord getNewContact() {
|
||||
public @NonNull SignalContactRecord getNew() {
|
||||
return newContact;
|
||||
}
|
||||
|
||||
|
@ -397,6 +508,72 @@ public final class StorageSyncHelper {
|
|||
}
|
||||
}
|
||||
|
||||
public static final class GroupV1Update {
|
||||
private final SignalGroupV1Record oldGroup;
|
||||
private final SignalGroupV1Record newGroup;
|
||||
|
||||
|
||||
public GroupV1Update(@NonNull SignalGroupV1Record oldGroup, @NonNull SignalGroupV1Record newGroup) {
|
||||
this.oldGroup = oldGroup;
|
||||
this.newGroup = newGroup;
|
||||
}
|
||||
|
||||
public @NonNull SignalGroupV1Record getOld() {
|
||||
return oldGroup;
|
||||
}
|
||||
|
||||
public @NonNull SignalGroupV1Record getNew() {
|
||||
return newGroup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
GroupV1Update that = (GroupV1Update) o;
|
||||
return oldGroup.equals(that.oldGroup) &&
|
||||
newGroup.equals(that.newGroup);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(oldGroup, newGroup);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static class RecordUpdate {
|
||||
private final SignalStorageRecord oldRecord;
|
||||
private final SignalStorageRecord newRecord;
|
||||
|
||||
RecordUpdate(@NonNull SignalStorageRecord oldRecord, @NonNull SignalStorageRecord newRecord) {
|
||||
this.oldRecord = oldRecord;
|
||||
this.newRecord = newRecord;
|
||||
}
|
||||
|
||||
public @NonNull SignalStorageRecord getOld() {
|
||||
return oldRecord;
|
||||
}
|
||||
|
||||
public @NonNull SignalStorageRecord getNew() {
|
||||
return newRecord;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
RecordUpdate that = (RecordUpdate) o;
|
||||
return oldRecord.equals(that.oldRecord) &&
|
||||
newRecord.equals(that.newRecord);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(oldRecord, newRecord);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class KeyDifferenceResult {
|
||||
private final List<byte[]> remoteOnlyKeys;
|
||||
private final List<byte[]> localOnlyKeys;
|
||||
|
@ -422,25 +599,31 @@ public final class StorageSyncHelper {
|
|||
public static final class MergeResult {
|
||||
private final Set<SignalContactRecord> localContactInserts;
|
||||
private final Set<ContactUpdate> localContactUpdates;
|
||||
private final Set<SignalContactRecord> remoteContactInserts;
|
||||
private final Set<ContactUpdate> remoteContactUpdates;
|
||||
private final Set<SignalGroupV1Record> localGroupV1Inserts;
|
||||
private final Set<GroupV1Update> localGroupV1Updates;
|
||||
private final Set<SignalStorageRecord> localUnknownInserts;
|
||||
private final Set<SignalStorageRecord> localUnknownDeletes;
|
||||
private final Set<SignalStorageRecord> remoteInserts;
|
||||
private final Set<RecordUpdate> remoteUpdates;
|
||||
|
||||
@VisibleForTesting
|
||||
MergeResult(@NonNull Set<SignalContactRecord> localContactInserts,
|
||||
@NonNull Set<ContactUpdate> localContactUpdates,
|
||||
@NonNull Set<SignalContactRecord> remoteContactInserts,
|
||||
@NonNull Set<ContactUpdate> remoteContactUpdates,
|
||||
@NonNull Set<ContactUpdate> localContactUpdates,
|
||||
@NonNull Set<SignalGroupV1Record> localGroupV1Inserts,
|
||||
@NonNull Set<GroupV1Update> localGroupV1Updates,
|
||||
@NonNull Set<SignalStorageRecord> localUnknownInserts,
|
||||
@NonNull Set<SignalStorageRecord> localUnknownDeletes)
|
||||
@NonNull Set<SignalStorageRecord> localUnknownDeletes,
|
||||
@NonNull Set<SignalStorageRecord> remoteInserts,
|
||||
@NonNull Set<RecordUpdate> remoteUpdates)
|
||||
{
|
||||
this.localContactInserts = localContactInserts;
|
||||
this.localContactUpdates = localContactUpdates;
|
||||
this.remoteContactInserts = remoteContactInserts;
|
||||
this.remoteContactUpdates = remoteContactUpdates;
|
||||
this.localGroupV1Inserts = localGroupV1Inserts;
|
||||
this.localGroupV1Updates = localGroupV1Updates;
|
||||
this.localUnknownInserts = localUnknownInserts;
|
||||
this.localUnknownDeletes = localUnknownDeletes;
|
||||
this.remoteInserts = remoteInserts;
|
||||
this.remoteUpdates = remoteUpdates;
|
||||
}
|
||||
|
||||
public @NonNull Set<SignalContactRecord> getLocalContactInserts() {
|
||||
|
@ -451,12 +634,12 @@ public final class StorageSyncHelper {
|
|||
return localContactUpdates;
|
||||
}
|
||||
|
||||
public @NonNull Set<SignalContactRecord> getRemoteContactInserts() {
|
||||
return remoteContactInserts;
|
||||
public @NonNull Set<SignalGroupV1Record> getLocalGroupV1Inserts() {
|
||||
return localGroupV1Inserts;
|
||||
}
|
||||
|
||||
public @NonNull Set<ContactUpdate> getRemoteContactUpdates() {
|
||||
return remoteContactUpdates;
|
||||
public @NonNull Set<GroupV1Update> getLocalGroupV1Updates() {
|
||||
return localGroupV1Updates;
|
||||
}
|
||||
|
||||
public @NonNull Set<SignalStorageRecord> getLocalUnknownInserts() {
|
||||
|
@ -466,6 +649,21 @@ public final class StorageSyncHelper {
|
|||
public @NonNull Set<SignalStorageRecord> getLocalUnknownDeletes() {
|
||||
return localUnknownDeletes;
|
||||
}
|
||||
|
||||
public @NonNull Set<SignalStorageRecord> getRemoteInserts() {
|
||||
return remoteInserts;
|
||||
}
|
||||
|
||||
public @NonNull Set<RecordUpdate> getRemoteUpdates() {
|
||||
return remoteUpdates;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return String.format(Locale.ENGLISH,
|
||||
"localContactInserts: %d, localContactUpdates: %d, localGroupInserts: %d, localGroupUpdates: %d, localUnknownInserts: %d, localUnknownDeletes: %d, remoteInserts: %d, remoteUpdates: %d",
|
||||
localContactInserts.size(), localContactUpdates.size(), localGroupV1Inserts.size(), localGroupV1Updates.size(), localUnknownInserts.size(), localUnknownDeletes.size(), remoteInserts.size(), remoteUpdates.size());
|
||||
}
|
||||
}
|
||||
|
||||
public static final class WriteOperationResult {
|
||||
|
@ -493,6 +691,20 @@ public final class StorageSyncHelper {
|
|||
public @NonNull List<byte[]> getDeletes() {
|
||||
return deletes;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return inserts.isEmpty() && deletes.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return String.format(Locale.ENGLISH,
|
||||
"ManifestVersion: %d, Total Keys: %d, Inserts: %d, Deletes: %d",
|
||||
manifest.getVersion(),
|
||||
manifest.getStorageKeys().size(),
|
||||
inserts.size(),
|
||||
deletes.size());
|
||||
}
|
||||
}
|
||||
|
||||
public static class LocalWriteResult {
|
||||
|
@ -531,6 +743,24 @@ public final class StorageSyncHelper {
|
|||
}
|
||||
}
|
||||
|
||||
private static final class GroupV1RecordMergeResult {
|
||||
final Set<SignalGroupV1Record> localInserts;
|
||||
final Set<GroupV1Update> localUpdates;
|
||||
final Set<SignalGroupV1Record> remoteInserts;
|
||||
final Set<GroupV1Update> remoteUpdates;
|
||||
|
||||
GroupV1RecordMergeResult(@NonNull Set<SignalGroupV1Record> localInserts,
|
||||
@NonNull Set<GroupV1Update> localUpdates,
|
||||
@NonNull Set<SignalGroupV1Record> remoteInserts,
|
||||
@NonNull Set<GroupV1Update> remoteUpdates)
|
||||
{
|
||||
this.localInserts = localInserts;
|
||||
this.localUpdates = localUpdates;
|
||||
this.remoteInserts = remoteInserts;
|
||||
this.remoteUpdates = remoteUpdates;
|
||||
}
|
||||
}
|
||||
|
||||
interface KeyGenerator {
|
||||
@NonNull byte[] generate();
|
||||
}
|
||||
|
|
|
@ -21,21 +21,24 @@ import org.thoughtcrime.securesms.contacts.sync.StorageSyncHelper;
|
|||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.io.Closeable;
|
||||
|
@ -62,6 +65,7 @@ public class RecipientDatabase extends Database {
|
|||
public static final String PHONE = "phone";
|
||||
public static final String EMAIL = "email";
|
||||
static final String GROUP_ID = "group_id";
|
||||
private static final String GROUP_TYPE = "group_type";
|
||||
private static final String BLOCKED = "blocked";
|
||||
private static final String MESSAGE_RINGTONE = "message_ringtone";
|
||||
private static final String MESSAGE_VIBRATE = "message_vibrate";
|
||||
|
@ -100,7 +104,7 @@ public class RecipientDatabase extends Database {
|
|||
|
||||
|
||||
private static final String[] RECIPIENT_PROJECTION = new String[] {
|
||||
UUID, USERNAME, PHONE, EMAIL, GROUP_ID,
|
||||
UUID, USERNAME, PHONE, EMAIL, GROUP_ID, GROUP_TYPE,
|
||||
BLOCKED, MESSAGE_RINGTONE, CALL_RINGTONE, MESSAGE_VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, MESSAGE_EXPIRATION_TIME, REGISTERED,
|
||||
PROFILE_KEY, PROFILE_KEY_CREDENTIAL,
|
||||
SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, SYSTEM_CONTACT_URI,
|
||||
|
@ -120,6 +124,7 @@ public class RecipientDatabase extends Database {
|
|||
|
||||
public static final String[] CREATE_INDEXS = new String[] {
|
||||
"CREATE INDEX IF NOT EXISTS recipient_dirty_index ON " + TABLE_NAME + " (" + DIRTY + ");",
|
||||
"CREATE INDEX IF NOT EXISTS recipient_group_type_index ON " + TABLE_NAME + " (" + GROUP_TYPE + ");",
|
||||
};
|
||||
|
||||
private static final String[] ID_PROJECTION = new String[]{ID};
|
||||
|
@ -219,6 +224,24 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
public enum GroupType {
|
||||
NONE(0), MMS(1), SIGNAL_V1(2);
|
||||
|
||||
private final int id;
|
||||
|
||||
GroupType(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public static GroupType fromId(int id) {
|
||||
return values()[id];
|
||||
}
|
||||
}
|
||||
|
||||
public static final String CREATE_TABLE =
|
||||
"CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
UUID + " TEXT UNIQUE DEFAULT NULL, " +
|
||||
|
@ -226,6 +249,7 @@ public class RecipientDatabase extends Database {
|
|||
PHONE + " TEXT UNIQUE DEFAULT NULL, " +
|
||||
EMAIL + " TEXT UNIQUE DEFAULT NULL, " +
|
||||
GROUP_ID + " TEXT UNIQUE DEFAULT NULL, " +
|
||||
GROUP_TYPE + " INTEGER DEFAULT " + GroupType.NONE.getId() + ", " +
|
||||
BLOCKED + " INTEGER DEFAULT 0," +
|
||||
MESSAGE_RINGTONE + " TEXT DEFAULT NULL, " +
|
||||
MESSAGE_VIBRATE + " INTEGER DEFAULT " + VibrateState.DEFAULT.getId() + ", " +
|
||||
|
@ -255,7 +279,7 @@ public class RecipientDatabase extends Database {
|
|||
FORCE_SMS_SELECTION + " INTEGER DEFAULT 0, " +
|
||||
UUID_SUPPORTED + " INTEGER DEFAULT 0, " +
|
||||
STORAGE_SERVICE_KEY + " TEXT UNIQUE DEFAULT NULL, " +
|
||||
DIRTY + " INTEGER DEFAULT 0);";
|
||||
DIRTY + " INTEGER DEFAULT " + DirtyState.CLEAN.getId() + ");";
|
||||
|
||||
private static final String INSIGHTS_INVITEE_LIST = "SELECT " + TABLE_NAME + "." + ID +
|
||||
" FROM " + TABLE_NAME +
|
||||
|
@ -305,19 +329,35 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
|
||||
public @NonNull RecipientId getOrInsertFromUuid(@NonNull UUID uuid) {
|
||||
return getOrInsertByColumn(UUID, uuid.toString());
|
||||
return getOrInsertByColumn(UUID, uuid.toString()).recipientId;
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getOrInsertFromE164(@NonNull String e164) {
|
||||
return getOrInsertByColumn(PHONE, e164);
|
||||
return getOrInsertByColumn(PHONE, e164).recipientId;
|
||||
}
|
||||
|
||||
public RecipientId getOrInsertFromEmail(@NonNull String email) {
|
||||
return getOrInsertByColumn(EMAIL, email);
|
||||
public @NonNull RecipientId getOrInsertFromEmail(@NonNull String email) {
|
||||
return getOrInsertByColumn(EMAIL, email).recipientId;
|
||||
}
|
||||
|
||||
public RecipientId getOrInsertFromGroupId(@NonNull String groupId) {
|
||||
return getOrInsertByColumn(GROUP_ID, groupId);
|
||||
public @NonNull RecipientId getOrInsertFromGroupId(@NonNull String groupId) {
|
||||
GetOrInsertResult result = getOrInsertByColumn(GROUP_ID, groupId);
|
||||
|
||||
if (result.neededInsert) {
|
||||
ContentValues values = new ContentValues();
|
||||
|
||||
if (GroupUtil.isMmsGroup(groupId)) {
|
||||
values.put(GROUP_TYPE, GroupType.MMS.getId());
|
||||
} else {
|
||||
values.put(GROUP_TYPE, GroupType.SIGNAL_V1.getId());
|
||||
values.put(DIRTY, DirtyState.INSERT.getId());
|
||||
values.put(STORAGE_SERVICE_KEY, Base64.encodeBytes(StorageSyncHelper.generateKey()));
|
||||
}
|
||||
|
||||
update(result.recipientId, values);
|
||||
}
|
||||
|
||||
return result.recipientId;
|
||||
}
|
||||
|
||||
public Cursor getBlocked() {
|
||||
|
@ -355,15 +395,24 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
|
||||
public @NonNull List<RecipientSettings> getPendingRecipientSyncUpdates() {
|
||||
return getRecipientSettings(DIRTY + " = ?", new String[] { String.valueOf(DirtyState.UPDATE.getId()) });
|
||||
String query = DIRTY + " = ? AND " + STORAGE_SERVICE_KEY + " NOT NULL";
|
||||
String[] args = new String[] { String.valueOf(DirtyState.UPDATE.getId()) };
|
||||
|
||||
return getRecipientSettings(query, args);
|
||||
}
|
||||
|
||||
public @NonNull List<RecipientSettings> getPendingRecipientSyncInsertions() {
|
||||
return getRecipientSettings(DIRTY + " = ?", new String[] { String.valueOf(DirtyState.INSERT.getId()) });
|
||||
String query = DIRTY + " = ? AND " + STORAGE_SERVICE_KEY + " NOT NULL";
|
||||
String[] args = new String[] { String.valueOf(DirtyState.INSERT.getId()) };
|
||||
|
||||
return getRecipientSettings(query, args);
|
||||
}
|
||||
|
||||
public @NonNull List<RecipientSettings> getPendingRecipientSyncDeletions() {
|
||||
return getRecipientSettings(DIRTY + " = ?", new String[] { String.valueOf(DirtyState.DELETE.getId()) });
|
||||
String query = DIRTY + " = ? AND " + STORAGE_SERVICE_KEY + " NOT NULL";
|
||||
String[] args = new String[] { String.valueOf(DirtyState.DELETE.getId()) };
|
||||
|
||||
return getRecipientSettings(query, args);
|
||||
}
|
||||
|
||||
public @Nullable RecipientSettings getByStorageSyncKey(@NonNull byte[] key) {
|
||||
|
@ -396,8 +445,10 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
public void applyStorageSyncUpdates(@NonNull Collection<SignalContactRecord> inserts,
|
||||
@NonNull Collection<StorageSyncHelper.ContactUpdate> updates)
|
||||
public void applyStorageSyncUpdates(@NonNull Collection<SignalContactRecord> contactInserts,
|
||||
@NonNull Collection<StorageSyncHelper.ContactUpdate> contactUpdates,
|
||||
@NonNull Collection<SignalGroupV1Record> groupV1Inserts,
|
||||
@NonNull Collection<StorageSyncHelper.GroupV1Update> groupV1Updates)
|
||||
{
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
|
||||
|
@ -405,7 +456,7 @@ public class RecipientDatabase extends Database {
|
|||
db.beginTransaction();
|
||||
|
||||
try {
|
||||
for (SignalContactRecord insert : inserts) {
|
||||
for (SignalContactRecord insert : contactInserts) {
|
||||
ContentValues values = getValuesForStorageContact(insert);
|
||||
long id = db.insertOrThrow(TABLE_NAME, null, values);
|
||||
RecipientId recipientId = RecipientId.from(id);
|
||||
|
@ -420,17 +471,21 @@ public class RecipientDatabase extends Database {
|
|||
Log.w(TAG, "Failed to process identity key during insert! Skipping.", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (Recipient.self().getId().equals(recipientId)) {
|
||||
TextSecurePreferences.setProfileName(context, ProfileName.fromParts(insert.getGivenName().orNull(), insert.getFamilyName().orNull()));
|
||||
}
|
||||
}
|
||||
|
||||
for (StorageSyncHelper.ContactUpdate update : updates) {
|
||||
ContentValues values = getValuesForStorageContact(update.getNewContact());
|
||||
int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_KEY + " = ?", new String[]{Base64.encodeBytes(update.getOldContact().getKey())});
|
||||
for (StorageSyncHelper.ContactUpdate update : contactUpdates) {
|
||||
ContentValues values = getValuesForStorageContact(update.getNew());
|
||||
int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_KEY + " = ?", new String[]{Base64.encodeBytes(update.getOld().getKey())});
|
||||
|
||||
if (updateCount < 1) {
|
||||
throw new AssertionError("Had an update, but it didn't match any rows!");
|
||||
}
|
||||
|
||||
RecipientId recipientId = getByStorageKeyOrThrow(update.getNewContact().getKey());
|
||||
RecipientId recipientId = getByStorageKeyOrThrow(update.getNew().getKey());
|
||||
|
||||
if (update.profileKeyChanged()) {
|
||||
clearProfileKeyCredential(recipientId);
|
||||
|
@ -438,9 +493,11 @@ public class RecipientDatabase extends Database {
|
|||
|
||||
try {
|
||||
Optional<IdentityRecord> oldIdentityRecord = identityDatabase.getIdentity(recipientId);
|
||||
IdentityKey identityKey = update.getNewContact().getIdentityKey().isPresent() ? new IdentityKey(update.getNewContact().getIdentityKey().get(), 0) : null;
|
||||
|
||||
DatabaseFactory.getIdentityDatabase(context).updateIdentityAfterSync(recipientId, identityKey, StorageSyncHelper.remoteToLocalIdentityStatus(update.getNewContact().getIdentityState()));
|
||||
if (update.getNew().getIdentityKey().isPresent()) {
|
||||
IdentityKey identityKey = new IdentityKey(update.getNew().getIdentityKey().get(), 0);
|
||||
DatabaseFactory.getIdentityDatabase(context).updateIdentityAfterSync(recipientId, identityKey, StorageSyncHelper.remoteToLocalIdentityStatus(update.getNew().getIdentityState()));
|
||||
}
|
||||
|
||||
Optional<IdentityRecord> newIdentityRecord = identityDatabase.getIdentity(recipientId);
|
||||
|
||||
|
@ -458,6 +515,19 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
for (SignalGroupV1Record insert : groupV1Inserts) {
|
||||
db.insertOrThrow(TABLE_NAME, null, getValuesForStorageGroupV1(insert));
|
||||
}
|
||||
|
||||
for (StorageSyncHelper.GroupV1Update update : groupV1Updates) {
|
||||
ContentValues values = getValuesForStorageGroupV1(update.getNew());
|
||||
int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_KEY + " = ?", new String[]{Base64.encodeBytes(update.getOld().getKey())});
|
||||
|
||||
if (updateCount < 1) {
|
||||
throw new AssertionError("Had an update, but it didn't match any rows!");
|
||||
}
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
@ -508,14 +578,14 @@ public class RecipientDatabase extends Database {
|
|||
values.put(UUID, contact.getAddress().getUuid().get().toString());
|
||||
}
|
||||
|
||||
ProfileName profileName = ProfileName.fromSerialized(contact.getProfileName().orNull());
|
||||
ProfileName profileName = ProfileName.fromParts(contact.getGivenName().orNull(), contact.getFamilyName().orNull());
|
||||
|
||||
values.put(PHONE, contact.getAddress().getNumber().orNull());
|
||||
values.put(PROFILE_GIVEN_NAME, profileName.getGivenName());
|
||||
values.put(PROFILE_FAMILY_NAME, profileName.getFamilyName());
|
||||
values.put(PROFILE_JOINED_NAME, profileName.toString());
|
||||
values.put(PROFILE_KEY, contact.getProfileKey().orNull());
|
||||
// TODO [greyson] Username
|
||||
values.put(PROFILE_KEY, contact.getProfileKey().transform(Base64::encodeBytes).orNull());
|
||||
values.put(USERNAME, contact.getUsername().orNull());
|
||||
values.put(PROFILE_SHARING, contact.isProfileSharingEnabled() ? "1" : "0");
|
||||
values.put(BLOCKED, contact.isBlocked() ? "1" : "0");
|
||||
values.put(STORAGE_SERVICE_KEY, Base64.encodeBytes(contact.getKey()));
|
||||
|
@ -523,6 +593,17 @@ public class RecipientDatabase extends Database {
|
|||
return values;
|
||||
}
|
||||
|
||||
private static @NonNull ContentValues getValuesForStorageGroupV1(@NonNull SignalGroupV1Record groupV1) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(GROUP_ID, GroupUtil.getEncodedId(groupV1.getGroupId(), false));
|
||||
values.put(GROUP_TYPE, GroupType.SIGNAL_V1.getId());
|
||||
values.put(PROFILE_SHARING, groupV1.isProfileSharingEnabled() ? "1" : "0");
|
||||
values.put(BLOCKED, groupV1.isBlocked() ? "1" : "0");
|
||||
values.put(STORAGE_SERVICE_KEY, Base64.encodeBytes(groupV1.getKey()));
|
||||
values.put(DIRTY, DirtyState.CLEAN.getId());
|
||||
return values;
|
||||
}
|
||||
|
||||
private List<RecipientSettings> getRecipientSettings(@Nullable String query, @Nullable String[] args) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String table = TABLE_NAME + " LEFT OUTER JOIN " + IdentityDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ID + " = " + IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.RECIPIENT_ID;
|
||||
|
@ -555,27 +636,24 @@ public class RecipientDatabase extends Database {
|
|||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, new String[] { ID, STORAGE_SERVICE_KEY }, query, args, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
RecipientId id = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID)));
|
||||
RecipientId id = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID)));
|
||||
String encodedKey = cursor.getString(cursor.getColumnIndexOrThrow(STORAGE_SERVICE_KEY));
|
||||
|
||||
try {
|
||||
out.put(id, Base64.decode(encodedKey));
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
out.put(id, Base64.decodeOrThrow(encodedKey));
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@NonNull RecipientSettings getRecipientSettings(@NonNull Cursor cursor) {
|
||||
private @NonNull RecipientSettings getRecipientSettings(@NonNull Cursor cursor) {
|
||||
long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
|
||||
UUID uuid = UuidUtil.parseOrNull(cursor.getString(cursor.getColumnIndexOrThrow(UUID)));
|
||||
String username = cursor.getString(cursor.getColumnIndexOrThrow(USERNAME));
|
||||
String e164 = cursor.getString(cursor.getColumnIndexOrThrow(PHONE));
|
||||
String email = cursor.getString(cursor.getColumnIndexOrThrow(EMAIL));
|
||||
String groupId = cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID));
|
||||
int groupType = cursor.getInt(cursor.getColumnIndexOrThrow(GROUP_TYPE));
|
||||
boolean blocked = cursor.getInt(cursor.getColumnIndexOrThrow(BLOCKED)) == 1;
|
||||
String messageRingtone = cursor.getString(cursor.getColumnIndexOrThrow(MESSAGE_RINGTONE));
|
||||
String callRingtone = cursor.getString(cursor.getColumnIndexOrThrow(CALL_RINGTONE));
|
||||
|
@ -634,23 +712,12 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
byte[] storageKey = null;
|
||||
try {
|
||||
storageKey = storageKeyRaw != null ? Base64.decode(storageKeyRaw) : null;
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
byte[] identityKey = null;
|
||||
try {
|
||||
identityKey = identityKeyRaw != null ? Base64.decode(identityKeyRaw) : null;
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
byte[] storageKey = storageKeyRaw != null ? Base64.decodeOrThrow(storageKeyRaw) : null;
|
||||
byte[] identityKey = identityKeyRaw != null ? Base64.decodeOrThrow(identityKeyRaw) : null;
|
||||
|
||||
IdentityDatabase.VerifiedStatus identityStatus = IdentityDatabase.VerifiedStatus.forState(identityStatusRaw);
|
||||
|
||||
return new RecipientSettings(RecipientId.from(id), uuid, username, e164, email, groupId, blocked, muteUntil,
|
||||
return new RecipientSettings(RecipientId.from(id), uuid, username, e164, email, groupId, GroupType.fromId(groupType), blocked, muteUntil,
|
||||
VibrateState.fromId(messageVibrateState),
|
||||
VibrateState.fromId(callVibrateState),
|
||||
Util.uri(messageRingtone), Util.uri(callRingtone),
|
||||
|
@ -820,6 +887,7 @@ public class RecipientDatabase extends Database {
|
|||
if (update(updateQuery, valuesToSet)) {
|
||||
markDirty(id, DirtyState.UPDATE);
|
||||
Recipient.live(id).refresh();
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
@ -865,6 +933,7 @@ public class RecipientDatabase extends Database {
|
|||
if (update(id, contentValues)) {
|
||||
markDirty(id, DirtyState.UPDATE);
|
||||
Recipient.live(id).refresh();
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -882,6 +951,7 @@ public class RecipientDatabase extends Database {
|
|||
if (update(id, contentValues)) {
|
||||
markDirty(id, DirtyState.UPDATE);
|
||||
Recipient.live(id).refresh();
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -947,7 +1017,6 @@ public class RecipientDatabase extends Database {
|
|||
ContentValues contentValues = new ContentValues(3);
|
||||
contentValues.put(REGISTERED, RegisteredState.REGISTERED.getId());
|
||||
contentValues.put(UUID, uuid.toString().toLowerCase());
|
||||
contentValues.put(STORAGE_SERVICE_KEY, Base64.encodeBytes(StorageSyncHelper.generateKey()));
|
||||
if (update(id, contentValues)) {
|
||||
markDirty(id, DirtyState.INSERT);
|
||||
Recipient.live(id).refresh();
|
||||
|
@ -962,7 +1031,6 @@ public class RecipientDatabase extends Database {
|
|||
public void markRegistered(@NonNull RecipientId id) {
|
||||
ContentValues contentValues = new ContentValues(2);
|
||||
contentValues.put(REGISTERED, RegisteredState.REGISTERED.getId());
|
||||
contentValues.put(STORAGE_SERVICE_KEY, Base64.encodeBytes(StorageSyncHelper.generateKey()));
|
||||
if (update(id, contentValues)) {
|
||||
markDirty(id, DirtyState.INSERT);
|
||||
Recipient.live(id).refresh();
|
||||
|
@ -1010,10 +1078,22 @@ public class RecipientDatabase extends Database {
|
|||
|
||||
@Deprecated
|
||||
public void setRegistered(@NonNull RecipientId id, RegisteredState registeredState) {
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
ContentValues contentValues = new ContentValues(2);
|
||||
contentValues.put(REGISTERED, registeredState.getId());
|
||||
update(id, contentValues);
|
||||
Recipient.live(id).refresh();
|
||||
|
||||
if (registeredState == RegisteredState.REGISTERED) {
|
||||
contentValues.put(STORAGE_SERVICE_KEY, Base64.encodeBytes(StorageSyncHelper.generateKey()));
|
||||
}
|
||||
|
||||
if (update(id, contentValues)) {
|
||||
if (registeredState == RegisteredState.REGISTERED) {
|
||||
markDirty(id, DirtyState.INSERT);
|
||||
} else if (registeredState == RegisteredState.NOT_REGISTERED) {
|
||||
markDirty(id, DirtyState.DELETE);
|
||||
}
|
||||
|
||||
Recipient.live(id).refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
|
@ -1021,10 +1101,11 @@ public class RecipientDatabase extends Database {
|
|||
@NonNull Collection<RecipientId> inactiveIds)
|
||||
{
|
||||
for (RecipientId activeId : activeIds) {
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
contentValues.put(REGISTERED, RegisteredState.REGISTERED.getId());
|
||||
ContentValues registeredValues = new ContentValues(1);
|
||||
registeredValues.put(REGISTERED, RegisteredState.REGISTERED.getId());
|
||||
|
||||
if (update(activeId, contentValues)) {
|
||||
if (update(activeId, registeredValues)) {
|
||||
markDirty(activeId, DirtyState.INSERT);
|
||||
Recipient.live(activeId).refresh();
|
||||
}
|
||||
}
|
||||
|
@ -1034,6 +1115,7 @@ public class RecipientDatabase extends Database {
|
|||
contentValues.put(REGISTERED, RegisteredState.NOT_REGISTERED.getId());
|
||||
|
||||
if (update(inactiveId, contentValues)) {
|
||||
markDirty(inactiveId, DirtyState.DELETE);
|
||||
Recipient.live(inactiveId).refresh();
|
||||
}
|
||||
}
|
||||
|
@ -1282,14 +1364,27 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
|
||||
void markDirty(@NonNull RecipientId recipientId, @NonNull DirtyState dirtyState) {
|
||||
if (!FeatureFlags.storageService()) return;
|
||||
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
contentValues.put(DIRTY, dirtyState.getId());
|
||||
|
||||
String query = ID + " = ? AND (" + UUID + " NOT NULL OR " + PHONE + " NOT NULL) AND " + DIRTY + " < ?";
|
||||
String query = ID + " = ? AND (" + UUID + " NOT NULL OR " + PHONE + " NOT NULL) AND ";
|
||||
String[] args = new String[] { recipientId.serialize(), String.valueOf(dirtyState.id) };
|
||||
|
||||
switch (dirtyState) {
|
||||
case INSERT:
|
||||
query += "(" + DIRTY + " < ? OR " + DIRTY + " = ?)";
|
||||
args = SqlUtil.appendArg(args, String.valueOf(DirtyState.DELETE.getId()));
|
||||
|
||||
contentValues.put(STORAGE_SERVICE_KEY, Base64.encodeBytes(StorageSyncHelper.generateKey()));
|
||||
break;
|
||||
case DELETE:
|
||||
query += "(" + DIRTY + " < ? OR " + DIRTY + " = ?)";
|
||||
args = SqlUtil.appendArg(args, String.valueOf(DirtyState.INSERT.getId()));
|
||||
break;
|
||||
default:
|
||||
query += DIRTY + " < ?";
|
||||
}
|
||||
|
||||
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, query, args);
|
||||
}
|
||||
|
||||
|
@ -1330,7 +1425,7 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
private @NonNull RecipientId getOrInsertByColumn(@NonNull String column, String value) {
|
||||
private @NonNull GetOrInsertResult getOrInsertByColumn(@NonNull String column, String value) {
|
||||
if (TextUtils.isEmpty(value)) {
|
||||
throw new AssertionError(column + " cannot be empty.");
|
||||
}
|
||||
|
@ -1338,7 +1433,7 @@ public class RecipientDatabase extends Database {
|
|||
Optional<RecipientId> existing = getByColumn(column, value);
|
||||
|
||||
if (existing.isPresent()) {
|
||||
return existing.get();
|
||||
return new GetOrInsertResult(existing.get(), false);
|
||||
} else {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(column, value);
|
||||
|
@ -1349,12 +1444,12 @@ public class RecipientDatabase extends Database {
|
|||
existing = getByColumn(column, value);
|
||||
|
||||
if (existing.isPresent()) {
|
||||
return existing.get();
|
||||
return new GetOrInsertResult(existing.get(), false);
|
||||
} else {
|
||||
throw new AssertionError("Failed to insert recipient!");
|
||||
}
|
||||
} else {
|
||||
return RecipientId.from(id);
|
||||
return new GetOrInsertResult(RecipientId.from(id), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1445,6 +1540,7 @@ public class RecipientDatabase extends Database {
|
|||
private final String e164;
|
||||
private final String email;
|
||||
private final String groupId;
|
||||
private final GroupType groupType;
|
||||
private final boolean blocked;
|
||||
private final long muteUntil;
|
||||
private final VibrateState messageVibrateState;
|
||||
|
@ -1479,7 +1575,9 @@ public class RecipientDatabase extends Database {
|
|||
@Nullable String e164,
|
||||
@Nullable String email,
|
||||
@Nullable String groupId,
|
||||
boolean blocked, long muteUntil,
|
||||
@NonNull GroupType groupType,
|
||||
boolean blocked,
|
||||
long muteUntil,
|
||||
@NonNull VibrateState messageVibrateState,
|
||||
@NonNull VibrateState callVibrateState,
|
||||
@Nullable Uri messageRingtone,
|
||||
|
@ -1512,6 +1610,7 @@ public class RecipientDatabase extends Database {
|
|||
this.e164 = e164;
|
||||
this.email = email;
|
||||
this.groupId = groupId;
|
||||
this.groupType = groupType;
|
||||
this.blocked = blocked;
|
||||
this.muteUntil = muteUntil;
|
||||
this.messageVibrateState = messageVibrateState;
|
||||
|
@ -1565,6 +1664,10 @@ public class RecipientDatabase extends Database {
|
|||
return groupId;
|
||||
}
|
||||
|
||||
public @NonNull GroupType getGroupType() {
|
||||
return groupType;
|
||||
}
|
||||
|
||||
public @Nullable MaterialColor getColor() {
|
||||
return color;
|
||||
}
|
||||
|
@ -1720,4 +1823,14 @@ public class RecipientDatabase extends Database {
|
|||
super("Failed to find recipient with ID: " + id);
|
||||
}
|
||||
}
|
||||
|
||||
private static class GetOrInsertResult {
|
||||
final RecipientId recipientId;
|
||||
final boolean neededInsert;
|
||||
|
||||
private GetOrInsertResult(@NonNull RecipientId recipientId, boolean neededInsert) {
|
||||
this.recipientId = recipientId;
|
||||
this.neededInsert = neededInsert;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import net.sqlcipher.database.SQLiteDatabase;
|
|||
import net.sqlcipher.database.SQLiteDatabaseHook;
|
||||
import net.sqlcipher.database.SQLiteOpenHelper;
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.sync.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
|
@ -111,8 +112,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||
private static final int PROFILE_KEY_TO_DB = 47;
|
||||
private static final int PROFILE_KEY_CREDENTIALS = 48;
|
||||
private static final int ATTACHMENT_FILE_INDEX = 49;
|
||||
private static final int STORAGE_SERVICE_ACTIVE = 50;
|
||||
|
||||
private static final int DATABASE_VERSION = 49;
|
||||
private static final int DATABASE_VERSION = 50;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
|
@ -673,20 +675,6 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||
|
||||
db.execSQL("CREATE UNIQUE INDEX recipient_storage_service_key ON recipient (storage_service_key)");
|
||||
db.execSQL("CREATE INDEX recipient_dirty_index ON recipient (dirty)");
|
||||
|
||||
// TODO [greyson] Do this in a future DB migration
|
||||
// db.execSQL("UPDATE recipient SET dirty = 2 WHERE registered = 1");
|
||||
//
|
||||
// try (Cursor cursor = db.rawQuery("SELECT _id FROM recipient WHERE registered = 1", null)) {
|
||||
// while (cursor != null && cursor.moveToNext()) {
|
||||
// String id = cursor.getString(cursor.getColumnIndexOrThrow("_id"));
|
||||
// ContentValues values = new ContentValues(1);
|
||||
//
|
||||
// values.put("storage_service_key", Base64.encodeBytes(StorageSyncHelper.generateKey()));
|
||||
//
|
||||
// db.update("recipient", values, "_id = ?", new String[] { id });
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
if (oldVersion < REACTIONS_UNREAD_INDEX) {
|
||||
|
@ -753,6 +741,26 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||
db.execSQL("CREATE INDEX IF NOT EXISTS part_data_index ON part (_data)");
|
||||
}
|
||||
|
||||
if (oldVersion < STORAGE_SERVICE_ACTIVE) {
|
||||
db.execSQL("ALTER TABLE recipient ADD COLUMN group_type INTEGER DEFAULT 0");
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS recipient_group_type_index ON recipient (group_type)");
|
||||
|
||||
db.execSQL("UPDATE recipient set group_type = 1 WHERE group_id NOT NULL AND group_id LIKE '__signal_mms_group__%'");
|
||||
db.execSQL("UPDATE recipient set group_type = 2 WHERE group_id NOT NULL AND group_id LIKE '__textsecure_group__%'");
|
||||
|
||||
try (Cursor cursor = db.rawQuery("SELECT _id FROM recipient WHERE registered = 1 or group_type = 2", null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String id = cursor.getString(cursor.getColumnIndexOrThrow("_id"));
|
||||
ContentValues values = new ContentValues(1);
|
||||
|
||||
values.put("dirty", 2);
|
||||
values.put("storage_service_key", Base64.encodeBytes(StorageSyncHelper.generateKey()));
|
||||
|
||||
db.update("recipient", values, "_id = ?", new String[] { id });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
|
|
@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage;
|
|||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.Debouncer;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
@ -23,9 +24,12 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* Allows the scheduling of durable jobs that will be run as early as possible.
|
||||
|
@ -159,6 +163,43 @@ public class JobManager implements ConstraintObserver.Notifier {
|
|||
executor.execute(() -> jobController.cancelJob(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the specified job synchronously. Beware: All normal dependencies are respected, meaning
|
||||
* you must take great care where you call this. It could take a very long time to complete!
|
||||
*
|
||||
* @return If the job completed, this will contain its completion state. If it timed out or
|
||||
* otherwise didn't complete, this will be absent.
|
||||
*/
|
||||
@WorkerThread
|
||||
public Optional<JobTracker.JobState> runSynchronously(@NonNull Job job, long timeout) {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicReference<JobTracker.JobState> resultState = new AtomicReference<>();
|
||||
|
||||
addListener(job.getId(), new JobTracker.JobListener() {
|
||||
@Override
|
||||
public void onStateChanged(@NonNull JobTracker.JobState jobState) {
|
||||
if (jobState.isComplete()) {
|
||||
removeListener(this);
|
||||
resultState.set(jobState);
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
add(job);
|
||||
|
||||
try {
|
||||
if (!latch.await(timeout, TimeUnit.MILLISECONDS)) {
|
||||
return Optional.absent();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Log.w(TAG, "Interrupted during runSynchronously()", e);
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
return Optional.fromNullable(resultState.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a string representing the state of the job queue. Intended for debugging.
|
||||
*/
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.migrations.RecipientSearchMigrationJob;
|
|||
import org.thoughtcrime.securesms.migrations.RegistrationPinV2MigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.StickerAdditionMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.StickerLaunchMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.StorageServiceMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.UuidMigrationJob;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
@ -116,6 +117,7 @@ public final class JobManagerFactories {
|
|||
put(RegistrationPinV2MigrationJob.KEY, new RegistrationPinV2MigrationJob.Factory());
|
||||
put(StickerLaunchMigrationJob.KEY, new StickerLaunchMigrationJob.Factory());
|
||||
put(StickerAdditionMigrationJob.KEY, new StickerAdditionMigrationJob.Factory());
|
||||
put(StorageServiceMigrationJob.KEY, new StorageServiceMigrationJob.Factory());
|
||||
put(UuidMigrationJob.KEY, new UuidMigrationJob.Factory());
|
||||
|
||||
// Dead jobs
|
||||
|
|
|
@ -15,6 +15,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
|||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.KeysMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
|
@ -58,15 +59,8 @@ public class MultiDeviceKeysUpdateJob extends BaseJob {
|
|||
return;
|
||||
}
|
||||
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
|
||||
MasterKey masterKey = SignalStore.kbsValues().getPinBackedMasterKey();
|
||||
byte[] storageServiceKey = masterKey != null ? masterKey.deriveStorageServiceKey()
|
||||
: null;
|
||||
|
||||
if (storageServiceKey == null) {
|
||||
Log.w(TAG, "Syncing a null storage service key.");
|
||||
}
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
StorageKey storageServiceKey = SignalStore.storageServiceValues().getOrCreateStorageMasterKey().deriveStorageServiceKey();
|
||||
|
||||
messageSender.sendMessage(SignalServiceSyncMessage.forKeys(new KeysMessage(Optional.fromNullable(storageServiceKey))),
|
||||
UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
|
|
|
@ -654,10 +654,17 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
}
|
||||
|
||||
private static void handleSynchronizeFetchMessage(@NonNull SignalServiceSyncMessage.FetchType fetchType) {
|
||||
if (fetchType == SignalServiceSyncMessage.FetchType.LOCAL_PROFILE) {
|
||||
ApplicationDependencies.getJobManager().add(new RefreshOwnProfileJob());
|
||||
} else {
|
||||
Log.w(TAG, "Received a fetch message for an unknown type.");
|
||||
Log.i(TAG, "Received fetch request with type: " + fetchType);
|
||||
|
||||
switch (fetchType) {
|
||||
case LOCAL_PROFILE:
|
||||
ApplicationDependencies.getJobManager().add(new RefreshOwnProfileJob());
|
||||
break;
|
||||
case STORAGE_MANIFEST:
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
break;
|
||||
default:
|
||||
Log.w(TAG, "Received a fetch message for an unknown type.");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -777,7 +784,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
}
|
||||
|
||||
if (message.isKeysRequest()) {
|
||||
// ApplicationDependencies.getJobManager().add(new );
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceKeysUpdateJob());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,12 +16,12 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
|||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||
|
@ -30,12 +30,14 @@ import java.io.IOException;
|
|||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Forces remote storage to match our local state. This should only be done after a key change or
|
||||
* when we detect that the remote data is badly-encrypted.
|
||||
* Forces remote storage to match our local state. This should only be done when we detect that the
|
||||
* remote data is badly-encrypted (which should only happen after re-registering without a PIN).
|
||||
*/
|
||||
public class StorageForcePushJob extends BaseJob {
|
||||
|
||||
|
@ -45,10 +47,10 @@ public class StorageForcePushJob extends BaseJob {
|
|||
|
||||
public StorageForcePushJob() {
|
||||
this(new Parameters.Builder().addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue(StorageSyncJob.QUEUE_KEY)
|
||||
.setMaxInstances(1)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.build());
|
||||
.setQueue(StorageSyncJob.QUEUE_KEY)
|
||||
.setMaxInstances(1)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.build());
|
||||
}
|
||||
|
||||
private StorageForcePushJob(@NonNull Parameters parameters) {
|
||||
|
@ -67,45 +69,42 @@ public class StorageForcePushJob extends BaseJob {
|
|||
|
||||
@Override
|
||||
protected void onRun() throws IOException, RetryLaterException {
|
||||
if (!FeatureFlags.storageService()) throw new AssertionError();
|
||||
|
||||
MasterKey kbsMasterKey = SignalStore.kbsValues().getPinBackedMasterKey();
|
||||
|
||||
if (kbsMasterKey == null) {
|
||||
Log.w(TAG, "No KBS master key is set! Must abort.");
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] storageServiceKey = kbsMasterKey.deriveStorageServiceKey();
|
||||
StorageKey storageServiceKey = SignalStore.storageServiceValues().getOrCreateStorageMasterKey().deriveStorageServiceKey();
|
||||
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
StorageKeyDatabase storageKeyDatabase = DatabaseFactory.getStorageKeyDatabase(context);
|
||||
|
||||
long currentVersion = accountManager.getStorageManifestVersion();
|
||||
Map<RecipientId, byte[]> oldContactKeys = recipientDatabase.getAllStorageSyncKeysMap();
|
||||
List<byte[]> oldUnknownKeys = storageKeyDatabase.getAllKeys();
|
||||
Map<RecipientId, byte[]> oldStorageKeys = recipientDatabase.getAllStorageSyncKeysMap();
|
||||
|
||||
long newVersion = currentVersion + 1;
|
||||
Map<RecipientId, byte[]> newContactKeys = generateNewKeys(oldContactKeys);
|
||||
List<byte[]> keysToDelete = Util.concatenatedList(new ArrayList<>(oldContactKeys.values()), oldUnknownKeys);
|
||||
List<SignalStorageRecord> inserts = Stream.of(oldContactKeys.keySet())
|
||||
.map(recipientDatabase::getRecipientSettings)
|
||||
.withoutNulls()
|
||||
.map(StorageSyncHelper::localToRemoteContact)
|
||||
.map(r -> SignalStorageRecord.forContact(r.getKey(), r))
|
||||
.toList();
|
||||
|
||||
SignalStorageManifest manifest = new SignalStorageManifest(newVersion, new ArrayList<>(newContactKeys.values()));
|
||||
|
||||
try {
|
||||
accountManager.writeStorageRecords(storageServiceKey, manifest, inserts, keysToDelete);
|
||||
} catch (InvalidKeyException e) {
|
||||
Log.w(TAG, "Hit an invalid key exception, which likely indicates a conflict.");
|
||||
throw new RetryLaterException();
|
||||
if (currentVersion < 1) {
|
||||
throw new IllegalStateException("We should never be force-pushing a manifest as the first version!");
|
||||
}
|
||||
|
||||
long newVersion = currentVersion + 1;
|
||||
Map<RecipientId, byte[]> newStorageKeys = generateNewKeys(oldStorageKeys);
|
||||
List<SignalStorageRecord> inserts = Stream.of(oldStorageKeys.keySet())
|
||||
.map(recipientDatabase::getRecipientSettings)
|
||||
.withoutNulls()
|
||||
.map(s -> StorageSyncHelper.localToRemoteRecord(s, Objects.requireNonNull(newStorageKeys.get(s.getId()))))
|
||||
.toList();
|
||||
|
||||
SignalStorageManifest manifest = new SignalStorageManifest(newVersion, new ArrayList<>(newStorageKeys.values()));
|
||||
|
||||
try {
|
||||
Log.i(TAG, String.format(Locale.ENGLISH, "Force-pushing data. Inserting %d keys.", inserts.size()));
|
||||
if (accountManager.resetStorageRecords(storageServiceKey, manifest, inserts).isPresent()) {
|
||||
Log.w(TAG, "Hit a conflict. Trying again.");
|
||||
throw new RetryLaterException();
|
||||
}
|
||||
} catch (InvalidKeyException e) {
|
||||
Log.w(TAG, "Hit an invalid key exception, which likely indicates a conflict.");
|
||||
throw new RetryLaterException(e);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Force push succeeded. Updating local manifest version to: " + newVersion);
|
||||
TextSecurePreferences.setStorageManifestVersion(context, newVersion);
|
||||
recipientDatabase.applyStorageSyncKeyUpdates(newContactKeys);
|
||||
recipientDatabase.applyStorageSyncKeyUpdates(newStorageKeys);
|
||||
storageKeyDatabase.deleteAll();
|
||||
}
|
||||
|
||||
|
@ -129,10 +128,8 @@ public class StorageForcePushJob extends BaseJob {
|
|||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<StorageForcePushJob> {
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
StorageForcePushJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
public @NonNull StorageForcePushJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new StorageForcePushJob(parameters);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,9 +29,8 @@ import org.thoughtcrime.securesms.util.Util;
|
|||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||
|
||||
|
@ -39,6 +38,7 @@ import java.io.IOException;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
|
@ -55,6 +55,8 @@ public class StorageSyncJob extends BaseJob {
|
|||
|
||||
private static final String TAG = Log.tag(StorageSyncJob.class);
|
||||
|
||||
private static final long REFRESH_INTERVAL = TimeUnit.HOURS.toMillis(2);
|
||||
|
||||
public StorageSyncJob() {
|
||||
this(new Job.Parameters.Builder().addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue(QUEUE_KEY)
|
||||
|
@ -63,6 +65,17 @@ public class StorageSyncJob extends BaseJob {
|
|||
.build());
|
||||
}
|
||||
|
||||
public static void scheduleIfNecessary() {
|
||||
long timeSinceLastSync = System.currentTimeMillis() - SignalStore.storageServiceValues().getLastSyncTime();
|
||||
|
||||
if (timeSinceLastSync > REFRESH_INTERVAL) {
|
||||
Log.d(TAG, "Scheduling a sync. Last sync was " + timeSinceLastSync + " ms ago.");
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
} else {
|
||||
Log.d(TAG, "No need for sync. Last sync was " + timeSinceLastSync + " ms ago.");
|
||||
}
|
||||
}
|
||||
|
||||
private StorageSyncJob(@NonNull Parameters parameters) {
|
||||
super(parameters);
|
||||
}
|
||||
|
@ -79,7 +92,7 @@ public class StorageSyncJob extends BaseJob {
|
|||
|
||||
@Override
|
||||
protected void onRun() throws IOException, RetryLaterException {
|
||||
if (!FeatureFlags.storageService()) throw new AssertionError();
|
||||
if (!FeatureFlags.storageService()) return;
|
||||
|
||||
try {
|
||||
boolean needsMultiDeviceSync = performSync();
|
||||
|
@ -87,6 +100,8 @@ public class StorageSyncJob extends BaseJob {
|
|||
if (TextSecurePreferences.isMultiDevice(context) && needsMultiDeviceSync) {
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceStorageSyncRequestJob());
|
||||
}
|
||||
|
||||
SignalStore.storageServiceValues().onSyncCompleted();
|
||||
} catch (InvalidKeyException e) {
|
||||
Log.w(TAG, "Failed to decrypt remote storage! Force-pushing and syncing the storage key to linked devices.", e);
|
||||
|
||||
|
@ -94,6 +109,11 @@ public class StorageSyncJob extends BaseJob {
|
|||
.then(new StorageForcePushJob())
|
||||
.then(new MultiDeviceStorageSyncRequestJob())
|
||||
.enqueue();
|
||||
} finally {
|
||||
if (!SignalStore.storageServiceValues().hasFirstStorageSyncCompleted()) {
|
||||
SignalStore.storageServiceValues().setFirstStorageSyncCompleted(true);
|
||||
ApplicationDependencies.getJobManager().add(new DirectoryRefreshJob(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,47 +130,62 @@ public class StorageSyncJob extends BaseJob {
|
|||
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
StorageKeyDatabase storageKeyDatabase = DatabaseFactory.getStorageKeyDatabase(context);
|
||||
MasterKey kbsMasterKey = SignalStore.kbsValues().getPinBackedMasterKey();
|
||||
StorageKey storageServiceKey = SignalStore.storageServiceValues().getOrCreateStorageMasterKey().deriveStorageServiceKey();
|
||||
|
||||
if (kbsMasterKey == null) {
|
||||
Log.w(TAG, "No KBS master key is set! Must abort.");
|
||||
return false;
|
||||
}
|
||||
boolean needsMultiDeviceSync = false;
|
||||
long localManifestVersion = TextSecurePreferences.getStorageManifestVersion(context);
|
||||
Optional<SignalStorageManifest> remoteManifest = accountManager.getStorageManifestIfDifferentVersion(storageServiceKey, localManifestVersion);
|
||||
long remoteManifestVersion = remoteManifest.transform(SignalStorageManifest::getVersion).or(localManifestVersion);
|
||||
|
||||
byte[] storageServiceKey = kbsMasterKey.deriveStorageServiceKey();
|
||||
boolean needsMultiDeviceSync = false;
|
||||
long localManifestVersion = TextSecurePreferences.getStorageManifestVersion(context);
|
||||
SignalStorageManifest remoteManifest = accountManager.getStorageManifest(storageServiceKey).or(new SignalStorageManifest(0, Collections.emptyList()));
|
||||
Log.i(TAG, "Our version: " + localManifestVersion + ", their version: " + remoteManifestVersion);
|
||||
|
||||
if (remoteManifest.getVersion() > localManifestVersion) {
|
||||
Log.i(TAG, "Newer manifest version found! Our version: " + localManifestVersion + ", their version: " + remoteManifest.getVersion());
|
||||
if (remoteManifest.isPresent() && remoteManifestVersion > localManifestVersion) {
|
||||
Log.i(TAG, "[Remote Newer] Newer manifest version found!");
|
||||
|
||||
List<byte[]> allLocalStorageKeys = getAllLocalStorageKeys(context);
|
||||
KeyDifferenceResult keyDifference = StorageSyncHelper.findKeyDifference(remoteManifest.getStorageKeys(), allLocalStorageKeys);
|
||||
KeyDifferenceResult keyDifference = StorageSyncHelper.findKeyDifference(remoteManifest.get().getStorageKeys(), allLocalStorageKeys);
|
||||
|
||||
if (!keyDifference.isEmpty()) {
|
||||
Log.i(TAG, "[Remote Newer] There's a difference in keys. Local-only: " + keyDifference.getLocalOnlyKeys().size() + ", Remote-only: " + keyDifference.getRemoteOnlyKeys().size());
|
||||
|
||||
List<SignalStorageRecord> localOnly = buildLocalStorageRecords(context, keyDifference.getLocalOnlyKeys());
|
||||
List<SignalStorageRecord> remoteOnly = accountManager.readStorageRecords(storageServiceKey, keyDifference.getRemoteOnlyKeys());
|
||||
MergeResult mergeResult = StorageSyncHelper.resolveConflict(remoteOnly, localOnly);
|
||||
WriteOperationResult writeOperationResult = StorageSyncHelper.createWriteOperation(remoteManifest.getVersion(), allLocalStorageKeys, mergeResult);
|
||||
WriteOperationResult writeOperationResult = StorageSyncHelper.createWriteOperation(remoteManifest.get().getVersion(), allLocalStorageKeys, mergeResult);
|
||||
|
||||
Optional<SignalStorageManifest> conflict = accountManager.writeStorageRecords(storageServiceKey, writeOperationResult.getManifest(), writeOperationResult.getInserts(), writeOperationResult.getDeletes());
|
||||
Log.i(TAG, "[Remote Newer] MergeResult :: " + mergeResult);
|
||||
|
||||
if (conflict.isPresent()) {
|
||||
Log.w(TAG, "Hit a conflict when trying to resolve the conflict! Retrying.");
|
||||
throw new RetryLaterException();
|
||||
if (!writeOperationResult.isEmpty()) {
|
||||
Log.i(TAG, "[Remote Newer] WriteOperationResult :: " + writeOperationResult);
|
||||
Log.i(TAG, "[Remote Newer] We have something to write remotely.");
|
||||
|
||||
if (writeOperationResult.getManifest().getStorageKeys().size() != remoteManifest.get().getStorageKeys().size() + writeOperationResult.getInserts().size() - writeOperationResult.getDeletes().size()) {
|
||||
Log.w(TAG, String.format(Locale.ENGLISH, "Bad storage key management! originalRemoteKeys: %d, newRemoteKeys: %d, insertedKeys: %d, deletedKeys: %d",
|
||||
remoteManifest.get().getStorageKeys().size(), writeOperationResult.getManifest().getStorageKeys().size(), writeOperationResult.getInserts().size(), writeOperationResult.getDeletes().size()));
|
||||
}
|
||||
|
||||
Optional<SignalStorageManifest> conflict = accountManager.writeStorageRecords(storageServiceKey, writeOperationResult.getManifest(), writeOperationResult.getInserts(), writeOperationResult.getDeletes());
|
||||
|
||||
if (conflict.isPresent()) {
|
||||
Log.w(TAG, "[Remote Newer] Hit a conflict when trying to resolve the conflict! Retrying.");
|
||||
throw new RetryLaterException();
|
||||
}
|
||||
|
||||
remoteManifestVersion = writeOperationResult.getManifest().getVersion();
|
||||
} else {
|
||||
Log.i(TAG, "[Remote Newer] After resolving the conflict, all changes are local. No remote writes needed.");
|
||||
}
|
||||
|
||||
recipientDatabase.applyStorageSyncUpdates(mergeResult.getLocalContactInserts(), mergeResult.getLocalContactUpdates());
|
||||
recipientDatabase.applyStorageSyncUpdates(mergeResult.getLocalContactInserts(), mergeResult.getLocalContactUpdates(), mergeResult.getLocalGroupV1Inserts(), mergeResult.getLocalGroupV1Updates());
|
||||
storageKeyDatabase.applyStorageSyncUpdates(mergeResult.getLocalUnknownInserts(), mergeResult.getLocalUnknownDeletes());
|
||||
needsMultiDeviceSync = true;
|
||||
|
||||
Log.i(TAG, "[Post-Conflict] Updating local manifest version to: " + writeOperationResult.getManifest().getVersion());
|
||||
TextSecurePreferences.setStorageManifestVersion(context, writeOperationResult.getManifest().getVersion());
|
||||
Log.i(TAG, "[Remote Newer] Updating local manifest version to: " + remoteManifestVersion);
|
||||
TextSecurePreferences.setStorageManifestVersion(context, remoteManifestVersion);
|
||||
} else {
|
||||
Log.i(TAG, "Remote version was newer, but our local data matched.");
|
||||
Log.i(TAG, "[Post-Empty-Conflict] Updating local manifest version to: " + remoteManifest.getVersion());
|
||||
TextSecurePreferences.setStorageManifestVersion(context, remoteManifest.getVersion());
|
||||
Log.i(TAG, "[Remote Newer] Remote version was newer, but our local data matched.");
|
||||
Log.i(TAG, "[Remote Newer] Updating local manifest version to: " + remoteManifest.get().getVersion());
|
||||
TextSecurePreferences.setStorageManifestVersion(context, remoteManifest.get().getVersion());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -167,11 +202,20 @@ public class StorageSyncJob extends BaseJob {
|
|||
pendingDeletions);
|
||||
|
||||
if (localWriteResult.isPresent()) {
|
||||
WriteOperationResult localWrite = localWriteResult.get().getWriteResult();
|
||||
Log.i(TAG, String.format(Locale.ENGLISH, "[Local Changes] Local changes present. %d updates, %d inserts, %d deletes.", pendingUpdates.size(), pendingInsertions.size(), pendingDeletions.size()));
|
||||
|
||||
WriteOperationResult localWrite = localWriteResult.get().getWriteResult();
|
||||
|
||||
Log.i(TAG, "[Local Changes] WriteOperationResult :: " + localWrite);
|
||||
|
||||
if (localWrite.isEmpty()) {
|
||||
throw new AssertionError("Decided there were local writes, but our write result was empty!");
|
||||
}
|
||||
|
||||
Optional<SignalStorageManifest> conflict = accountManager.writeStorageRecords(storageServiceKey, localWrite.getManifest(), localWrite.getInserts(), localWrite.getDeletes());
|
||||
|
||||
if (conflict.isPresent()) {
|
||||
Log.w(TAG, "Hit a conflict when trying to upload our local writes! Retrying.");
|
||||
Log.w(TAG, "[Local Changes] Hit a conflict when trying to upload our local writes! Retrying.");
|
||||
throw new RetryLaterException();
|
||||
}
|
||||
|
||||
|
@ -186,21 +230,21 @@ public class StorageSyncJob extends BaseJob {
|
|||
|
||||
needsMultiDeviceSync = true;
|
||||
|
||||
Log.i(TAG, "[Post Write] Updating local manifest version to: " + localWriteResult.get().getWriteResult().getManifest().getVersion());
|
||||
Log.i(TAG, "[Local Changes] Updating local manifest version to: " + localWriteResult.get().getWriteResult().getManifest().getVersion());
|
||||
TextSecurePreferences.setStorageManifestVersion(context, localWriteResult.get().getWriteResult().getManifest().getVersion());
|
||||
} else {
|
||||
Log.i(TAG, "Nothing locally to write.");
|
||||
Log.i(TAG, "[Local Changes] No local changes.");
|
||||
}
|
||||
|
||||
return needsMultiDeviceSync;
|
||||
}
|
||||
|
||||
public static @NonNull List<byte[]> getAllLocalStorageKeys(@NonNull Context context) {
|
||||
private static @NonNull List<byte[]> getAllLocalStorageKeys(@NonNull Context context) {
|
||||
return Util.concatenatedList(DatabaseFactory.getRecipientDatabase(context).getAllStorageSyncKeys(),
|
||||
DatabaseFactory.getStorageKeyDatabase(context).getAllKeys());
|
||||
}
|
||||
|
||||
public static @NonNull List<SignalStorageRecord> buildLocalStorageRecords(@NonNull Context context, @NonNull List<byte[]> keys) {
|
||||
private static @NonNull List<SignalStorageRecord> buildLocalStorageRecords(@NonNull Context context, @NonNull List<byte[]> keys) {
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
StorageKeyDatabase storageKeyDatabase = DatabaseFactory.getStorageKeyDatabase(context);
|
||||
|
||||
|
@ -208,10 +252,7 @@ public class StorageSyncJob extends BaseJob {
|
|||
|
||||
for (byte[] key : keys) {
|
||||
SignalStorageRecord record = Optional.fromNullable(recipientDatabase.getByStorageSyncKey(key))
|
||||
.transform(recipient -> {
|
||||
SignalContactRecord contact = StorageSyncHelper.localToRemoteContact(recipient);
|
||||
return SignalStorageRecord.forContact(key, contact);
|
||||
})
|
||||
.transform(StorageSyncHelper::localToRemoteRecord)
|
||||
.or(() -> storageKeyDatabase.getByKey(key));
|
||||
records.add(record);
|
||||
}
|
||||
|
@ -220,7 +261,6 @@ public class StorageSyncJob extends BaseJob {
|
|||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<StorageSyncJob> {
|
||||
|
||||
@Override
|
||||
public @NonNull StorageSyncJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new StorageSyncJob(parameters);
|
||||
|
|
|
@ -14,7 +14,7 @@ public final class RegistrationValues {
|
|||
this.store = store;
|
||||
}
|
||||
|
||||
public synchronized void onNewInstall() {
|
||||
public synchronized void onFirstEverAppLaunch() {
|
||||
store.beginWrite()
|
||||
.putBoolean(REGISTRATION_COMPLETE, false)
|
||||
// TODO [greyson] [pins] Maybe re-enable in the future
|
||||
|
@ -23,7 +23,7 @@ public final class RegistrationValues {
|
|||
}
|
||||
|
||||
public synchronized void clearRegistrationComplete() {
|
||||
onNewInstall();
|
||||
onFirstEverAppLaunch();
|
||||
}
|
||||
|
||||
public synchronized void setRegistrationComplete() {
|
||||
|
|
|
@ -4,6 +4,7 @@ import androidx.annotation.NonNull;
|
|||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.SignalUncaughtExceptionHandler;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
|
||||
/**
|
||||
* Simple, encrypted key-value store.
|
||||
|
@ -15,6 +16,11 @@ public final class SignalStore {
|
|||
|
||||
private SignalStore() {}
|
||||
|
||||
public static void onFirstEverAppLaunch() {
|
||||
registrationValues().onFirstEverAppLaunch();
|
||||
storageServiceValues().setFirstStorageSyncCompleted(false);
|
||||
}
|
||||
|
||||
public static @NonNull KbsValues kbsValues() {
|
||||
return new KbsValues(getStore());
|
||||
}
|
||||
|
@ -31,6 +37,10 @@ public final class SignalStore {
|
|||
return new RemoteConfigValues(getStore());
|
||||
}
|
||||
|
||||
public static @NonNull StorageServiceValues storageServiceValues() {
|
||||
return new StorageServiceValues(getStore());
|
||||
}
|
||||
|
||||
public static long getLastPrekeyRefreshTime() {
|
||||
return getStore().getLong(LAST_PREKEY_REFRESH_TIME, 0);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
package org.thoughtcrime.securesms.keyvalue;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public class StorageServiceValues {
|
||||
|
||||
private static final String STORAGE_MASTER_KEY = "storage.storage_master_key";
|
||||
private static final String FIRST_STORAGE_SYNC_COMPLETED = "storage.first_storage_sync_completed";
|
||||
private static final String LAST_SYNC_TIME = "storage.last_sync_time";
|
||||
|
||||
private final KeyValueStore store;
|
||||
|
||||
StorageServiceValues(@NonNull KeyValueStore store) {
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
public synchronized MasterKey getOrCreateStorageMasterKey() {
|
||||
byte[] blob = store.getBlob(STORAGE_MASTER_KEY, null);
|
||||
|
||||
if (blob == null) {
|
||||
store.beginWrite()
|
||||
.putBlob(STORAGE_MASTER_KEY, MasterKey.createNew(new SecureRandom()).serialize())
|
||||
.commit();
|
||||
blob = store.getBlob(STORAGE_MASTER_KEY, null);
|
||||
}
|
||||
|
||||
return new MasterKey(blob);
|
||||
}
|
||||
|
||||
public boolean hasFirstStorageSyncCompleted() {
|
||||
return !FeatureFlags.storageServiceRestore() || store.getBoolean(FIRST_STORAGE_SYNC_COMPLETED, true);
|
||||
}
|
||||
|
||||
public void setFirstStorageSyncCompleted(boolean completed) {
|
||||
store.beginWrite().putBoolean(FIRST_STORAGE_SYNC_COMPLETED, completed).apply();
|
||||
}
|
||||
|
||||
public long getLastSyncTime() {
|
||||
return store.getLong(LAST_SYNC_TIME, 0);
|
||||
}
|
||||
|
||||
public void onSyncCompleted() {
|
||||
store.beginWrite().putLong(LAST_SYNC_TIME, System.currentTimeMillis()).apply();
|
||||
}
|
||||
}
|
|
@ -40,7 +40,7 @@ public class ApplicationMigrations {
|
|||
|
||||
private static final int LEGACY_CANONICAL_VERSION = 455;
|
||||
|
||||
public static final int CURRENT_VERSION = 10;
|
||||
public static final int CURRENT_VERSION = 11;
|
||||
|
||||
private static final class Version {
|
||||
static final int LEGACY = 1;
|
||||
|
@ -53,6 +53,7 @@ public class ApplicationMigrations {
|
|||
static final int STICKERS_LAUNCH = 8;
|
||||
static final int TEST_ARGON2 = 9;
|
||||
static final int SWOON_STICKERS = 10;
|
||||
static final int STORAGE_SERVICE = 11;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -205,6 +206,10 @@ public class ApplicationMigrations {
|
|||
jobs.put(Version.SWOON_STICKERS, new StickerAdditionMigrationJob(BlessedPacks.SWOON_HANDS, BlessedPacks.SWOON_FACES));
|
||||
}
|
||||
|
||||
if (lastSeenVersion < Version.STORAGE_SERVICE) {
|
||||
jobs.put(Version.STORAGE_SERVICE, new StorageServiceMigrationJob());
|
||||
}
|
||||
|
||||
return jobs;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
package org.thoughtcrime.securesms.migrations;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceKeysUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.stickers.BlessedPacks;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class StorageServiceMigrationJob extends MigrationJob {
|
||||
|
||||
private static final String TAG = Log.tag(StorageServiceMigrationJob.class);
|
||||
|
||||
public static final String KEY = "StorageServiceMigrationJob";
|
||||
|
||||
StorageServiceMigrationJob() {
|
||||
this(new Parameters.Builder().build());
|
||||
}
|
||||
|
||||
private StorageServiceMigrationJob(@NonNull Parameters parameters) {
|
||||
super(parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUiBlocking() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void performMigration() {
|
||||
JobManager jobManager = ApplicationDependencies.getJobManager();
|
||||
|
||||
if (TextSecurePreferences.isMultiDevice(context)) {
|
||||
Log.i(TAG, "Multi-device.");
|
||||
jobManager.startChain(new MultiDeviceKeysUpdateJob())
|
||||
.then(new StorageSyncJob())
|
||||
.enqueue();
|
||||
} else {
|
||||
Log.i(TAG, "Single-device.");
|
||||
jobManager.add(new StorageSyncJob());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean shouldRetry(@NonNull Exception e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static class Factory implements Job.Factory<StorageServiceMigrationJob> {
|
||||
@Override
|
||||
public @NonNull StorageServiceMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new StorageServiceMigrationJob(parameters);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -39,8 +39,7 @@ public final class ProfileName implements Parcelable {
|
|||
return givenName;
|
||||
}
|
||||
|
||||
public @NonNull
|
||||
String getFamilyName() {
|
||||
public @NonNull String getFamilyName() {
|
||||
return familyName;
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob;
|
|||
import org.thoughtcrime.securesms.jobs.MultiDeviceMessageRequestResponseJob;
|
||||
import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
|
||||
|
@ -86,6 +87,7 @@ public class RecipientUtil {
|
|||
}
|
||||
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceBlockedUpdateJob());
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
@ -96,6 +98,7 @@ public class RecipientUtil {
|
|||
|
||||
DatabaseFactory.getRecipientDatabase(context).setBlocked(recipient.getId(), false);
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceBlockedUpdateJob());
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
|
||||
if (FeatureFlags.messageRequests()) {
|
||||
ApplicationDependencies.getJobManager().add(MultiDeviceMessageRequestResponseJob.forAccept(recipient.getId()));
|
||||
|
|
|
@ -6,7 +6,6 @@ import android.text.InputType;
|
|||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
|
@ -21,13 +20,17 @@ import androidx.navigation.Navigation;
|
|||
import com.dd.CircularProgressButton;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.registration.service.CodeVerificationRequest;
|
||||
import org.thoughtcrime.securesms.registration.service.RegistrationService;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
@ -172,10 +175,7 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment {
|
|||
|
||||
@Override
|
||||
public void onSuccessfulRegistration() {
|
||||
cancelSpinning(pinButton);
|
||||
SignalStore.kbsValues().setKeyboardType(getPinEntryKeyboardType());
|
||||
|
||||
Navigation.findNavController(requireView()).navigate(RegistrationLockFragmentDirections.actionSuccessfulRegistration());
|
||||
handleSuccessfulPinEntry();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -301,4 +301,28 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment {
|
|||
ServiceUtil.getInputMethodManager(pinEntry.getContext()).showSoftInput(pinEntry, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSuccessfulPinEntry() {
|
||||
SignalStore.kbsValues().setKeyboardType(getPinEntryKeyboardType());
|
||||
|
||||
if (FeatureFlags.storageServiceRestore()) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
SimpleTask.run(() -> {
|
||||
return ApplicationDependencies.getJobManager().runSynchronously(new StorageSyncJob(), TimeUnit.SECONDS.toMillis(10));
|
||||
}, result -> {
|
||||
long elapsedTime = System.currentTimeMillis() - startTime;
|
||||
|
||||
if (result.isPresent()) {
|
||||
Log.i(TAG, "Storage Service restore completed: " + result.get().name() + ". (Took " + elapsedTime + " ms)");
|
||||
} else {
|
||||
Log.i(TAG, "Storage Service restore failed to complete in the allotted time. (" + elapsedTime + " ms elapsed)");
|
||||
}
|
||||
cancelSpinning(pinButton);
|
||||
Navigation.findNavController(requireView()).navigate(RegistrationLockFragmentDirections.actionSuccessfulRegistration());
|
||||
});
|
||||
} else {
|
||||
cancelSpinning(pinButton);
|
||||
Navigation.findNavController(requireView()).navigate(RegistrationLockFragmentDirections.actionSuccessfulRegistration());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
|||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
import org.whispersystems.libsignal.state.PreKeyRecord;
|
||||
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||
|
@ -166,7 +167,7 @@ public final class CodeVerificationRequest {
|
|||
break;
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
}.executeOnExecutor(SignalExecutors.UNBOUNDED);
|
||||
}
|
||||
|
||||
private static TokenResponse getToken(@Nullable String basicStorageCredentials) throws IOException {
|
||||
|
|
|
@ -49,11 +49,11 @@ public final class FeatureFlags {
|
|||
private static final String UUIDS = "android.uuids";
|
||||
private static final String MESSAGE_REQUESTS = "android.messageRequests";
|
||||
private static final String USERNAMES = "android.usernames";
|
||||
private static final String STORAGE_SERVICE = "android.storageService";
|
||||
private static final String PINS_FOR_ALL = "android.pinsForAll";
|
||||
private static final String PINS_MEGAPHONE_KILL_SWITCH = "android.pinsMegaphoneKillSwitch";
|
||||
private static final String PROFILE_NAMES_MEGAPHONE = "android.profileNamesMegaphone";
|
||||
private static final String VIDEO_TRIMMING = "android.videoTrimming";
|
||||
private static final String STORAGE_SERVICE = "android.storageService";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
|
@ -65,7 +65,8 @@ public final class FeatureFlags {
|
|||
PINS_FOR_ALL,
|
||||
PINS_MEGAPHONE_KILL_SWITCH,
|
||||
PROFILE_NAMES_MEGAPHONE,
|
||||
MESSAGE_REQUESTS
|
||||
MESSAGE_REQUESTS,
|
||||
STORAGE_SERVICE
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -86,15 +87,16 @@ public final class FeatureFlags {
|
|||
* more burden on the reader to ensure that the app experience remains consistent.
|
||||
*/
|
||||
private static final Set<String> HOT_SWAPPABLE = Sets.newHashSet(
|
||||
VIDEO_TRIMMING,
|
||||
PINS_MEGAPHONE_KILL_SWITCH
|
||||
VIDEO_TRIMMING,
|
||||
PINS_MEGAPHONE_KILL_SWITCH,
|
||||
STORAGE_SERVICE
|
||||
);
|
||||
|
||||
/**
|
||||
* Flags in this set will stay true forever once they receive a true value from a remote config.
|
||||
*/
|
||||
private static final Set<String> STICKY = Sets.newHashSet(
|
||||
PINS_FOR_ALL
|
||||
PINS_FOR_ALL
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -179,11 +181,6 @@ public final class FeatureFlags {
|
|||
return value;
|
||||
}
|
||||
|
||||
/** Storage service. */
|
||||
public static boolean storageService() {
|
||||
return getValue(STORAGE_SERVICE, false);
|
||||
}
|
||||
|
||||
/** Enables new KBS UI and notices but does not require user to set a pin */
|
||||
public static boolean pinsForAll() {
|
||||
return SignalStore.registrationValues().pinWasRequiredAtRegistration() ||
|
||||
|
@ -207,6 +204,16 @@ public final class FeatureFlags {
|
|||
return getValue(VIDEO_TRIMMING, false);
|
||||
}
|
||||
|
||||
/** Whether or not we can actually restore data on a new installation. NOT remote-configurable. */
|
||||
public static boolean storageServiceRestore() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Whether or not we sync to the storage service. */
|
||||
public static boolean storageService() {
|
||||
return getValue(STORAGE_SERVICE, false);
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
public static synchronized @NonNull Map<String, Boolean> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
|
|
|
@ -74,6 +74,15 @@ public final class SqlUtil {
|
|||
return new UpdateQuery("(" + selection + ") AND (" + qualifier + ")", fullArgs.toArray(new String[0]));
|
||||
}
|
||||
|
||||
public static String[] appendArg(@NonNull String[] args, String addition) {
|
||||
String[] output = new String[args.length + 1];
|
||||
|
||||
System.arraycopy(args, 0, output, 0, args.length);
|
||||
output[output.length - 1] = addition;
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public static class UpdateQuery {
|
||||
private final String where;
|
||||
private final String[] whereArgs;
|
||||
|
|
|
@ -12,6 +12,8 @@ import org.thoughtcrime.securesms.contacts.sync.StorageSyncHelper.MergeResult;
|
|||
import org.thoughtcrime.securesms.util.Conversions;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
||||
import org.whispersystems.signalservice.api.storage.SignalRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
|
@ -78,12 +80,12 @@ public final class StorageSyncHelperTest {
|
|||
|
||||
assertEquals(setOf(remote1), result.getLocalContactInserts());
|
||||
assertTrue(result.getLocalContactUpdates().isEmpty());
|
||||
assertEquals(setOf(local1), result.getRemoteContactInserts());
|
||||
assertTrue(result.getRemoteContactUpdates().isEmpty());
|
||||
assertEquals(setOf(SignalStorageRecord.forContact(local1)), result.getRemoteInserts());
|
||||
assertTrue(result.getRemoteUpdates().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveConflict_sameAsRemote() {
|
||||
public void resolveConflict_contact_sameAsRemote() {
|
||||
SignalContactRecord remote1 = contact(1, UUID_A, E164_A, "a");
|
||||
SignalContactRecord local1 = contact(2, UUID_A, E164_A, "a");
|
||||
|
||||
|
@ -93,12 +95,27 @@ public final class StorageSyncHelperTest {
|
|||
|
||||
assertTrue(result.getLocalContactInserts().isEmpty());
|
||||
assertEquals(setOf(contactUpdate(local1, expectedMerge)), result.getLocalContactUpdates());
|
||||
assertTrue(result.getRemoteContactInserts().isEmpty());
|
||||
assertTrue(result.getRemoteContactUpdates().isEmpty());
|
||||
assertTrue(result.getRemoteInserts().isEmpty());
|
||||
assertTrue(result.getRemoteUpdates().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveConflict_sameAsLocal() {
|
||||
public void resolveConflict_group_sameAsRemote() {
|
||||
SignalGroupV1Record remote1 = groupV1(1, 1, true, false);
|
||||
SignalGroupV1Record local1 = groupV1(2, 1, true, false);
|
||||
|
||||
MergeResult result = StorageSyncHelper.resolveConflict(recordSetOf(remote1), recordSetOf(local1));
|
||||
|
||||
SignalGroupV1Record expectedMerge = groupV1(1, 1, true, false);
|
||||
|
||||
assertTrue(result.getLocalContactInserts().isEmpty());
|
||||
assertEquals(setOf(groupV1Update(local1, expectedMerge)), result.getLocalGroupV1Updates());
|
||||
assertTrue(result.getRemoteInserts().isEmpty());
|
||||
assertTrue(result.getRemoteUpdates().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveConflict_contact_sameAsLocal() {
|
||||
SignalContactRecord remote1 = contact(1, UUID_A, E164_A, null);
|
||||
SignalContactRecord local1 = contact(2, UUID_A, E164_A, "a");
|
||||
|
||||
|
@ -108,8 +125,23 @@ public final class StorageSyncHelperTest {
|
|||
|
||||
assertTrue(result.getLocalContactInserts().isEmpty());
|
||||
assertTrue(result.getLocalContactUpdates().isEmpty());
|
||||
assertTrue(result.getRemoteContactInserts().isEmpty());
|
||||
assertEquals(setOf(contactUpdate(remote1, expectedMerge)), result.getRemoteContactUpdates());
|
||||
assertTrue(result.getRemoteInserts().isEmpty());
|
||||
assertEquals(setOf(recordUpdate(remote1, expectedMerge)), result.getRemoteUpdates());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveConflict_group_sameAsLocal() {
|
||||
SignalGroupV1Record remote1 = groupV1(1, 1, true, false);
|
||||
SignalGroupV1Record local1 = groupV1(2, 1, true, true);
|
||||
|
||||
MergeResult result = StorageSyncHelper.resolveConflict(recordSetOf(remote1), recordSetOf(local1));
|
||||
|
||||
SignalGroupV1Record expectedMerge = groupV1(2, 1, true, true);
|
||||
|
||||
assertTrue(result.getLocalContactInserts().isEmpty());
|
||||
assertTrue(result.getLocalGroupV1Updates().isEmpty());
|
||||
assertTrue(result.getRemoteInserts().isEmpty());
|
||||
assertEquals(setOf(recordUpdate(remote1, expectedMerge)), result.getRemoteUpdates());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -138,26 +170,28 @@ public final class StorageSyncHelperTest {
|
|||
SignalContactRecord remote3 = contact(5, UUID_C, E164_C, "c");
|
||||
SignalContactRecord local3 = contact(6, UUID_D, E164_D, "d");
|
||||
|
||||
SignalStorageRecord unknownRemote = unknown(7);
|
||||
SignalStorageRecord unknownLocal = unknown(8);
|
||||
SignalGroupV1Record remote4 = groupV1(7, 1, true, false);
|
||||
SignalGroupV1Record local4 = groupV1(8, 1, false, true);
|
||||
|
||||
StorageSyncHelper.setTestKeyGenerator(new TestGenerator(999));
|
||||
SignalStorageRecord unknownRemote = unknown(9);
|
||||
SignalStorageRecord unknownLocal = unknown(10);
|
||||
|
||||
Set<SignalStorageRecord> remoteOnly = recordSetOf(remote1, remote2, remote3);
|
||||
Set<SignalStorageRecord> localOnly = recordSetOf(local1, local2, local3);
|
||||
StorageSyncHelper.setTestKeyGenerator(new TestGenerator(111, 222));
|
||||
|
||||
remoteOnly.add(unknownRemote);
|
||||
localOnly.add(unknownLocal);
|
||||
Set<SignalStorageRecord> remoteOnly = recordSetOf(remote1, remote2, remote3, remote4, unknownRemote);
|
||||
Set<SignalStorageRecord> localOnly = recordSetOf(local1, local2, local3, local4, unknownLocal);
|
||||
|
||||
MergeResult result = StorageSyncHelper.resolveConflict(remoteOnly, localOnly);
|
||||
|
||||
SignalContactRecord merge1 = contact(2, UUID_A, E164_A, "a");
|
||||
SignalContactRecord merge2 = contact(999, UUID_B, E164_B, "b");
|
||||
SignalContactRecord merge2 = contact(111, UUID_B, E164_B, "b");
|
||||
SignalGroupV1Record merge4 = groupV1(222, 1, true, true);
|
||||
|
||||
assertEquals(setOf(remote3), result.getLocalContactInserts());
|
||||
assertEquals(setOf(contactUpdate(local2, merge2)), result.getLocalContactUpdates());
|
||||
assertEquals(setOf(local3), result.getRemoteContactInserts());
|
||||
assertEquals(setOf(contactUpdate(remote1, merge1), contactUpdate(remote2, merge2)), result.getRemoteContactUpdates());
|
||||
assertEquals(setOf(groupV1Update(local4, merge4)), result.getLocalGroupV1Updates());
|
||||
assertEquals(setOf(SignalStorageRecord.forContact(local3)), result.getRemoteInserts());
|
||||
assertEquals(setOf(recordUpdate(remote1, merge1), recordUpdate(remote2, merge2), recordUpdate(remote4, merge4)), result.getRemoteUpdates());
|
||||
assertEquals(setOf(unknownRemote), result.getLocalUnknownInserts());
|
||||
assertEquals(setOf(unknownLocal), result.getLocalUnknownDeletes());
|
||||
}
|
||||
|
@ -169,7 +203,8 @@ public final class StorageSyncHelperTest {
|
|||
.setIdentityKey(byteArray(2))
|
||||
.setIdentityState(SignalContactRecord.IdentityState.VERIFIED)
|
||||
.setProfileKey(byteArray(3))
|
||||
.setProfileName("profile name A")
|
||||
.setGivenName("AFirst")
|
||||
.setFamilyName("ALast")
|
||||
.setUsername("username A")
|
||||
.setNickname("nickname A")
|
||||
.setProfileSharingEnabled(true)
|
||||
|
@ -179,7 +214,8 @@ public final class StorageSyncHelperTest {
|
|||
.setIdentityKey(byteArray(99))
|
||||
.setIdentityState(SignalContactRecord.IdentityState.DEFAULT)
|
||||
.setProfileKey(byteArray(999))
|
||||
.setProfileName("profile name B")
|
||||
.setGivenName("BFirst")
|
||||
.setFamilyName("BLast")
|
||||
.setUsername("username B")
|
||||
.setNickname("nickname B")
|
||||
.setProfileSharingEnabled(false)
|
||||
|
@ -192,7 +228,8 @@ public final class StorageSyncHelperTest {
|
|||
assertArrayEquals(byteArray(2), merged.getIdentityKey().get());
|
||||
assertEquals(SignalContactRecord.IdentityState.VERIFIED, merged.getIdentityState());
|
||||
assertArrayEquals(byteArray(3), merged.getProfileKey().get());
|
||||
assertEquals("profile name A", merged.getProfileName().get());
|
||||
assertEquals("AFirst", merged.getGivenName().get());
|
||||
assertEquals("ALast", merged.getFamilyName().get());
|
||||
assertEquals("username A", merged.getUsername().get());
|
||||
assertEquals("nickname B", merged.getNickname().get());
|
||||
assertTrue(merged.isProfileSharingEnabled());
|
||||
|
@ -202,14 +239,16 @@ public final class StorageSyncHelperTest {
|
|||
public void mergeContacts_fillInGaps() {
|
||||
SignalContactRecord remote = new SignalContactRecord.Builder(byteArray(1), new SignalServiceAddress(UUID_A, null))
|
||||
.setBlocked(true)
|
||||
.setProfileName("profile name A")
|
||||
.setGivenName("AFirst")
|
||||
.setFamilyName("")
|
||||
.setProfileSharingEnabled(true)
|
||||
.build();
|
||||
SignalContactRecord local = new SignalContactRecord.Builder(byteArray(2), new SignalServiceAddress(UUID_B, E164_B))
|
||||
.setBlocked(false)
|
||||
.setIdentityKey(byteArray(2))
|
||||
.setProfileKey(byteArray(3))
|
||||
.setProfileName("profile name B")
|
||||
.setGivenName("BFirst")
|
||||
.setFamilyName("BLast")
|
||||
.setUsername("username B")
|
||||
.setProfileSharingEnabled(false)
|
||||
.build();
|
||||
|
@ -221,41 +260,47 @@ public final class StorageSyncHelperTest {
|
|||
assertArrayEquals(byteArray(2), merged.getIdentityKey().get());
|
||||
assertEquals(SignalContactRecord.IdentityState.DEFAULT, merged.getIdentityState());
|
||||
assertArrayEquals(byteArray(3), merged.getProfileKey().get());
|
||||
assertEquals("profile name A", merged.getProfileName().get());
|
||||
assertEquals("AFirst", merged.getGivenName().get());
|
||||
assertEquals("", merged.getFamilyName().get());
|
||||
assertEquals("username B", merged.getUsername().get());
|
||||
assertTrue(merged.isProfileSharingEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createWriteOperation_generic() {
|
||||
List<byte[]> localKeys = byteListOf(1, 2, 3, 4);
|
||||
List<byte[]> localKeys = byteListOf(1, 2, 3, 4, 100);
|
||||
SignalContactRecord insert1 = contact(6, UUID_A, E164_A, "a" );
|
||||
SignalContactRecord old1 = contact(1, UUID_B, E164_B, "b" );
|
||||
SignalContactRecord new1 = contact(5, UUID_B, E164_B, "z" );
|
||||
SignalContactRecord insert2 = contact(7, UUID_C, E164_C, "c" );
|
||||
SignalContactRecord old2 = contact(2, UUID_D, E164_D, "d" );
|
||||
SignalContactRecord new2 = contact(8, UUID_D, E164_D, "z2");
|
||||
SignalStorageRecord unknownInsert = unknown(9);
|
||||
SignalStorageRecord unknownDelete = unknown(10);
|
||||
SignalGroupV1Record insert3 = groupV1(9, 1, true, true);
|
||||
SignalGroupV1Record old3 = groupV1(100, 1, true, true);
|
||||
SignalGroupV1Record new3 = groupV1(10, 1, false, true);
|
||||
SignalStorageRecord unknownInsert = unknown(11);
|
||||
SignalStorageRecord unknownDelete = unknown(12);
|
||||
|
||||
StorageSyncHelper.WriteOperationResult result = StorageSyncHelper.createWriteOperation(1,
|
||||
localKeys,
|
||||
new MergeResult(setOf(insert2),
|
||||
setOf(contactUpdate(old2, new2)),
|
||||
setOf(insert1),
|
||||
setOf(contactUpdate(old1, new1)),
|
||||
setOf(insert3),
|
||||
setOf(groupV1Update(old3, new3)),
|
||||
setOf(unknownInsert),
|
||||
setOf(unknownDelete)));
|
||||
setOf(unknownDelete),
|
||||
recordSetOf(insert1, insert3),
|
||||
setOf(recordUpdate(old1, new1), recordUpdate(old3, new3))));
|
||||
|
||||
assertEquals(2, result.getManifest().getVersion());
|
||||
assertByteListEquals(byteListOf(3, 4, 5, 6, 7, 8, 9), result.getManifest().getStorageKeys());
|
||||
assertTrue(recordSetOf(insert1, new1).containsAll(result.getInserts()));
|
||||
assertEquals(2, result.getInserts().size());
|
||||
assertByteListEquals(byteListOf(1), result.getDeletes());
|
||||
assertByteListEquals(byteListOf(3, 4, 5, 6, 7, 8, 9, 10, 11), result.getManifest().getStorageKeys());
|
||||
assertTrue(recordSetOf(insert1, new1, insert3, new3).containsAll(result.getInserts()));
|
||||
assertEquals(4, result.getInserts().size());
|
||||
assertByteListEquals(byteListOf(1, 100), result.getDeletes());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contacts_with_same_profile_key_contents_are_equal() {
|
||||
public void ContactUpdate_equals_sameProfileKeys() {
|
||||
byte[] profileKey = new byte[32];
|
||||
byte[] profileKeyCopy = profileKey.clone();
|
||||
|
||||
|
@ -269,7 +314,7 @@ public final class StorageSyncHelperTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void contacts_with_different_profile_key_contents_are_not_equal() {
|
||||
public void ContactUpdate_equals_differentProfileKeys() {
|
||||
byte[] profileKey = new byte[32];
|
||||
byte[] profileKeyCopy = profileKey.clone();
|
||||
profileKeyCopy[0] = 1;
|
||||
|
@ -283,41 +328,33 @@ public final class StorageSyncHelperTest {
|
|||
assertTrue(contactUpdate(a, b).profileKeyChanged());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contacts_with_same_identity_key_contents_are_equal() {
|
||||
byte[] profileKey = new byte[32];
|
||||
byte[] profileKeyCopy = profileKey.clone();
|
||||
|
||||
SignalContactRecord a = contactBuilder(1, UUID_A, E164_A, "a").setIdentityKey(profileKey).build();
|
||||
SignalContactRecord b = contactBuilder(1, UUID_A, E164_A, "a").setIdentityKey(profileKeyCopy).build();
|
||||
|
||||
assertEquals(a, b);
|
||||
assertEquals(a.hashCode(), b.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contacts_with_different_identity_key_contents_are_not_equal() {
|
||||
byte[] profileKey = new byte[32];
|
||||
byte[] profileKeyCopy = profileKey.clone();
|
||||
profileKeyCopy[0] = 1;
|
||||
|
||||
SignalContactRecord a = contactBuilder(1, UUID_A, E164_A, "a").setIdentityKey(profileKey).build();
|
||||
SignalContactRecord b = contactBuilder(1, UUID_A, E164_A, "a").setIdentityKey(profileKeyCopy).build();
|
||||
|
||||
assertNotEquals(a, b);
|
||||
assertNotEquals(a.hashCode(), b.hashCode());
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
private static <E> Set<E> setOf(E... values) {
|
||||
return Sets.newHashSet(values);
|
||||
}
|
||||
|
||||
private static Set<SignalStorageRecord> recordSetOf(SignalContactRecord... contactRecords) {
|
||||
private static Set<SignalStorageRecord> recordSetOf(SignalRecord... records) {
|
||||
LinkedHashSet<SignalStorageRecord> storageRecords = new LinkedHashSet<>();
|
||||
|
||||
for (SignalContactRecord contactRecord : contactRecords) {
|
||||
storageRecords.add(SignalStorageRecord.forContact(contactRecord.getKey(), contactRecord));
|
||||
for (SignalRecord record : records) {
|
||||
if (record instanceof SignalContactRecord) {
|
||||
storageRecords.add(SignalStorageRecord.forContact(record.getKey(), (SignalContactRecord) record));
|
||||
} else if (record instanceof SignalGroupV1Record) {
|
||||
storageRecords.add(SignalStorageRecord.forGroupV1(record.getKey(), (SignalGroupV1Record) record));
|
||||
} else {
|
||||
storageRecords.add(SignalStorageRecord.forUnknown(record.getKey(), UNKNOWN_TYPE));
|
||||
}
|
||||
}
|
||||
|
||||
return storageRecords;
|
||||
}
|
||||
|
||||
private static Set<SignalStorageRecord> recordSetOf(SignalGroupV1Record... groupRecords) {
|
||||
LinkedHashSet<SignalStorageRecord> storageRecords = new LinkedHashSet<>();
|
||||
|
||||
for (SignalGroupV1Record contactRecord : groupRecords) {
|
||||
storageRecords.add(SignalStorageRecord.forGroupV1(contactRecord.getKey(), contactRecord));
|
||||
}
|
||||
|
||||
return storageRecords;
|
||||
|
@ -329,7 +366,7 @@ public final class StorageSyncHelperTest {
|
|||
String profileName)
|
||||
{
|
||||
return new SignalContactRecord.Builder(byteArray(key), new SignalServiceAddress(uuid, e164))
|
||||
.setProfileName(profileName);
|
||||
.setGivenName(profileName);
|
||||
}
|
||||
|
||||
private static SignalContactRecord contact(int key,
|
||||
|
@ -340,10 +377,30 @@ public final class StorageSyncHelperTest {
|
|||
return contactBuilder(key, uuid, e164, profileName).build();
|
||||
}
|
||||
|
||||
private static SignalGroupV1Record groupV1(int key,
|
||||
int groupId,
|
||||
boolean blocked,
|
||||
boolean profileSharing)
|
||||
{
|
||||
return new SignalGroupV1Record.Builder(byteArray(key), byteArray(groupId)).setBlocked(blocked).setProfileSharingEnabled(profileSharing).build();
|
||||
}
|
||||
|
||||
private static StorageSyncHelper.ContactUpdate contactUpdate(SignalContactRecord oldContact, SignalContactRecord newContact) {
|
||||
return new StorageSyncHelper.ContactUpdate(oldContact, newContact);
|
||||
}
|
||||
|
||||
private static StorageSyncHelper.GroupV1Update groupV1Update(SignalGroupV1Record oldGroup, SignalGroupV1Record newGroup) {
|
||||
return new StorageSyncHelper.GroupV1Update(oldGroup, newGroup);
|
||||
}
|
||||
|
||||
private static StorageSyncHelper.RecordUpdate recordUpdate(SignalContactRecord oldContact, SignalContactRecord newContact) {
|
||||
return new StorageSyncHelper.RecordUpdate(SignalStorageRecord.forContact(oldContact), SignalStorageRecord.forContact(newContact));
|
||||
}
|
||||
|
||||
private static StorageSyncHelper.RecordUpdate recordUpdate(SignalGroupV1Record oldGroup, SignalGroupV1Record newGroup) {
|
||||
return new StorageSyncHelper.RecordUpdate(SignalStorageRecord.forGroupV1(oldGroup), SignalStorageRecord.forGroupV1(newGroup));
|
||||
}
|
||||
|
||||
private static SignalStorageRecord unknown(int key) {
|
||||
return SignalStorageRecord.forUnknown(byteArray(key), UNKNOWN_TYPE);
|
||||
}
|
||||
|
@ -372,15 +429,17 @@ public final class StorageSyncHelperTest {
|
|||
}
|
||||
|
||||
private static class TestGenerator implements StorageSyncHelper.KeyGenerator {
|
||||
private final byte[] key;
|
||||
private final int[] keys;
|
||||
|
||||
private TestGenerator(int key) {
|
||||
this.key = byteArray(key);
|
||||
private int index = 0;
|
||||
|
||||
private TestGenerator(int... keys) {
|
||||
this.keys = keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull byte[] generate() {
|
||||
return key;
|
||||
return byteArray(keys[index++]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,11 +17,14 @@ import org.whispersystems.libsignal.ecc.ECPublicKey;
|
|||
import org.whispersystems.libsignal.logging.Log;
|
||||
import org.whispersystems.libsignal.state.PreKeyRecord;
|
||||
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.FeatureFlags;
|
||||
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NoContentException;
|
||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
||||
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite;
|
||||
|
@ -32,6 +35,7 @@ import org.whispersystems.signalservice.api.storage.SignalStorageCipher;
|
|||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageModels;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||
import org.whispersystems.signalservice.api.storage.StorageManifestKey;
|
||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||
|
@ -66,6 +70,7 @@ import java.security.NoSuchAlgorithmException;
|
|||
import java.security.SignatureException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
|
@ -404,41 +409,48 @@ public class SignalServiceAccountManager {
|
|||
}
|
||||
}
|
||||
|
||||
public Optional<SignalStorageManifest> getStorageManifest(byte[] storageServiceKey) throws IOException, InvalidKeyException {
|
||||
public Optional<SignalStorageManifest> getStorageManifestIfDifferentVersion(StorageKey storageKey, long manifestVersion) throws IOException, InvalidKeyException {
|
||||
try {
|
||||
SignalStorageCipher cipher = new SignalStorageCipher(storageServiceKey);
|
||||
String authToken = this.pushServiceSocket.getStorageAuth();
|
||||
StorageManifest storageManifest = this.pushServiceSocket.getStorageManifest(authToken);
|
||||
byte[] rawRecord = cipher.decrypt(storageManifest.getValue().toByteArray());
|
||||
ManifestRecord manifestRecord = ManifestRecord.parseFrom(rawRecord);
|
||||
List<byte[]> keys = new ArrayList<>(manifestRecord.getKeysCount());
|
||||
String authToken = this.pushServiceSocket.getStorageAuth();
|
||||
StorageManifest storageManifest = this.pushServiceSocket.getStorageManifestIfDifferentVersion(authToken, manifestVersion);
|
||||
|
||||
if (storageManifest.getValue().isEmpty()) {
|
||||
Log.w(TAG, "Got an empty storage manifest!");
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
byte[] rawRecord = SignalStorageCipher.decrypt(storageKey.deriveManifestKey(storageManifest.getVersion()), storageManifest.getValue().toByteArray());
|
||||
ManifestRecord manifestRecord = ManifestRecord.parseFrom(rawRecord);
|
||||
List<byte[]> keys = new ArrayList<>(manifestRecord.getKeysCount());
|
||||
|
||||
for (ByteString key : manifestRecord.getKeysList()) {
|
||||
keys.add(key.toByteArray());
|
||||
}
|
||||
|
||||
return Optional.of(new SignalStorageManifest(manifestRecord.getVersion(), keys));
|
||||
} catch (NotFoundException e) {
|
||||
} catch (NoContentException e) {
|
||||
return Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
public List<SignalStorageRecord> readStorageRecords(byte[] storageServiceKey, List<byte[]> storageKeys) throws IOException, InvalidKeyException {
|
||||
public List<SignalStorageRecord> readStorageRecords(StorageKey storageKey, List<byte[]> storageKeys) throws IOException, InvalidKeyException {
|
||||
ReadOperation.Builder operation = ReadOperation.newBuilder();
|
||||
|
||||
for (byte[] key : storageKeys) {
|
||||
operation.addReadKey(ByteString.copyFrom(key));
|
||||
}
|
||||
|
||||
String authToken = this.pushServiceSocket.getStorageAuth();
|
||||
StorageItems items = this.pushServiceSocket.readStorageItems(authToken, operation.build());
|
||||
String authToken = this.pushServiceSocket.getStorageAuth();
|
||||
StorageItems items = this.pushServiceSocket.readStorageItems(authToken, operation.build());
|
||||
List<SignalStorageRecord> result = new ArrayList<>(items.getItemsCount());
|
||||
|
||||
SignalStorageCipher storageCipher = new SignalStorageCipher(storageServiceKey);
|
||||
List<SignalStorageRecord> result = new ArrayList<>(items.getItemsCount());
|
||||
if (items.getItemsCount() != storageKeys.size()) {
|
||||
Log.w(TAG, "Failed to find all remote keys! Requested: " + storageKeys.size() + ", Found: " + items.getItemsCount());
|
||||
}
|
||||
|
||||
for (StorageItem item : items.getItemsList()) {
|
||||
if (item.hasKey()) {
|
||||
result.add(SignalStorageModels.remoteToLocalStorageRecord(item, storageCipher));
|
||||
result.add(SignalStorageModels.remoteToLocalStorageRecord(item, storageKey));
|
||||
} else {
|
||||
Log.w(TAG, "Encountered a StorageItem with no key! Skipping.");
|
||||
}
|
||||
|
@ -446,15 +458,38 @@ public class SignalServiceAccountManager {
|
|||
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* @return If there was a conflict, the latest {@link SignalStorageManifest}. Otherwise absent.
|
||||
*/
|
||||
public Optional<SignalStorageManifest> resetStorageRecords(StorageKey storageKey,
|
||||
SignalStorageManifest manifest,
|
||||
List<SignalStorageRecord> allRecords)
|
||||
throws IOException, InvalidKeyException
|
||||
{
|
||||
return writeStorageRecords(storageKey, manifest, allRecords, Collections.<byte[]>emptyList(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If there was a conflict, the latest {@link SignalStorageManifest}. Otherwise absent.
|
||||
*/
|
||||
public Optional<SignalStorageManifest> writeStorageRecords(byte[] storageServiceKey,
|
||||
public Optional<SignalStorageManifest> writeStorageRecords(StorageKey storageKey,
|
||||
SignalStorageManifest manifest,
|
||||
List<SignalStorageRecord> inserts,
|
||||
List<byte[]> deletes)
|
||||
throws IOException, InvalidKeyException
|
||||
{
|
||||
return writeStorageRecords(storageKey, manifest, inserts, deletes, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If there was a conflict, the latest {@link SignalStorageManifest}. Otherwise absent.
|
||||
*/
|
||||
private Optional<SignalStorageManifest> writeStorageRecords(StorageKey storageKey,
|
||||
SignalStorageManifest manifest,
|
||||
List<SignalStorageRecord> inserts,
|
||||
List<byte[]> deletes,
|
||||
boolean clearAll)
|
||||
throws IOException, InvalidKeyException
|
||||
{
|
||||
ManifestRecord.Builder manifestRecordBuilder = ManifestRecord.newBuilder().setVersion(manifest.getVersion());
|
||||
|
||||
|
@ -462,29 +497,34 @@ public class SignalServiceAccountManager {
|
|||
manifestRecordBuilder.addKeys(ByteString.copyFrom(key));
|
||||
}
|
||||
|
||||
String authToken = this.pushServiceSocket.getStorageAuth();
|
||||
SignalStorageCipher cipher = new SignalStorageCipher(storageServiceKey);
|
||||
byte[] encryptedRecord = cipher.encrypt(manifestRecordBuilder.build().toByteArray());
|
||||
StorageManifest storageManifest = StorageManifest.newBuilder()
|
||||
String authToken = this.pushServiceSocket.getStorageAuth();
|
||||
StorageManifestKey manifestKey = storageKey.deriveManifestKey(manifest.getVersion());
|
||||
byte[] encryptedRecord = SignalStorageCipher.encrypt(manifestKey, manifestRecordBuilder.build().toByteArray());
|
||||
StorageManifest storageManifest = StorageManifest.newBuilder()
|
||||
.setVersion(manifest.getVersion())
|
||||
.setValue(ByteString.copyFrom(encryptedRecord))
|
||||
.build();
|
||||
WriteOperation.Builder writeBuilder = WriteOperation.newBuilder().setManifest(storageManifest);
|
||||
WriteOperation.Builder writeBuilder = WriteOperation.newBuilder().setManifest(storageManifest);
|
||||
|
||||
for (SignalStorageRecord insert : inserts) {
|
||||
writeBuilder.addInsertItem(SignalStorageModels.localToRemoteStorageRecord(insert, cipher));
|
||||
writeBuilder.addInsertItem(SignalStorageModels.localToRemoteStorageRecord(insert, storageKey));
|
||||
}
|
||||
|
||||
for (byte[] delete : deletes) {
|
||||
writeBuilder.addDeleteKey(ByteString.copyFrom(delete));
|
||||
if (clearAll) {
|
||||
writeBuilder.setClearAll(true);
|
||||
} else {
|
||||
for (byte[] delete : deletes) {
|
||||
writeBuilder.addDeleteKey(ByteString.copyFrom(delete));
|
||||
}
|
||||
}
|
||||
|
||||
Optional<StorageManifest> conflict = this.pushServiceSocket.writeStorageContacts(authToken, writeBuilder.build());
|
||||
|
||||
if (conflict.isPresent()) {
|
||||
byte[] rawManifestRecord = cipher.decrypt(conflict.get().getValue().toByteArray());
|
||||
ManifestRecord record = ManifestRecord.parseFrom(rawManifestRecord);
|
||||
List<byte[]> keys = new ArrayList<>(record.getKeysCount());
|
||||
StorageManifestKey conflictKey = storageKey.deriveManifestKey(conflict.get().getVersion());
|
||||
byte[] rawManifestRecord = SignalStorageCipher.decrypt(conflictKey, conflict.get().getValue().toByteArray());
|
||||
ManifestRecord record = ManifestRecord.parseFrom(rawManifestRecord);
|
||||
List<byte[]> keys = new ArrayList<>(record.getKeysCount());
|
||||
|
||||
for (ByteString key : record.getKeysList()) {
|
||||
keys.add(key.toByteArray());
|
||||
|
|
|
@ -36,6 +36,7 @@ import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMess
|
|||
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.KeysMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||
|
@ -305,6 +306,8 @@ public class SignalServiceMessageSender {
|
|||
content = createMultiDeviceFetchTypeContent(message.getFetchType().get());
|
||||
} else if (message.getMessageRequestResponse().isPresent()) {
|
||||
content = createMultiDeviceMessageRequestResponseContent(message.getMessageRequestResponse().get());
|
||||
} else if (message.getKeys().isPresent()) {
|
||||
content = createMultiDeviceSyncKeysContent(message.getKeys().get());
|
||||
} else if (message.getVerified().isPresent()) {
|
||||
sendMessage(message.getVerified().get(), unidentifiedAccess);
|
||||
return;
|
||||
|
@ -822,8 +825,8 @@ public class SignalServiceMessageSender {
|
|||
}
|
||||
|
||||
private byte[] createMultiDeviceMessageRequestResponseContent(MessageRequestResponseMessage message) {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
|
||||
Content.Builder container = Content.newBuilder();
|
||||
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
|
||||
SyncMessage.MessageRequestResponse.Builder responseMessage = SyncMessage.MessageRequestResponse.newBuilder();
|
||||
|
||||
if (message.getGroupId().isPresent()) {
|
||||
|
@ -863,6 +866,20 @@ public class SignalServiceMessageSender {
|
|||
return container.setSyncMessage(syncMessage).build().toByteArray();
|
||||
}
|
||||
|
||||
private byte[] createMultiDeviceSyncKeysContent(KeysMessage keysMessage) {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
|
||||
SyncMessage.Keys.Builder builder = SyncMessage.Keys.newBuilder();
|
||||
|
||||
if (keysMessage.getStorageService().isPresent()) {
|
||||
builder.setStorageService(ByteString.copyFrom(keysMessage.getStorageService().get().serialize()));
|
||||
} else {
|
||||
Log.w(TAG, "Invalid keys message!");
|
||||
}
|
||||
|
||||
return container.setSyncMessage(syncMessage.setKeys(builder)).build().toByteArray();
|
||||
}
|
||||
|
||||
private byte[] createMultiDeviceVerifiedContent(VerifiedMessage verifiedMessage, byte[] nullMessage) {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package org.whispersystems.signalservice.api.kbs;
|
||||
|
||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
||||
import org.whispersystems.signalservice.internal.util.Hex;
|
||||
import org.whispersystems.util.StringUtil;
|
||||
|
||||
|
@ -30,8 +31,8 @@ public final class MasterKey {
|
|||
return Hex.toStringCondensed(derive("Registration Lock"));
|
||||
}
|
||||
|
||||
public byte[] deriveStorageServiceKey() {
|
||||
return derive("Storage Service Encryption");
|
||||
public StorageKey deriveStorageServiceKey() {
|
||||
return new StorageKey(derive("Storage Service Encryption"));
|
||||
}
|
||||
|
||||
private byte[] derive(String keyName) {
|
||||
|
|
|
@ -2,16 +2,17 @@ package org.whispersystems.signalservice.api.messages.multidevice;
|
|||
|
||||
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
||||
|
||||
public class KeysMessage {
|
||||
|
||||
private final Optional<byte[]> storageService;
|
||||
private final Optional<StorageKey> storageService;
|
||||
|
||||
public KeysMessage(Optional<byte[]> storageService) {
|
||||
public KeysMessage(Optional<StorageKey> storageService) {
|
||||
this.storageService = storageService;
|
||||
}
|
||||
|
||||
public Optional<byte[]> getStorageService() {
|
||||
public Optional<StorageKey> getStorageService() {
|
||||
return storageService;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||
*
|
||||
* Licensed according to the LICENSE file in this repository.
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.push.exceptions;
|
||||
|
||||
public class NoContentException extends NonSuccessfulResponseCodeException {
|
||||
public NoContentException(String s) {
|
||||
super(s);
|
||||
}
|
||||
}
|
|
@ -7,11 +7,12 @@ import org.whispersystems.signalservice.api.util.OptionalUtil;
|
|||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class SignalContactRecord {
|
||||
public final class SignalContactRecord implements SignalRecord {
|
||||
|
||||
private final byte[] key;
|
||||
private final SignalServiceAddress address;
|
||||
private final Optional<String> profileName;
|
||||
private final Optional<String> givenName;
|
||||
private final Optional<String> familyName;
|
||||
private final Optional<byte[]> profileKey;
|
||||
private final Optional<String> username;
|
||||
private final Optional<byte[]> identityKey;
|
||||
|
@ -23,7 +24,8 @@ public final class SignalContactRecord {
|
|||
|
||||
private SignalContactRecord(byte[] key,
|
||||
SignalServiceAddress address,
|
||||
String profileName,
|
||||
String givenName,
|
||||
String familyName,
|
||||
byte[] profileKey,
|
||||
String username,
|
||||
byte[] identityKey,
|
||||
|
@ -35,7 +37,8 @@ public final class SignalContactRecord {
|
|||
{
|
||||
this.key = key;
|
||||
this.address = address;
|
||||
this.profileName = Optional.fromNullable(profileName);
|
||||
this.givenName = Optional.fromNullable(givenName);
|
||||
this.familyName = Optional.fromNullable(familyName);
|
||||
this.profileKey = Optional.fromNullable(profileKey);
|
||||
this.username = Optional.fromNullable(username);
|
||||
this.identityKey = Optional.fromNullable(identityKey);
|
||||
|
@ -46,6 +49,7 @@ public final class SignalContactRecord {
|
|||
this.protoVersion = protoVersion;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getKey() {
|
||||
return key;
|
||||
}
|
||||
|
@ -54,8 +58,12 @@ public final class SignalContactRecord {
|
|||
return address;
|
||||
}
|
||||
|
||||
public Optional<String> getProfileName() {
|
||||
return profileName;
|
||||
public Optional<String> getGivenName() {
|
||||
return givenName;
|
||||
}
|
||||
|
||||
public Optional<String> getFamilyName() {
|
||||
return familyName;
|
||||
}
|
||||
|
||||
public Optional<byte[]> getProfileKey() {
|
||||
|
@ -99,7 +107,8 @@ public final class SignalContactRecord {
|
|||
profileSharingEnabled == contact.profileSharingEnabled &&
|
||||
Arrays.equals(key, contact.key) &&
|
||||
Objects.equals(address, contact.address) &&
|
||||
profileName.equals(contact.profileName) &&
|
||||
givenName.equals(contact.givenName) &&
|
||||
familyName.equals(contact.familyName) &&
|
||||
OptionalUtil.byteArrayEquals(profileKey, contact.profileKey) &&
|
||||
username.equals(contact.username) &&
|
||||
OptionalUtil.byteArrayEquals(identityKey, contact.identityKey) &&
|
||||
|
@ -109,7 +118,7 @@ public final class SignalContactRecord {
|
|||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Objects.hash(address, profileName, username, identityState, blocked, profileSharingEnabled, nickname);
|
||||
int result = Objects.hash(address, givenName, familyName, username, identityState, blocked, profileSharingEnabled, nickname);
|
||||
result = 31 * result + Arrays.hashCode(key);
|
||||
result = 31 * result + OptionalUtil.byteArrayHashCode(profileKey);
|
||||
result = 31 * result + OptionalUtil.byteArrayHashCode(identityKey);
|
||||
|
@ -120,7 +129,8 @@ public final class SignalContactRecord {
|
|||
private final byte[] key;
|
||||
private final SignalServiceAddress address;
|
||||
|
||||
private String profileName;
|
||||
private String givenName;
|
||||
private String familyName;
|
||||
private byte[] profileKey;
|
||||
private String username;
|
||||
private byte[] identityKey;
|
||||
|
@ -135,8 +145,13 @@ public final class SignalContactRecord {
|
|||
this.address = address;
|
||||
}
|
||||
|
||||
public Builder setProfileName(String profileName) {
|
||||
this.profileName = profileName;
|
||||
public Builder setGivenName(String givenName) {
|
||||
this.givenName = givenName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setFamilyName(String familyName) {
|
||||
this.familyName = familyName;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -183,7 +198,8 @@ public final class SignalContactRecord {
|
|||
public SignalContactRecord build() {
|
||||
return new SignalContactRecord(key,
|
||||
address,
|
||||
profileName,
|
||||
givenName,
|
||||
familyName,
|
||||
profileKey,
|
||||
username,
|
||||
identityKey,
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.OptionalUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class SignalGroupV1Record implements SignalRecord {
|
||||
|
||||
private final byte[] key;
|
||||
private final byte[] groupId;
|
||||
private final boolean blocked;
|
||||
private final boolean profileSharingEnabled;
|
||||
|
||||
private SignalGroupV1Record(byte[] key, byte[] groupId, boolean blocked, boolean profileSharingEnabled) {
|
||||
this.key = key;
|
||||
this.groupId = groupId;
|
||||
this.blocked = blocked;
|
||||
this.profileSharingEnabled = profileSharingEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public byte[] getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public boolean isBlocked() {
|
||||
return blocked;
|
||||
}
|
||||
|
||||
public boolean isProfileSharingEnabled() {
|
||||
return profileSharingEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SignalGroupV1Record that = (SignalGroupV1Record) o;
|
||||
return blocked == that.blocked &&
|
||||
profileSharingEnabled == that.profileSharingEnabled &&
|
||||
Arrays.equals(key, that.key) &&
|
||||
Arrays.equals(groupId, that.groupId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Objects.hash(blocked, profileSharingEnabled);
|
||||
result = 31 * result + Arrays.hashCode(key);
|
||||
result = 31 * result + Arrays.hashCode(groupId);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private final byte[] key;
|
||||
private final byte[] groupId;
|
||||
private boolean blocked;
|
||||
private boolean profileSharingEnabled;
|
||||
|
||||
public Builder(byte[] key, byte[] groupId) {
|
||||
this.key = key;
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
public Builder setBlocked(boolean blocked) {
|
||||
this.blocked = blocked;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setProfileSharingEnabled(boolean profileSharingEnabled) {
|
||||
this.profileSharingEnabled = profileSharingEnabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SignalGroupV1Record build() {
|
||||
return new SignalGroupV1Record(key, groupId, blocked, profileSharingEnabled);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
public interface SignalRecord {
|
||||
byte[] getKey();
|
||||
}
|
|
@ -1,22 +1,13 @@
|
|||
package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.util.ByteUtil;
|
||||
import org.whispersystems.signalservice.api.crypto.CryptoUtil;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.CipherInputStream;
|
||||
import javax.crypto.CipherOutputStream;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
|
@ -27,34 +18,28 @@ import javax.crypto.spec.SecretKeySpec;
|
|||
*/
|
||||
public class SignalStorageCipher {
|
||||
|
||||
private final byte[] key;
|
||||
|
||||
public SignalStorageCipher(byte[] storageServiceKey) {
|
||||
this.key = storageServiceKey;
|
||||
}
|
||||
|
||||
public byte[] encrypt(byte[] data) {
|
||||
public static byte[] encrypt(StorageCipherKey key, byte[] data) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
byte[] iv = Util.getSecretBytes(16);
|
||||
|
||||
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, iv));
|
||||
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key.serialize(), "AES"), new GCMParameterSpec(128, iv));
|
||||
byte[] ciphertext = cipher.doFinal(data);
|
||||
|
||||
return ByteUtil.combine(iv, ciphertext);
|
||||
return Util.join(iv, ciphertext);
|
||||
} catch (NoSuchAlgorithmException | java.security.InvalidKeyException | InvalidAlgorithmParameterException | NoSuchPaddingException | BadPaddingException | IllegalBlockSizeException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] decrypt(byte[] data) throws InvalidKeyException {
|
||||
public static byte[] decrypt(StorageCipherKey key, byte[] data) throws InvalidKeyException {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
byte[][] split = Util.split(data, 16, data.length - 16);
|
||||
byte[] iv = split[0];
|
||||
byte[] cipherText = split[1];
|
||||
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, iv));
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.serialize(), "AES"), new GCMParameterSpec(128, iv));
|
||||
return cipher.doFinal(cipherText);
|
||||
} catch (java.security.InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) {
|
||||
throw new InvalidKeyException(e);
|
||||
|
|
|
@ -6,6 +6,7 @@ import org.whispersystems.libsignal.InvalidKeyException;
|
|||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.GroupV1Record;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.StorageItem;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.StorageRecord;
|
||||
|
||||
|
@ -13,31 +14,37 @@ import java.io.IOException;
|
|||
|
||||
public final class SignalStorageModels {
|
||||
|
||||
public static SignalStorageRecord remoteToLocalStorageRecord(StorageItem item, SignalStorageCipher cipher) throws IOException, InvalidKeyException {
|
||||
byte[] rawRecord = cipher.decrypt(item.getValue().toByteArray());
|
||||
StorageRecord record = StorageRecord.parseFrom(rawRecord);
|
||||
byte[] storageKey = item.getKey().toByteArray();
|
||||
public static SignalStorageRecord remoteToLocalStorageRecord(StorageItem item, StorageKey storageKey) throws IOException, InvalidKeyException {
|
||||
byte[] key = item.getKey().toByteArray();
|
||||
byte[] rawRecord = SignalStorageCipher.decrypt(storageKey.deriveItemKey(key), item.getValue().toByteArray());
|
||||
StorageRecord record = StorageRecord.parseFrom(rawRecord);
|
||||
|
||||
if (record.getType() == StorageRecord.Type.CONTACT_VALUE && record.hasContact()) {
|
||||
return SignalStorageRecord.forContact(storageKey, remoteToLocalContactRecord(storageKey, record.getContact()));
|
||||
} else {
|
||||
return SignalStorageRecord.forUnknown(storageKey, record.getType());
|
||||
switch (record.getType()) {
|
||||
case StorageRecord.Type.CONTACT_VALUE:
|
||||
return SignalStorageRecord.forContact(key, remoteToLocalContactRecord(key, record.getContact()));
|
||||
case StorageRecord.Type.GROUPV1_VALUE:
|
||||
return SignalStorageRecord.forGroupV1(key, remoteToLocalGroupV1Record(key, record.getGroupV1()));
|
||||
default:
|
||||
return SignalStorageRecord.forUnknown(key, record.getType());
|
||||
}
|
||||
}
|
||||
|
||||
public static StorageItem localToRemoteStorageRecord(SignalStorageRecord record, SignalStorageCipher cipher) throws IOException {
|
||||
public static StorageItem localToRemoteStorageRecord(SignalStorageRecord record, StorageKey storageKey) {
|
||||
StorageRecord.Builder builder = StorageRecord.newBuilder();
|
||||
|
||||
if (record.getContact().isPresent()) {
|
||||
builder.setContact(localToRemoteContactRecord(record.getContact().get()));
|
||||
} else if (record.getGroupV1().isPresent()) {
|
||||
builder.setGroupV1(localToRemoteGroupV1Record(record.getGroupV1().get()));
|
||||
} else {
|
||||
throw new InvalidStorageWriteError();
|
||||
}
|
||||
|
||||
builder.setType(record.getType());
|
||||
|
||||
StorageRecord remoteRecord = builder.build();
|
||||
byte[] encryptedRecord = cipher.encrypt(remoteRecord.toByteArray());
|
||||
StorageRecord remoteRecord = builder.build();
|
||||
StorageItemKey itemKey = storageKey.deriveItemKey(record.getKey());
|
||||
byte[] encryptedRecord = SignalStorageCipher.encrypt(itemKey, remoteRecord.toByteArray());
|
||||
|
||||
return StorageItem.newBuilder()
|
||||
.setKey(ByteString.copyFrom(record.getKey()))
|
||||
|
@ -45,9 +52,9 @@ public final class SignalStorageModels {
|
|||
.build();
|
||||
}
|
||||
|
||||
public static SignalContactRecord remoteToLocalContactRecord(byte[] storageKey, ContactRecord contact) throws IOException {
|
||||
private static SignalContactRecord remoteToLocalContactRecord(byte[] key, ContactRecord contact) {
|
||||
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(contact.getServiceUuid()), contact.getServiceE164());
|
||||
SignalContactRecord.Builder builder = new SignalContactRecord.Builder(storageKey, address);
|
||||
SignalContactRecord.Builder builder = new SignalContactRecord.Builder(key, address);
|
||||
|
||||
if (contact.hasBlocked()) {
|
||||
builder.setBlocked(contact.getBlocked());
|
||||
|
@ -66,8 +73,12 @@ public final class SignalStorageModels {
|
|||
builder.setProfileKey(contact.getProfile().getKey().toByteArray());
|
||||
}
|
||||
|
||||
if (contact.getProfile().hasName()) {
|
||||
builder.setProfileName(contact.getProfile().getName());
|
||||
if (contact.getProfile().hasGivenName()) {
|
||||
builder.setGivenName(contact.getProfile().getGivenName());
|
||||
}
|
||||
|
||||
if (contact.getProfile().hasFamilyName()) {
|
||||
builder.setFamilyName(contact.getProfile().getFamilyName());
|
||||
}
|
||||
|
||||
if (contact.getProfile().hasUsername()) {
|
||||
|
@ -84,7 +95,7 @@ public final class SignalStorageModels {
|
|||
switch (contact.getIdentity().getState()) {
|
||||
case VERIFIED: builder.setIdentityState(SignalContactRecord.IdentityState.VERIFIED);
|
||||
case UNVERIFIED: builder.setIdentityState(SignalContactRecord.IdentityState.UNVERIFIED);
|
||||
default: builder.setIdentityState(SignalContactRecord.IdentityState.VERIFIED);
|
||||
default: builder.setIdentityState(SignalContactRecord.IdentityState.DEFAULT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -92,7 +103,21 @@ public final class SignalStorageModels {
|
|||
return builder.build();
|
||||
}
|
||||
|
||||
public static ContactRecord localToRemoteContactRecord(SignalContactRecord contact) {
|
||||
private static SignalGroupV1Record remoteToLocalGroupV1Record(byte[] key, GroupV1Record groupV1) {
|
||||
SignalGroupV1Record.Builder builder = new SignalGroupV1Record.Builder(key, groupV1.getId().toByteArray());
|
||||
|
||||
if (groupV1.hasBlocked()) {
|
||||
builder.setBlocked(groupV1.getBlocked());
|
||||
}
|
||||
|
||||
if (groupV1.hasWhitelisted()) {
|
||||
builder.setProfileSharingEnabled(groupV1.getWhitelisted());
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static ContactRecord localToRemoteContactRecord(SignalContactRecord contact) {
|
||||
ContactRecord.Builder contactRecordBuilder = ContactRecord.newBuilder()
|
||||
.setBlocked(contact.isBlocked())
|
||||
.setWhitelisted(contact.isProfileSharingEnabled());
|
||||
|
@ -128,8 +153,12 @@ public final class SignalStorageModels {
|
|||
profileBuilder.setKey(ByteString.copyFrom(contact.getProfileKey().get()));
|
||||
}
|
||||
|
||||
if (contact.getProfileName().isPresent()) {
|
||||
profileBuilder.setName(contact.getProfileName().get());
|
||||
if (contact.getGivenName().isPresent()) {
|
||||
profileBuilder.setGivenName(contact.getGivenName().get());
|
||||
}
|
||||
|
||||
if (contact.getFamilyName().isPresent()) {
|
||||
profileBuilder.setFamilyName(contact.getFamilyName().get());
|
||||
}
|
||||
|
||||
if (contact.getUsername().isPresent()) {
|
||||
|
@ -141,6 +170,14 @@ public final class SignalStorageModels {
|
|||
return contactRecordBuilder.build();
|
||||
}
|
||||
|
||||
private static GroupV1Record localToRemoteGroupV1Record(SignalGroupV1Record groupV1) {
|
||||
return GroupV1Record.newBuilder()
|
||||
.setId(ByteString.copyFrom(groupV1.getGroupId()))
|
||||
.setBlocked(groupV1.isBlocked())
|
||||
.setWhitelisted(groupV1.isProfileSharingEnabled())
|
||||
.build();
|
||||
}
|
||||
|
||||
private static class InvalidStorageWriteError extends Error {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,26 +6,45 @@ import org.whispersystems.signalservice.internal.storage.protos.StorageRecord;
|
|||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public class SignalStorageRecord {
|
||||
public class SignalStorageRecord implements SignalRecord {
|
||||
|
||||
private final byte[] key;
|
||||
private final int type;
|
||||
private final Optional<SignalContactRecord> contact;
|
||||
private final Optional<SignalGroupV1Record> groupV1;
|
||||
|
||||
public static SignalStorageRecord forContact(SignalContactRecord contact) {
|
||||
return forContact(contact.getKey(), contact);
|
||||
}
|
||||
|
||||
public static SignalStorageRecord forContact(byte[] key, SignalContactRecord contact) {
|
||||
return new SignalStorageRecord(key, StorageRecord.Type.CONTACT_VALUE, Optional.of(contact));
|
||||
return new SignalStorageRecord(key, StorageRecord.Type.CONTACT_VALUE, Optional.of(contact), Optional.<SignalGroupV1Record>absent());
|
||||
}
|
||||
|
||||
public static SignalStorageRecord forGroupV1(SignalGroupV1Record groupV1) {
|
||||
return forGroupV1(groupV1.getKey(), groupV1);
|
||||
}
|
||||
|
||||
public static SignalStorageRecord forGroupV1(byte[] key, SignalGroupV1Record groupV1) {
|
||||
return new SignalStorageRecord(key, StorageRecord.Type.GROUPV1_VALUE, Optional.<SignalContactRecord>absent(), Optional.of(groupV1));
|
||||
}
|
||||
|
||||
public static SignalStorageRecord forUnknown(byte[] key, int type) {
|
||||
return new SignalStorageRecord(key, type, Optional.<SignalContactRecord>absent());
|
||||
return new SignalStorageRecord(key, type, Optional.<SignalContactRecord>absent(), Optional.<SignalGroupV1Record>absent());
|
||||
}
|
||||
|
||||
private SignalStorageRecord(byte key[], int type, Optional<SignalContactRecord> contact) {
|
||||
private SignalStorageRecord(byte[] key,
|
||||
int type,
|
||||
Optional<SignalContactRecord> contact,
|
||||
Optional<SignalGroupV1Record> groupV1)
|
||||
{
|
||||
this.key = key;
|
||||
this.type = type;
|
||||
this.contact = contact;
|
||||
this.groupV1 = groupV1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getKey() {
|
||||
return key;
|
||||
}
|
||||
|
@ -38,8 +57,12 @@ public class SignalStorageRecord {
|
|||
return contact;
|
||||
}
|
||||
|
||||
public Optional<SignalGroupV1Record> getGroupV1() {
|
||||
return groupV1;
|
||||
}
|
||||
|
||||
public boolean isUnknown() {
|
||||
return !contact.isPresent();
|
||||
return !contact.isPresent() && !groupV1.isPresent();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -48,13 +71,14 @@ public class SignalStorageRecord {
|
|||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SignalStorageRecord record = (SignalStorageRecord) o;
|
||||
return type == record.type &&
|
||||
Arrays.equals(key, record.key) &&
|
||||
contact.equals(record.contact);
|
||||
Arrays.equals(key, record.key) &&
|
||||
contact.equals(record.contact) &&
|
||||
groupV1.equals(record.groupV1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Objects.hash(type, contact);
|
||||
int result = Objects.hash(type, contact, groupV1);
|
||||
result = 31 * result + Arrays.hashCode(key);
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
public interface StorageCipherKey {
|
||||
byte[] serialize();
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Key used to encrypt individual storage items in the storage service.
|
||||
*
|
||||
* Created via {@link StorageKey#deriveItemKey(byte[]) }.
|
||||
*/
|
||||
public final class StorageItemKey implements StorageCipherKey {
|
||||
|
||||
private static final int LENGTH = 32;
|
||||
|
||||
private final byte[] key;
|
||||
|
||||
StorageItemKey(byte[] key) {
|
||||
if (key.length != LENGTH) throw new AssertionError();
|
||||
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
return key.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == null || o.getClass() != getClass()) return false;
|
||||
|
||||
return Arrays.equals(((StorageItemKey) o).key, key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Arrays.hashCode(key);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.internal.util.Hex;
|
||||
import org.whispersystems.util.Base64;
|
||||
import org.whispersystems.util.StringUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.whispersystems.signalservice.api.crypto.CryptoUtil.hmacSha256;
|
||||
|
||||
/**
|
||||
* Key used to encrypt data on the storage service. Not used directly -- instead we used keys that
|
||||
* are derived for each item we're storing.
|
||||
*
|
||||
* Created via {@link MasterKey#deriveStorageServiceKey()}.
|
||||
*/
|
||||
public final class StorageKey {
|
||||
|
||||
private static final int LENGTH = 32;
|
||||
|
||||
private final byte[] key;
|
||||
|
||||
public StorageKey(byte[] key) {
|
||||
if (key.length != LENGTH) throw new AssertionError();
|
||||
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public StorageManifestKey deriveManifestKey(long version) {
|
||||
return new StorageManifestKey(derive("Manifest_" + version));
|
||||
}
|
||||
|
||||
public StorageItemKey deriveItemKey(byte[] key) {
|
||||
return new StorageItemKey(derive("Item_" + Base64.encodeBytes(key)));
|
||||
}
|
||||
|
||||
private byte[] derive(String keyName) {
|
||||
return hmacSha256(key, StringUtil.utf8(keyName));
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
return key.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == null || o.getClass() != getClass()) return false;
|
||||
|
||||
return Arrays.equals(((StorageKey) o).key, key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Arrays.hashCode(key);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Key used to encrypt a manifest in the storage service.
|
||||
*
|
||||
* Created via {@link StorageKey#deriveManifestKey(long)}.
|
||||
*/
|
||||
public final class StorageManifestKey implements StorageCipherKey {
|
||||
|
||||
private static final int LENGTH = 32;
|
||||
|
||||
private final byte[] key;
|
||||
|
||||
StorageManifestKey(byte[] key) {
|
||||
if (key.length != LENGTH) throw new AssertionError();
|
||||
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
return key.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == null || o.getClass() != getClass()) return false;
|
||||
|
||||
return Arrays.equals(((StorageManifestKey) o).key, key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Arrays.hashCode(key);
|
||||
}
|
||||
}
|
|
@ -39,6 +39,7 @@ import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedE
|
|||
import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ContactManifestMismatchException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ExpectationFailedException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NoContentException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
|
@ -787,6 +788,16 @@ public class PushServiceSocket {
|
|||
return StorageManifest.parseFrom(response.body().bytes());
|
||||
}
|
||||
|
||||
public StorageManifest getStorageManifestIfDifferentVersion(String authToken, long version) throws IOException {
|
||||
Response response = makeStorageRequest(authToken, "/v1/storage/manifest/version/" + version, "GET", null);
|
||||
|
||||
if (response.body() == null) {
|
||||
throw new IOException("Missing body!");
|
||||
}
|
||||
|
||||
return StorageManifest.parseFrom(response.body().bytes());
|
||||
}
|
||||
|
||||
public StorageItems readStorageItems(String authToken, ReadOperation operation) throws IOException {
|
||||
Response response = makeStorageRequest(authToken, "/v1/storage/read", "PUT", operation.toByteArray());
|
||||
|
||||
|
@ -1119,8 +1130,8 @@ public class PushServiceSocket {
|
|||
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
|
||||
Log.w(TAG, "Push service URL: " + connectionHolder.getUrl());
|
||||
Log.w(TAG, "Opening URL: " + String.format("%s%s", connectionHolder.getUrl(), urlFragment));
|
||||
Log.d(TAG, "Push service URL: " + connectionHolder.getUrl());
|
||||
Log.d(TAG, "Opening URL: " + String.format("%s%s", connectionHolder.getUrl(), urlFragment));
|
||||
|
||||
Request.Builder request = new Request.Builder();
|
||||
request.url(String.format("%s%s", connectionHolder.getUrl(), urlFragment));
|
||||
|
@ -1262,6 +1273,8 @@ public class PushServiceSocket {
|
|||
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
|
||||
Log.d(TAG, "Opening URL: " + String.format("%s%s", connectionHolder.getUrl(), path));
|
||||
|
||||
Request.Builder request = new Request.Builder().url(connectionHolder.getUrl() + path);
|
||||
|
||||
if (body != null) {
|
||||
|
@ -1289,7 +1302,7 @@ public class PushServiceSocket {
|
|||
try {
|
||||
response = call.execute();
|
||||
|
||||
if (response.isSuccessful()) {
|
||||
if (response.isSuccessful() && response.code() != 204) {
|
||||
return response;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
|
@ -1301,6 +1314,8 @@ public class PushServiceSocket {
|
|||
}
|
||||
|
||||
switch (response.code()) {
|
||||
case 204:
|
||||
throw new NoContentException("No content!");
|
||||
case 401:
|
||||
case 403:
|
||||
throw new AuthorizationFailedException("Authorization failed!");
|
||||
|
|
|
@ -33,16 +33,19 @@ message WriteOperation {
|
|||
optional StorageManifest manifest = 1;
|
||||
repeated StorageItem insertItem = 2;
|
||||
repeated bytes deleteKey = 3;
|
||||
optional bool clearAll = 4;
|
||||
}
|
||||
|
||||
message StorageRecord {
|
||||
enum Type {
|
||||
UNKNOWN = 0;
|
||||
CONTACT = 1;
|
||||
GROUPV1 = 2;
|
||||
}
|
||||
|
||||
optional uint32 type = 1;
|
||||
optional ContactRecord contact = 2;
|
||||
optional GroupV1Record groupV1 = 3;
|
||||
}
|
||||
|
||||
message ContactRecord {
|
||||
|
@ -58,9 +61,10 @@ message ContactRecord {
|
|||
}
|
||||
|
||||
message Profile {
|
||||
optional string name = 1;
|
||||
optional bytes key = 2;
|
||||
optional string username = 3;
|
||||
optional string givenName = 1;
|
||||
optional string familyName = 4;
|
||||
optional bytes key = 2;
|
||||
optional string username = 3;
|
||||
}
|
||||
|
||||
optional string serviceUuid = 1;
|
||||
|
@ -72,6 +76,12 @@ message ContactRecord {
|
|||
optional string nickname = 7;
|
||||
}
|
||||
|
||||
message GroupV1Record {
|
||||
optional bytes id = 1;
|
||||
optional bool blocked = 2;
|
||||
optional bool whitelisted = 3;
|
||||
}
|
||||
|
||||
message ManifestRecord {
|
||||
optional uint64 version = 1;
|
||||
repeated bytes keys = 2;
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
|
||||
public class SignalContactRecordTest {
|
||||
|
||||
private static final UUID UUID_A = UuidUtil.parseOrThrow("ebef429e-695e-4f51-bcc4-526a60ac68c7");
|
||||
private static final String E164_A = "+16108675309";
|
||||
|
||||
@Test
|
||||
public void contacts_with_same_identity_key_contents_are_equal() {
|
||||
byte[] profileKey = new byte[32];
|
||||
byte[] profileKeyCopy = profileKey.clone();
|
||||
|
||||
SignalContactRecord a = contactBuilder(1, UUID_A, E164_A, "a").setIdentityKey(profileKey).build();
|
||||
SignalContactRecord b = contactBuilder(1, UUID_A, E164_A, "a").setIdentityKey(profileKeyCopy).build();
|
||||
|
||||
assertEquals(a, b);
|
||||
assertEquals(a.hashCode(), b.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contacts_with_different_identity_key_contents_are_not_equal() {
|
||||
byte[] profileKey = new byte[32];
|
||||
byte[] profileKeyCopy = profileKey.clone();
|
||||
profileKeyCopy[0] = 1;
|
||||
|
||||
SignalContactRecord a = contactBuilder(1, UUID_A, E164_A, "a").setIdentityKey(profileKey).build();
|
||||
SignalContactRecord b = contactBuilder(1, UUID_A, E164_A, "a").setIdentityKey(profileKeyCopy).build();
|
||||
|
||||
assertNotEquals(a, b);
|
||||
assertNotEquals(a.hashCode(), b.hashCode());
|
||||
}
|
||||
|
||||
private static byte[] byteArray(int a) {
|
||||
byte[] bytes = new byte[4];
|
||||
bytes[3] = (byte) a;
|
||||
bytes[2] = (byte)(a >> 8);
|
||||
bytes[1] = (byte)(a >> 16);
|
||||
bytes[0] = (byte)(a >> 24);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static SignalContactRecord.Builder contactBuilder(int key,
|
||||
UUID uuid,
|
||||
String e164,
|
||||
String givenName)
|
||||
{
|
||||
return new SignalContactRecord.Builder(byteArray(key), new SignalServiceAddress(uuid, e164))
|
||||
.setGivenName(givenName);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
|
||||
public class SignalStorageCipherTest {
|
||||
|
||||
@Test
|
||||
public void symmetry() throws InvalidKeyException {
|
||||
StorageItemKey key = new StorageItemKey(Util.getSecretBytes(32));
|
||||
byte[] data = Util.getSecretBytes(1337);
|
||||
|
||||
byte[] ciphertext = SignalStorageCipher.encrypt(key, data);
|
||||
byte[] plaintext = SignalStorageCipher.decrypt(key, ciphertext);
|
||||
|
||||
assertArrayEquals(data, plaintext);
|
||||
}
|
||||
|
||||
@Test(expected = InvalidKeyException.class)
|
||||
public void badKeyOnDecrypt() throws InvalidKeyException {
|
||||
StorageItemKey key = new StorageItemKey(Util.getSecretBytes(32));
|
||||
byte[] data = Util.getSecretBytes(1337);
|
||||
|
||||
byte[] badKey = key.serialize().clone();
|
||||
badKey[0] += 1;
|
||||
|
||||
byte[] ciphertext = SignalStorageCipher.encrypt(key, data);
|
||||
byte[] plaintext = SignalStorageCipher.decrypt(new StorageItemKey(badKey), ciphertext);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue