Update the storage service.

master
Greyson Parrelli 2020-02-10 13:42:43 -05:00
parent 133bd44b85
commit 6184e5f828
44 changed files with 1592 additions and 431 deletions

View File

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

View File

@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.
*/

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

@ -39,8 +39,7 @@ public final class ProfileName implements Parcelable {
return givenName;
}
public @NonNull
String getFamilyName() {
public @NonNull String getFamilyName() {
return familyName;
}

View File

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

View File

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

View File

@ -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 {

View File

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

View File

@ -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;

View File

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

View File

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

View File

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

View File

@ -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) {

View File

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

View File

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

View File

@ -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,

View File

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

View File

@ -0,0 +1,5 @@
package org.whispersystems.signalservice.api.storage;
public interface SignalRecord {
byte[] getKey();
}

View File

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

View File

@ -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 {
}
}

View File

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

View File

@ -0,0 +1,5 @@
package org.whispersystems.signalservice.api.storage;
public interface StorageCipherKey {
byte[] serialize();
}

View File

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

View File

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

View File

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

View File

@ -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!");

View File

@ -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;

View File

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

View File

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