Add contact and key sync message receive support.

This commit is contained in:
Cody Henthorne
2022-01-18 11:10:23 -05:00
committed by Greyson Parrelli
parent c5028720e3
commit c548816daa
17 changed files with 542 additions and 143 deletions

View File

@@ -207,7 +207,7 @@ public class FullBackupImporter extends FullBackupBase {
private static void processAvatar(@NonNull Context context, @NonNull SQLiteDatabase db, @NonNull BackupProtos.Avatar avatar, @NonNull BackupRecordInputStream inputStream) throws IOException {
if (avatar.hasRecipientId()) {
RecipientId recipientId = RecipientId.from(avatar.getRecipientId());
inputStream.readAttachmentTo(AvatarHelper.getOutputStream(context, recipientId), avatar.getLength());
inputStream.readAttachmentTo(AvatarHelper.getOutputStream(context, recipientId, false), avatar.getLength());
} else {
if (avatar.hasName() && SqlUtil.tableExists(db, "recipient_preferences")) {
Log.w(TAG, "Avatar is missing a recipientId. Clearing signal_profile_avatar (legacy) so it can be fetched later.");

View File

@@ -65,10 +65,6 @@ public class ProfileContactPhoto implements ContactPhoto {
}
private long getFileLastModified() {
if (!recipient.isSelf()) {
return 0;
}
return AvatarHelper.getLastModified(ApplicationDependencies.getApplication(), recipient.getId());
}
}

View File

@@ -1524,6 +1524,15 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
}
}
fun setSystemContactName(id: RecipientId, systemContactName: String) {
val values = ContentValues().apply {
put(SYSTEM_JOINED_NAME, systemContactName)
}
if (update(id, values)) {
Recipient.live(id).refresh()
}
}
fun setProfileName(id: RecipientId, profileName: ProfileName) {
val contentValues = ContentValues(1).apply {
put(PROFILE_GIVEN_NAME, profileName.givenName)

View File

@@ -104,6 +104,7 @@ public final class JobManagerFactories {
put(MmsSendJob.KEY, new MmsSendJob.Factory());
put(MultiDeviceBlockedUpdateJob.KEY, new MultiDeviceBlockedUpdateJob.Factory());
put(MultiDeviceConfigurationUpdateJob.KEY, new MultiDeviceConfigurationUpdateJob.Factory());
put(MultiDeviceContactSyncJob.KEY, new MultiDeviceContactSyncJob.Factory());
put(MultiDeviceContactUpdateJob.KEY, new MultiDeviceContactUpdateJob.Factory());
put(MultiDeviceGroupUpdateJob.KEY, new MultiDeviceGroupUpdateJob.Factory());
put(MultiDeviceKeysUpdateJob.KEY, new MultiDeviceKeysUpdateJob.Factory());

View File

@@ -0,0 +1,154 @@
package org.thoughtcrime.securesms.jobs
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobmanager.Data
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.net.NotPushRegisteredException
import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.whispersystems.libsignal.InvalidMessageException
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage.VerifiedState
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException
import org.whispersystems.signalservice.api.util.AttachmentPointerUtil
import java.io.File
import java.io.IOException
import java.io.InputStream
/**
* Sync contact data from primary device.
*/
class MultiDeviceContactSyncJob(parameters: Parameters, private val attachmentPointer: ByteArray) : BaseJob(parameters) {
constructor(contactsAttachment: SignalServiceAttachmentPointer) : this(
Parameters.Builder()
.setQueue("MultiDeviceContactSyncJob")
.build(),
AttachmentPointerUtil.createAttachmentPointer(contactsAttachment).toByteArray()
)
override fun serialize(): Data {
return Data.Builder()
.putBlobAsString(KEY_ATTACHMENT_POINTER, attachmentPointer)
.build()
}
override fun getFactoryKey(): String {
return KEY
}
override fun onRun() {
if (!Recipient.self().isRegistered) {
throw NotPushRegisteredException()
}
if (SignalStore.account().isPrimaryDevice) {
Log.i(TAG, "Sync jobs are for linked devices only")
return
}
val contactAttachment: SignalServiceAttachmentPointer = AttachmentPointerUtil.createSignalAttachmentPointer(attachmentPointer)
try {
val contactsFile: File = BlobProvider.getInstance().forNonAutoEncryptingSingleSessionOnDisk(context)
ApplicationDependencies.getSignalServiceMessageReceiver()
.retrieveAttachment(contactAttachment, contactsFile, MAX_ATTACHMENT_SIZE)
.use(this::processContactFile)
} catch (e: MissingConfigurationException) {
throw IOException(e)
} catch (e: InvalidMessageException) {
throw IOException(e)
}
}
private fun processContactFile(inputStream: InputStream) {
val deviceContacts = DeviceContactsInputStream(inputStream)
val recipients = SignalDatabase.recipients
val threads = SignalDatabase.threads
var contact: DeviceContact? = deviceContacts.read()
while (contact != null) {
val recipient = Recipient.externalPush(context, contact.address.aci, contact.address.number.orNull(), true)
if (recipient.isSelf) {
contact = deviceContacts.read()
continue
}
if (contact.name.isPresent) {
recipients.setSystemContactName(recipient.id, contact.name.get())
}
if (contact.expirationTimer.isPresent) {
recipients.setExpireMessages(recipient.id, contact.expirationTimer.get())
}
if (contact.profileKey.isPresent) {
val profileKey = contact.profileKey.get()
recipients.setProfileKey(recipient.id, profileKey)
}
if (contact.verified.isPresent) {
val verifiedStatus: VerifiedStatus = when (contact.verified.get().verified) {
VerifiedState.VERIFIED -> VerifiedStatus.VERIFIED
VerifiedState.UNVERIFIED -> VerifiedStatus.UNVERIFIED
else -> VerifiedStatus.DEFAULT
}
ApplicationDependencies.getIdentityStore().saveIdentityWithoutSideEffects(
recipient.id,
contact.verified.get().identityKey,
verifiedStatus,
false,
contact.verified.get().timestamp,
true
)
}
recipients.setBlocked(recipient.id, contact.isBlocked)
val threadRecord = threads.getThreadRecord(threads.getThreadIdFor(recipient.id))
if (threadRecord != null && contact.isArchived != threadRecord.isArchived) {
if (contact.isArchived) {
threads.archiveConversation(threadRecord.threadId)
} else {
threads.unarchiveConversation(threadRecord.threadId)
}
}
if (contact.avatar.isPresent) {
try {
AvatarHelper.setSyncAvatar(context, recipient.id, contact.avatar.get().inputStream)
} catch (e: IOException) {
Log.w(TAG, "Unable to set sync avatar for ${recipient.id}")
}
}
contact = deviceContacts.read()
}
}
override fun onShouldRetry(e: Exception): Boolean = false
override fun onFailure() = Unit
class Factory : Job.Factory<MultiDeviceContactSyncJob> {
override fun create(parameters: Parameters, data: Data): MultiDeviceContactSyncJob {
return MultiDeviceContactSyncJob(parameters, data.getStringAsBlob(KEY_ATTACHMENT_POINTER))
}
}
companion object {
const val KEY = "MultiDeviceContactSyncJob"
const val KEY_ATTACHMENT_POINTER = "attachment_pointer"
private const val MAX_ATTACHMENT_SIZE: Long = 100 * 1024 * 1024
private val TAG = Log.tag(MultiDeviceContactSyncJob::class.java)
}
}

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.keyvalue;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.whispersystems.libsignal.util.guava.Preconditions;
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
import org.whispersystems.signalservice.api.storage.StorageKey;
@@ -14,6 +15,7 @@ public class StorageServiceValues extends SignalStoreValues {
private static final String LAST_SYNC_TIME = "storage.last_sync_time";
private static final String NEEDS_ACCOUNT_RESTORE = "storage.needs_account_restore";
private static final String MANIFEST = "storage.manifest";
private static final String SYNC_STORAGE_KEY = "storage.syncStorageKey";
StorageServiceValues(@NonNull KeyValueStore store) {
super(store);
@@ -29,6 +31,9 @@ public class StorageServiceValues extends SignalStoreValues {
}
public synchronized StorageKey getOrCreateStorageKey() {
if (getStore().containsKey(SYNC_STORAGE_KEY)) {
return new StorageKey(getBlob(SYNC_STORAGE_KEY, null));
}
return SignalStore.kbsValues().getOrCreateMasterKey().deriveStorageServiceKey();
}
@@ -61,4 +66,14 @@ public class StorageServiceValues extends SignalStoreValues {
return SignalStorageManifest.EMPTY;
}
}
public synchronized void setStorageKeyFromPrimary(@NonNull StorageKey storageKey) {
Preconditions.checkState(SignalStore.account().isLinkedDevice(), "Can only set storage key directly on linked devices");
putBlob(SYNC_STORAGE_KEY, storageKey.serialize());
}
public void clearStorageKeyFromPrimary() {
Preconditions.checkState(SignalStore.account().isLinkedDevice(), "Can only clear storage key directly on linked devices");
remove(SYNC_STORAGE_KEY);
}
}

View File

@@ -65,6 +65,7 @@ import org.thoughtcrime.securesms.jobs.GroupCallPeekJob;
import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactSyncJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceGroupUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceKeysUpdateJob;
@@ -123,6 +124,7 @@ import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.protocol.DecryptionErrorMessage;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
@@ -139,6 +141,8 @@ import org.whispersystems.signalservice.api.messages.calls.OpaqueMessage;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage;
import org.whispersystems.signalservice.api.messages.multidevice.KeysMessage;
import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage;
import org.whispersystems.signalservice.api.messages.multidevice.OutgoingPaymentMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
@@ -309,6 +313,8 @@ public final class MessageContentProcessor {
else if (syncMessage.getFetchType().isPresent()) handleSynchronizeFetchMessage(syncMessage.getFetchType().get(), content.getTimestamp());
else if (syncMessage.getMessageRequestResponse().isPresent()) handleSynchronizeMessageRequestResponse(syncMessage.getMessageRequestResponse().get(), content.getTimestamp());
else if (syncMessage.getOutgoingPaymentMessage().isPresent()) handleSynchronizeOutgoingPayment(content, syncMessage.getOutgoingPaymentMessage().get());
else if (syncMessage.getKeys().isPresent()) handleSynchronizeKeys(syncMessage.getKeys().get(), content.getTimestamp());
else if (syncMessage.getContacts().isPresent()) handleSynchronizeContacts(syncMessage.getContacts().get(), content.getTimestamp());
else warn(String.valueOf(content.getTimestamp()), "Contains no known sync types...");
} else if (content.getCallMessage().isPresent()) {
log(String.valueOf(content.getTimestamp()), "Got call message...");
@@ -1081,6 +1087,35 @@ public final class MessageContentProcessor {
log("Inserted synchronized payment " + uuid);
}
private void handleSynchronizeKeys(@NonNull KeysMessage keysMessage, long envelopeTimestamp) {
if (SignalStore.account().isLinkedDevice()) {
log(envelopeTimestamp, "Synchronize keys.");
} else {
log(envelopeTimestamp, "Primary device ignores synchronize keys.");
return;
}
SignalStore.storageService().setStorageKeyFromPrimary(keysMessage.getStorageService().get());
}
private void handleSynchronizeContacts(@NonNull ContactsMessage contactsMessage, long envelopeTimestamp) throws IOException {
if (SignalStore.account().isLinkedDevice()) {
log(envelopeTimestamp, "Synchronize contacts.");
} else {
log(envelopeTimestamp, "Primary device ignores synchronize contacts.");
return;
}
if (!(contactsMessage.getContactsStream() instanceof SignalServiceAttachmentPointer)) {
warn(envelopeTimestamp, "No contact stream available.");
return;
}
SignalServiceAttachmentPointer contactsAttachment = (SignalServiceAttachmentPointer) contactsMessage.getContactsStream();
ApplicationDependencies.getJobManager().add(new MultiDeviceContactSyncJob(contactsAttachment));
}
private void handleSynchronizeSentMessage(@NonNull SignalServiceContent content,
@NonNull SentTranscriptMessage message,
@NonNull Recipient senderRecipient)
@@ -1171,7 +1206,12 @@ public final class MessageContentProcessor {
private void handleSynchronizeRequestMessage(@NonNull RequestMessage message, long envelopeTimestamp)
{
log(envelopeTimestamp, "Synchronize request message.");
if (SignalStore.account().isPrimaryDevice()) {
log(envelopeTimestamp, "Synchronize request message.");
} else {
log(envelopeTimestamp, "Linked device ignoring synchronize request message.");
return;
}
if (message.isContactsRequest()) {
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob(true));

View File

@@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.ByteUnit;
@@ -129,7 +130,24 @@ public class AvatarHelper {
OutputStream outputStream = null;
try {
outputStream = getOutputStream(context, recipientId);
outputStream = getOutputStream(context, recipientId, false);
StreamUtil.copy(inputStream, outputStream);
} finally {
StreamUtil.close(outputStream);
}
}
public static void setSyncAvatar(@NonNull Context context, @NonNull RecipientId recipientId, @Nullable InputStream inputStream)
throws IOException
{
if (inputStream == null) {
delete(context, recipientId);
return;
}
OutputStream outputStream = null;
try {
outputStream = getOutputStream(context, recipientId, true);
StreamUtil.copy(inputStream, outputStream);
} finally {
StreamUtil.close(outputStream);
@@ -140,9 +158,9 @@ public class AvatarHelper {
* Retrieves an output stream you can write to that will be saved as the avatar for the specified
* recipient. Only intended to be used for backup. Otherwise, use {@link #setAvatar(Context, RecipientId, InputStream)}.
*/
public static @NonNull OutputStream getOutputStream(@NonNull Context context, @NonNull RecipientId recipientId) throws IOException {
public static @NonNull OutputStream getOutputStream(@NonNull Context context, @NonNull RecipientId recipientId, boolean isSyncAvatar) throws IOException {
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
File targetFile = getAvatarFile(context, recipientId);
File targetFile = getAvatarFile(context, recipientId, isSyncAvatar);
return ModernEncryptingPartOutputStream.createFor(attachmentSecret, targetFile, true).second;
}
@@ -179,8 +197,25 @@ public class AvatarHelper {
}
private static @NonNull File getAvatarFile(@NonNull Context context, @NonNull RecipientId recipientId) {
File profileAvatar = getAvatarFile(context, recipientId, false);
boolean profileAvatarExists = profileAvatar.exists() && profileAvatar.length() > 0;
File syncAvatar = getAvatarFile(context, recipientId, true);
boolean syncAvatarExists = syncAvatar.exists() && syncAvatar.length() > 0;
if (SignalStore.settings().isPreferSystemContactPhotos() && syncAvatarExists) {
return syncAvatar;
} else if (profileAvatarExists) {
return profileAvatar;
} else if (syncAvatarExists) {
return syncAvatar;
}
return profileAvatar;
}
private static @NonNull File getAvatarFile(@NonNull Context context, @NonNull RecipientId recipientId, boolean isSyncAvatar) {
File directory = context.getDir(AVATAR_DIRECTORY, Context.MODE_PRIVATE);
return new File(directory, recipientId.serialize());
return new File(directory, recipientId.serialize() + (isSyncAvatar ? "-sync" : ""));
}
public static class Avatar {

View File

@@ -413,6 +413,19 @@ public class BlobProvider {
return context.getDir(directory, Context.MODE_PRIVATE);
}
/**
* Returns a {@link File} within the appropriate directory to be cleaned up as part of
* normal operations. Unlike other blobs, this is just a file reference and no
* automatic encryption occurs when reading or writing and must be done by the caller.
*
* @return file located in the appropriate directory to be delete on app session restarts
*/
public File forNonAutoEncryptingSingleSessionOnDisk(@NonNull Context context) {
String directory = getDirectory(StorageType.SINGLE_SESSION_DISK);
String id = UUID.randomUUID().toString();
return new File(getOrCreateDirectory(context, directory), buildFileName(id));
}
public class BlobBuilder {
private InputStream data;

View File

@@ -80,10 +80,6 @@ public final class ConversationShortcutPhoto implements Key {
}
private long getFileLastModified() {
if (!recipient.isSelf()) {
return 0;
}
return AvatarHelper.getLastModified(ApplicationDependencies.getApplication(), recipient.getId());
}