mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 21:15:48 +00:00
Add device linking infrastructure.
This commit is contained in:
@@ -4204,7 +4204,10 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
Log.w(TAG, "Contact is marked as registered, but has no serviceId! Can't locally mark registered. (Phone: ${contact.proto.e164.nullIfBlank()}, Username: ${username?.isNotEmpty()})")
|
||||
}
|
||||
|
||||
if (isInsert) {
|
||||
if (SignalStore.account.isLinkedDevice) {
|
||||
val avatarColor = StorageSyncModels.remoteToLocalAvatarColor(contact.proto.avatarColor) ?: AvatarColorHash.forAddress(contact.proto.signalAci ?: contact.proto.signalPni, contact.proto.e164)
|
||||
put(AVATAR_COLOR, avatarColor.serialize())
|
||||
} else if (isInsert) {
|
||||
put(AVATAR_COLOR, AvatarColorHash.forAddress(contact.proto.signalAci ?: contact.proto.signalPni, contact.proto.e164).serialize())
|
||||
}
|
||||
}
|
||||
@@ -4251,7 +4254,10 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
putNull(STORAGE_SERVICE_PROTO)
|
||||
}
|
||||
|
||||
if (isInsert) {
|
||||
if (SignalStore.account.isLinkedDevice) {
|
||||
val avatarColor = StorageSyncModels.remoteToLocalAvatarColor(groupV2.proto.avatarColor) ?: AvatarColorHash.forGroupId(groupId)
|
||||
put(AVATAR_COLOR, avatarColor.serialize())
|
||||
} else if (isInsert) {
|
||||
put(AVATAR_COLOR, AvatarColorHash.forGroupId(groupId).serialize())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ class StorageSyncJob private constructor(parameters: Parameters, private var loc
|
||||
|
||||
@Throws(IOException::class, RetryLaterException::class, UntrustedIdentityException::class)
|
||||
override fun onRun() {
|
||||
if (!(SignalStore.svr.hasPin() || SignalStore.account.restoredAccountEntropyPool) && !SignalStore.svr.hasOptedOut()) {
|
||||
if (!(SignalStore.svr.hasPin() || SignalStore.account.restoredAccountEntropyPool || SignalStore.account.restoredAccountEntropyPoolFromPrimary) && !SignalStore.svr.hasOptedOut()) {
|
||||
Log.i(TAG, "Doesn't have access to storage service. Skipping.")
|
||||
return
|
||||
}
|
||||
@@ -197,6 +197,11 @@ class StorageSyncJob private constructor(parameters: Parameters, private var loc
|
||||
return
|
||||
}
|
||||
|
||||
if (SignalStore.account.isLinkedDevice && !SignalStore.account.restoredAccountEntropyPoolFromPrimary) {
|
||||
Log.w(TAG, "Have not restored AEP from primary, skipping.")
|
||||
return
|
||||
}
|
||||
|
||||
val (storageServiceKey, usingTempKey) = SignalStore.storageService.storageKeyForInitialDataRestore?.let {
|
||||
Log.i(TAG, "Using temporary storage key.")
|
||||
it to true
|
||||
@@ -228,7 +233,6 @@ class StorageSyncJob private constructor(parameters: Parameters, private var loc
|
||||
.enqueue()
|
||||
} else {
|
||||
Log.w(TAG, "Failed to decrypt remote storage! Requesting new keys from primary.", e)
|
||||
SignalStore.storageService.clearStorageKeyFromPrimary()
|
||||
AppDependencies.signalServiceMessageSender.sendSyncMessage(SignalServiceSyncMessage.forRequest(RequestMessage.forType(SyncMessage.Request.Type.KEYS)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ class AccountValues internal constructor(store: KeyValueStore, context: Context)
|
||||
|
||||
private const val KEY_ACCOUNT_ENTROPY_POOL = "account.account_entropy_pool"
|
||||
private const val KEY_RESTORED_ACCOUNT_ENTROPY_KEY = "account.restored_account_entropy_pool"
|
||||
private const val KEY_RESTORED_ACCOUNT_ENTROPY_KEY_FROM_PRIMARY = "account.restore_account_entropy_pool_primary"
|
||||
|
||||
private val AEP_LOCK = ReentrantLock()
|
||||
}
|
||||
@@ -151,6 +152,17 @@ class AccountValues internal constructor(store: KeyValueStore, context: Context)
|
||||
}
|
||||
}
|
||||
|
||||
fun setAccountEntropyPoolFromPrimaryDevice(aep: AccountEntropyPool) {
|
||||
AEP_LOCK.withLock {
|
||||
Log.i(TAG, "Setting new AEP from primary device")
|
||||
store
|
||||
.beginWrite()
|
||||
.putString(KEY_ACCOUNT_ENTROPY_POOL, aep.value)
|
||||
.putBoolean(KEY_RESTORED_ACCOUNT_ENTROPY_KEY_FROM_PRIMARY, true)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
fun restoreAccountEntropyPool(aep: AccountEntropyPool) {
|
||||
AEP_LOCK.withLock {
|
||||
store
|
||||
@@ -173,9 +185,10 @@ class AccountValues internal constructor(store: KeyValueStore, context: Context)
|
||||
}
|
||||
|
||||
@get:JvmName("restoredAccountEntropyPool")
|
||||
@get:Synchronized
|
||||
val restoredAccountEntropyPool by booleanValue(KEY_RESTORED_ACCOUNT_ENTROPY_KEY, false)
|
||||
|
||||
val restoredAccountEntropyPoolFromPrimary by booleanValue(KEY_RESTORED_ACCOUNT_ENTROPY_KEY_FROM_PRIMARY, false)
|
||||
|
||||
/** The local user's [ACI]. */
|
||||
val aci: ACI?
|
||||
get() = ACI.parseOrNull(getString(KEY_ACI, null))
|
||||
@@ -300,18 +313,6 @@ class AccountValues internal constructor(store: KeyValueStore, context: Context)
|
||||
}
|
||||
}
|
||||
|
||||
/** When acting as a linked device, this method lets you store the identity keys sent from the primary device */
|
||||
fun setAciIdentityKeysFromPrimaryDevice(aciKeys: IdentityKeyPair) {
|
||||
synchronized(this) {
|
||||
require(isLinkedDevice) { "Must be a linked device!" }
|
||||
store
|
||||
.beginWrite()
|
||||
.putBlob(KEY_ACI_IDENTITY_PUBLIC_KEY, aciKeys.publicKey.serialize())
|
||||
.putBlob(KEY_ACI_IDENTITY_PRIVATE_KEY, aciKeys.privateKey.serialize())
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
/** Set an identity key pair for the PNI identity via change number. */
|
||||
fun setPniIdentityKeyAfterChangeNumber(key: IdentityKeyPair) {
|
||||
synchronized(this) {
|
||||
@@ -475,12 +476,7 @@ class AccountValues internal constructor(store: KeyValueStore, context: Context)
|
||||
Recipient.self().live().refresh()
|
||||
}
|
||||
|
||||
val deviceName: String?
|
||||
get() = getString(KEY_DEVICE_NAME, null)
|
||||
|
||||
fun setDeviceName(deviceName: String) {
|
||||
putString(KEY_DEVICE_NAME, deviceName)
|
||||
}
|
||||
var deviceName: String? by stringValue(KEY_DEVICE_NAME, null)
|
||||
|
||||
var deviceId: Int by integerValue(KEY_DEVICE_ID, SignalServiceAddress.DEFAULT_DEVICE_ID)
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.keyvalue
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest
|
||||
import org.whispersystems.signalservice.api.storage.StorageKey
|
||||
import org.whispersystems.signalservice.api.util.Preconditions
|
||||
|
||||
class StorageServiceValues internal constructor(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
companion object {
|
||||
@@ -12,9 +11,6 @@ class StorageServiceValues internal constructor(store: KeyValueStore) : SignalSt
|
||||
private const val LAST_SYNC_TIME = "storage.last_sync_time"
|
||||
private const val NEEDS_ACCOUNT_RESTORE = "storage.needs_account_restore"
|
||||
private const val MANIFEST = "storage.manifest"
|
||||
|
||||
// TODO [linked-device] No need to track this separately -- we'd get the AEP from the primary
|
||||
private const val SYNC_STORAGE_KEY = "storage.syncStorageKey"
|
||||
}
|
||||
|
||||
public override fun onFirstEverAppLaunch() = Unit
|
||||
@@ -23,24 +19,9 @@ class StorageServiceValues internal constructor(store: KeyValueStore) : SignalSt
|
||||
|
||||
val storageKey: StorageKey
|
||||
get() {
|
||||
if (store.containsKey(SYNC_STORAGE_KEY)) {
|
||||
return StorageKey(getBlob(SYNC_STORAGE_KEY, null))
|
||||
}
|
||||
return SignalStore.svr.masterKey.deriveStorageServiceKey()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun setStorageKeyFromPrimary(storageKey: StorageKey) {
|
||||
Preconditions.checkState(SignalStore.account.isLinkedDevice, "Can only set storage key directly on linked devices")
|
||||
putBlob(SYNC_STORAGE_KEY, storageKey.serialize())
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun clearStorageKeyFromPrimary() {
|
||||
Preconditions.checkState(SignalStore.account.isLinkedDevice, "Can only clear storage key directly on linked devices")
|
||||
remove(SYNC_STORAGE_KEY)
|
||||
}
|
||||
|
||||
var lastSyncTime: Long by longValue(LAST_SYNC_TIME, 0)
|
||||
|
||||
var needsAccountRestore: Boolean by booleanValue(NEEDS_ACCOUNT_RESTORE, false)
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.messages
|
||||
|
||||
import android.content.Context
|
||||
import com.mobilecoin.lib.exceptions.SerializationException
|
||||
import okio.ByteString
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.core.util.isNotEmpty
|
||||
@@ -102,6 +101,8 @@ import org.thoughtcrime.securesms.util.MessageConstraintsUtil
|
||||
import org.thoughtcrime.securesms.util.SignalE164Util
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.AccountEntropyPool
|
||||
import org.whispersystems.signalservice.api.backup.MediaRootBackupKey
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
|
||||
import org.whispersystems.signalservice.api.push.DistributionId
|
||||
@@ -109,7 +110,6 @@ import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.api.storage.StorageKey
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import org.whispersystems.signalservice.internal.push.AddressableMessage
|
||||
import org.whispersystems.signalservice.internal.push.Content
|
||||
@@ -166,6 +166,7 @@ object SyncMessageProcessor {
|
||||
syncMessage.messageRequestResponse != null -> handleSynchronizeMessageRequestResponse(syncMessage.messageRequestResponse!!, envelope.timestamp!!)
|
||||
syncMessage.outgoingPayment != null -> handleSynchronizeOutgoingPayment(syncMessage.outgoingPayment!!, envelope.timestamp!!)
|
||||
syncMessage.contacts != null -> handleSynchronizeContacts(syncMessage.contacts!!, envelope.timestamp!!)
|
||||
syncMessage.keys != null -> handleSynchronizeKeys(syncMessage.keys!!, envelope.timestamp!!)
|
||||
syncMessage.callEvent != null -> handleSynchronizeCallEvent(syncMessage.callEvent!!, envelope.timestamp!!)
|
||||
syncMessage.callLinkUpdate != null -> handleSynchronizeCallLink(syncMessage.callLinkUpdate!!, envelope.timestamp!!)
|
||||
syncMessage.callLogEvent != null -> handleSynchronizeCallLogEvent(syncMessage.callLogEvent!!, envelope.timestamp!!)
|
||||
@@ -1255,7 +1256,7 @@ object SyncMessageProcessor {
|
||||
log("Inserted synchronized payment $uuid")
|
||||
}
|
||||
|
||||
private fun handleSynchronizeKeys(storageKey: ByteString, envelopeTimestamp: Long) {
|
||||
private fun handleSynchronizeKeys(keys: SyncMessage.Keys, envelopeTimestamp: Long) {
|
||||
if (SignalStore.account.isLinkedDevice) {
|
||||
log(envelopeTimestamp, "Synchronize keys.")
|
||||
} else {
|
||||
@@ -1263,7 +1264,13 @@ object SyncMessageProcessor {
|
||||
return
|
||||
}
|
||||
|
||||
SignalStore.storageService.setStorageKeyFromPrimary(StorageKey(storageKey.toByteArray()))
|
||||
if (keys.accountEntropyPool != null) {
|
||||
SignalStore.account.setAccountEntropyPoolFromPrimaryDevice(AccountEntropyPool(keys.accountEntropyPool!!))
|
||||
}
|
||||
|
||||
if (keys.mediaRootBackupKey != null) {
|
||||
SignalStore.backup.mediaRootBackupKey = MediaRootBackupKey(keys.mediaRootBackupKey!!.toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
|
||||
@@ -57,6 +57,11 @@ public class PniAccountInitializationMigrationJob extends MigrationJob {
|
||||
|
||||
@Override
|
||||
public void performMigration() throws IOException {
|
||||
if (SignalStore.account().isLinkedDevice()) {
|
||||
Log.i(TAG, "Linked device, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
PNI pni = SignalStore.account().getPni();
|
||||
|
||||
if (pni == null || SignalStore.account().getAci() == null || !Recipient.self().isRegistered()) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import okio.ByteString.Companion.toByteString
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
import org.signal.libsignal.protocol.state.KyberPreKeyRecord
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.LinkedDeviceInfo
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.LocalRegistrationMetadata
|
||||
import org.whispersystems.signalservice.api.account.PreKeyCollection
|
||||
|
||||
@@ -17,7 +18,14 @@ import org.whispersystems.signalservice.api.account.PreKeyCollection
|
||||
* and combines them into a proto-backed class [LocalRegistrationMetadata] so they can be serialized & stored.
|
||||
*/
|
||||
object LocalRegistrationMetadataUtil {
|
||||
fun createLocalRegistrationMetadata(localAciIdentityKeyPair: IdentityKeyPair, localPniIdentityKeyPair: IdentityKeyPair, registrationData: RegistrationData, remoteResult: AccountRegistrationResult, reglockEnabled: Boolean): LocalRegistrationMetadata {
|
||||
fun createLocalRegistrationMetadata(
|
||||
localAciIdentityKeyPair: IdentityKeyPair,
|
||||
localPniIdentityKeyPair: IdentityKeyPair,
|
||||
registrationData: RegistrationData,
|
||||
remoteResult: AccountRegistrationResult,
|
||||
reglockEnabled: Boolean,
|
||||
linkedDeviceInfo: LinkedDeviceInfo? = null
|
||||
): LocalRegistrationMetadata {
|
||||
return LocalRegistrationMetadata.Builder().apply {
|
||||
aciIdentityKeyPair = localAciIdentityKeyPair.serialize().toByteString()
|
||||
aciSignedPreKey = remoteResult.aciPreKeyCollection.signedPreKey.serialize().toByteString()
|
||||
@@ -39,6 +47,7 @@ object LocalRegistrationMetadataUtil {
|
||||
profileKey = registrationData.profileKey.serialize().toByteString()
|
||||
servicePassword = registrationData.password
|
||||
this.reglockEnabled = reglockEnabled
|
||||
this.linkedDeviceInfo = linkedDeviceInfo
|
||||
}.build()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.data
|
||||
|
||||
data class RegisterAsLinkedDeviceResponse(
|
||||
val deviceId: Int,
|
||||
val accountRegistrationResult: AccountRegistrationResult
|
||||
)
|
||||
@@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.registrationv3.data
|
||||
import android.app.backup.BackupManager
|
||||
import android.content.Context
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.google.android.gms.auth.api.phone.SmsRetriever
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -32,8 +33,10 @@ import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.LocalRegistrationMetadata
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.gcm.FcmUtil
|
||||
import org.thoughtcrime.securesms.jobmanager.runJobBlocking
|
||||
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob
|
||||
import org.thoughtcrime.securesms.jobs.PreKeysSyncJob
|
||||
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob
|
||||
import org.thoughtcrime.securesms.jobs.RotateCertificateJob
|
||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
@@ -50,6 +53,7 @@ import org.thoughtcrime.securesms.registration.data.LocalRegistrationMetadataUti
|
||||
import org.thoughtcrime.securesms.registration.data.LocalRegistrationMetadataUtil.getAciPreKeyCollection
|
||||
import org.thoughtcrime.securesms.registration.data.LocalRegistrationMetadataUtil.getPniIdentityKeyPair
|
||||
import org.thoughtcrime.securesms.registration.data.LocalRegistrationMetadataUtil.getPniPreKeyCollection
|
||||
import org.thoughtcrime.securesms.registration.data.RegisterAsLinkedDeviceResponse
|
||||
import org.thoughtcrime.securesms.registration.data.RegistrationData
|
||||
import org.thoughtcrime.securesms.registration.data.network.BackupAuthCheckResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
|
||||
@@ -58,14 +62,17 @@ import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionC
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
|
||||
import org.thoughtcrime.securesms.registration.fcm.PushChallengeRequest
|
||||
import org.thoughtcrime.securesms.registration.secondary.DeviceNameCipher
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
|
||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener
|
||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.api.AccountEntropyPool
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.SvrNoDataException
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes
|
||||
import org.whispersystems.signalservice.api.account.PreKeyCollection
|
||||
import org.whispersystems.signalservice.api.backup.MediaRootBackupKey
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey
|
||||
import org.whispersystems.signalservice.api.kbs.PinHashUtil
|
||||
@@ -76,6 +83,7 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.api.registration.RegistrationApi
|
||||
import org.whispersystems.signalservice.api.svr.Svr3Credentials
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
import org.whispersystems.signalservice.internal.push.ProvisionMessage
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket
|
||||
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
@@ -207,6 +215,19 @@ object RegistrationRepository {
|
||||
saveOwnIdentityKey(selfId, aci, aciProtocolStore, now)
|
||||
saveOwnIdentityKey(selfId, pni, pniProtocolStore, now)
|
||||
|
||||
if (data.linkedDeviceInfo != null) {
|
||||
SignalStore.account.deviceId = data.linkedDeviceInfo.deviceId
|
||||
SignalStore.account.deviceName = data.linkedDeviceInfo.deviceName
|
||||
|
||||
if (data.linkedDeviceInfo.accountEntropyPool != null) {
|
||||
SignalStore.account.setAccountEntropyPoolFromPrimaryDevice(AccountEntropyPool(data.linkedDeviceInfo.accountEntropyPool))
|
||||
}
|
||||
|
||||
if (data.linkedDeviceInfo.mediaRootBackupKey != null) {
|
||||
SignalStore.backup.mediaRootBackupKey = MediaRootBackupKey(data.linkedDeviceInfo.mediaRootBackupKey.toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
SignalStore.account.setServicePassword(data.servicePassword)
|
||||
SignalStore.account.setRegistered(true)
|
||||
TextSecurePreferences.setPromptedPushRegistration(context, true)
|
||||
@@ -221,11 +242,23 @@ object RegistrationRepository {
|
||||
PreKeysSyncJob.enqueue()
|
||||
|
||||
val jobManager = AppDependencies.jobManager
|
||||
jobManager.add(DirectoryRefreshJob(false))
|
||||
jobManager.add(RotateCertificateJob())
|
||||
|
||||
DirectoryRefreshListener.schedule(context)
|
||||
RotateSignedPreKeyListener.schedule(context)
|
||||
if (data.linkedDeviceInfo == null) {
|
||||
jobManager.add(DirectoryRefreshJob(false))
|
||||
jobManager.add(RotateCertificateJob())
|
||||
|
||||
DirectoryRefreshListener.schedule(context)
|
||||
RotateSignedPreKeyListener.schedule(context)
|
||||
} else {
|
||||
// TODO [linked-device] May want to have a different opt out mechanism for linked devices
|
||||
SvrRepository.optOutOfPin()
|
||||
|
||||
SignalStore.registration.hasUploadedProfile = true
|
||||
jobManager.runJobBlocking(RefreshOwnProfileJob(), 30.seconds)
|
||||
|
||||
jobManager.add(RotateCertificateJob())
|
||||
RotateSignedPreKeyListener.schedule(context)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@@ -446,6 +479,62 @@ object RegistrationRepository {
|
||||
return@withContext RegisterAccountResult.from(result)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun registerAsLinkedDevice(
|
||||
context: Context,
|
||||
deviceName: String,
|
||||
message: ProvisionMessage,
|
||||
registrationData: RegistrationData,
|
||||
aciIdentityKeyPair: IdentityKeyPair,
|
||||
pniIdentityKeyPair: IdentityKeyPair
|
||||
): NetworkResult<RegisterAsLinkedDeviceResponse> {
|
||||
val aci = message.aciBinary?.let { ACI.parseOrThrow(it) } ?: ACI.parseOrThrow(message.aci)
|
||||
val pni = message.pniBinary?.let { PNI.parseOrThrow(it) } ?: PNI.parseOrThrow(message.pni)
|
||||
|
||||
val universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context)
|
||||
val unidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(registrationData.profileKey)
|
||||
|
||||
val encryptedDeviceName = DeviceNameCipher.encryptDeviceName(deviceName.toByteArray(StandardCharsets.UTF_8), aciIdentityKeyPair)
|
||||
|
||||
val accountAttributes = AccountAttributes(
|
||||
signalingKey = null,
|
||||
registrationId = getRegistrationId(),
|
||||
fetchesMessages = registrationData.fcmToken == null,
|
||||
registrationLock = null,
|
||||
unidentifiedAccessKey = unidentifiedAccessKey,
|
||||
unrestrictedUnidentifiedAccess = universalUnidentifiedAccess,
|
||||
capabilities = AppCapabilities.getCapabilities(false),
|
||||
discoverableByPhoneNumber = false,
|
||||
name = Base64.encodeWithPadding(encryptedDeviceName),
|
||||
pniRegistrationId = getPniRegistrationId(),
|
||||
recoveryPassword = null
|
||||
)
|
||||
|
||||
val aciPreKeys = generateSignedAndLastResortPreKeys(aciIdentityKeyPair, SignalStore.account.aciPreKeys)
|
||||
val pniPreKeys = generateSignedAndLastResortPreKeys(pniIdentityKeyPair, SignalStore.account.pniPreKeys)
|
||||
|
||||
return AccountManagerFactory
|
||||
.getInstance()
|
||||
.createUnauthenticated(context, message.number!!, -1, registrationData.password)
|
||||
.registrationApi
|
||||
.registerAsSecondaryDevice(message.provisioningCode!!, accountAttributes, aciPreKeys, pniPreKeys, registrationData.fcmToken)
|
||||
.map { respone ->
|
||||
RegisterAsLinkedDeviceResponse(
|
||||
deviceId = respone.deviceId.toInt(),
|
||||
accountRegistrationResult = AccountRegistrationResult(
|
||||
uuid = aci.toString(),
|
||||
pni = pni.toString(),
|
||||
storageCapable = false,
|
||||
number = message.number!!,
|
||||
masterKey = MasterKey(message.masterKey!!.toByteArray()),
|
||||
pin = null,
|
||||
aciPreKeyCollection = aciPreKeys,
|
||||
pniPreKeyCollection = pniPreKeys
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createSessionAndBlockForPushChallenge(accountManager: RegistrationApi, fcmToken: String, mcc: String?, mnc: String?): NetworkResult<RegistrationSessionMetadataResponse> =
|
||||
withContext(Dispatchers.IO) {
|
||||
// TODO [regv2]: do not use event bus nor latch
|
||||
|
||||
@@ -25,9 +25,13 @@ import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.isNotNullOrBlank
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.protocol.IdentityKey
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
import org.signal.libsignal.protocol.ecc.ECPrivateKey
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.RestoreTimestampResult
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.LinkedDeviceInfo
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.RestoreDecisionState
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.ReclaimUsernameAndLinkJob
|
||||
@@ -69,18 +73,26 @@ import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequ
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.TokenNotAccepted
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.UnknownError
|
||||
import org.thoughtcrime.securesms.registration.ui.toE164
|
||||
import org.thoughtcrime.securesms.registration.util.RegistrationUtil
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
|
||||
import org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.link.RegisterLinkDeviceResult
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.restore.StorageServiceRestore
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.dualsim.MccMncProducer
|
||||
import org.whispersystems.signalservice.api.AccountEntropyPool
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.SvrNoDataException
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage
|
||||
import org.whispersystems.signalservice.api.svr.Svr3Credentials
|
||||
import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
import org.whispersystems.signalservice.internal.push.ProvisionMessage
|
||||
import org.whispersystems.signalservice.internal.push.SyncMessage
|
||||
import java.io.IOException
|
||||
import java.nio.charset.StandardCharsets
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
@@ -1051,6 +1063,104 @@ class RegistrationViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun registerAsLinkedDevice(context: Context, message: ProvisionMessage): RegisterLinkDeviceResult {
|
||||
val deviceName = "Android"
|
||||
|
||||
val aciIdentityKeyPair = IdentityKeyPair(IdentityKey(message.aciIdentityKeyPublic!!.toByteArray()), ECPrivateKey(message.aciIdentityKeyPrivate!!.toByteArray()))
|
||||
val pniIdentityKeyPair = IdentityKeyPair(IdentityKey(message.pniIdentityKeyPublic!!.toByteArray()), ECPrivateKey(message.pniIdentityKeyPrivate!!.toByteArray()))
|
||||
|
||||
val profileKey = ProfileKey(message.profileKey!!.toByteArray())
|
||||
val serverAuthToken = Util.getSecret(18)
|
||||
val fcmToken = RegistrationRepository.getFcmToken(context)
|
||||
|
||||
val registrationData = RegistrationData(
|
||||
code = "",
|
||||
e164 = message.number!!,
|
||||
password = serverAuthToken,
|
||||
registrationId = RegistrationRepository.getRegistrationId(),
|
||||
profileKey = profileKey,
|
||||
fcmToken = fcmToken,
|
||||
pniRegistrationId = RegistrationRepository.getPniRegistrationId(),
|
||||
recoveryPassword = null
|
||||
)
|
||||
|
||||
val result = RegistrationRepository.registerAsLinkedDevice(
|
||||
context = context,
|
||||
deviceName = deviceName,
|
||||
message = message,
|
||||
registrationData = registrationData,
|
||||
aciIdentityKeyPair = aciIdentityKeyPair,
|
||||
pniIdentityKeyPair = pniIdentityKeyPair
|
||||
)
|
||||
|
||||
when (result) {
|
||||
is NetworkResult.Success -> {
|
||||
val data = LocalRegistrationMetadataUtil.createLocalRegistrationMetadata(
|
||||
localAciIdentityKeyPair = aciIdentityKeyPair,
|
||||
localPniIdentityKeyPair = pniIdentityKeyPair,
|
||||
registrationData = registrationData,
|
||||
remoteResult = result.result.accountRegistrationResult,
|
||||
reglockEnabled = false,
|
||||
linkedDeviceInfo = LinkedDeviceInfo(
|
||||
deviceId = result.result.deviceId,
|
||||
deviceName = deviceName,
|
||||
ephemeralBackupKey = message.ephemeralBackupKey,
|
||||
accountEntropyPool = message.accountEntropyPool,
|
||||
mediaRootBackupKey = message.mediaRootBackupKey
|
||||
)
|
||||
)
|
||||
|
||||
if (message.readReceipts != null) {
|
||||
TextSecurePreferences.setReadReceiptsEnabled(context, message.readReceipts!!)
|
||||
}
|
||||
|
||||
RegistrationRepository.registerAccountLocally(context, data)
|
||||
}
|
||||
is NetworkResult.ApplicationError -> return RegisterLinkDeviceResult.UnexpectedException(result.throwable)
|
||||
is NetworkResult.NetworkError<*> -> return RegisterLinkDeviceResult.NetworkException(result.exception)
|
||||
is NetworkResult.StatusCodeError -> {
|
||||
return when (result.code) {
|
||||
403 -> RegisterLinkDeviceResult.IncorrectVerification
|
||||
409 -> RegisterLinkDeviceResult.MissingCapability
|
||||
411 -> RegisterLinkDeviceResult.MaxLinkedDevices
|
||||
422 -> RegisterLinkDeviceResult.InvalidRequest
|
||||
429 -> RegisterLinkDeviceResult.RateLimited(result.retryAfter())
|
||||
else -> RegisterLinkDeviceResult.UnexpectedException(result.exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||
|
||||
refreshRemoteConfig()
|
||||
|
||||
for (type in SyncMessage.Request.Type.entries) {
|
||||
if (type == SyncMessage.Request.Type.UNKNOWN) {
|
||||
continue
|
||||
}
|
||||
|
||||
Log.i(TAG, "Sending sync request for $type")
|
||||
AppDependencies.signalServiceMessageSender.sendSyncMessage(
|
||||
SignalServiceSyncMessage.forRequest(RequestMessage(SyncMessage.Request(type = type)))
|
||||
)
|
||||
}
|
||||
|
||||
SignalStore.registration.restoreDecisionState = RestoreDecisionState.NewAccount
|
||||
SignalStore.onboarding.clearAll()
|
||||
|
||||
if (SignalStore.account.restoredAccountEntropyPoolFromPrimary) {
|
||||
StorageServiceRestore.restore()
|
||||
}
|
||||
|
||||
store.update {
|
||||
it.copy(
|
||||
registrationCheckpoint = RegistrationCheckpoint.LOCAL_REGISTRATION_COMPLETE
|
||||
)
|
||||
}
|
||||
|
||||
return RegisterLinkDeviceResult.Success
|
||||
}
|
||||
|
||||
/** Converts the basic-auth creds we have locally into username:password pairs that are suitable for handing off to the service. */
|
||||
private fun List<String?>.toSvrCredentials(): AuthCredentials? {
|
||||
return this
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registrationv3.ui.link
|
||||
|
||||
import kotlin.time.Duration
|
||||
|
||||
sealed interface RegisterLinkDeviceResult {
|
||||
data object Success : RegisterLinkDeviceResult
|
||||
data object IncorrectVerification : RegisterLinkDeviceResult
|
||||
data object MissingCapability : RegisterLinkDeviceResult
|
||||
data object MaxLinkedDevices : RegisterLinkDeviceResult
|
||||
data object InvalidRequest : RegisterLinkDeviceResult
|
||||
data class RateLimited(val retryAfter: Duration?) : RegisterLinkDeviceResult
|
||||
data class NetworkException(val t: Throwable) : RegisterLinkDeviceResult
|
||||
data class UnexpectedException(val t: Throwable) : RegisterLinkDeviceResult
|
||||
}
|
||||
@@ -121,7 +121,8 @@ class RestoreViaQrViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
return ProvisioningSocket.start(
|
||||
return ProvisioningSocket.start<RegistrationProvisionMessage>(
|
||||
mode = ProvisioningSocket.Mode.REREG,
|
||||
identityKeyPair = IdentityKeyUtil.generateIdentityKeyPair(),
|
||||
configuration = AppDependencies.signalServiceNetworkAccess.getConfiguration(),
|
||||
handler = { id, t ->
|
||||
@@ -152,9 +153,9 @@ class RestoreViaQrViewModel : ViewModel() {
|
||||
)
|
||||
}
|
||||
|
||||
val result = socket.getRegistrationProvisioningMessage()
|
||||
val result = socket.getProvisioningMessageDecryptResult()
|
||||
|
||||
if (result is SecondaryProvisioningCipher.RegistrationProvisionResult.Success) {
|
||||
if (result is SecondaryProvisioningCipher.ProvisioningDecryptResult.Success) {
|
||||
Log.i(TAG, "Saving restore method token: ***${result.message.restoreMethodToken.takeLast(4)}")
|
||||
SignalStore.registration.restoreMethodToken = result.message.restoreMethodToken
|
||||
SignalStore.registration.restoreBackupMediaSize = result.message.backupSizeBytes ?: 0
|
||||
|
||||
@@ -231,7 +231,7 @@ class ContactRecordProcessor(
|
||||
nickname = remote.proto.nickname
|
||||
pniSignatureVerified = remote.proto.pniSignatureVerified || local.proto.pniSignatureVerified
|
||||
note = remote.proto.note.nullIfBlank() ?: ""
|
||||
avatarColor = local.proto.avatarColor
|
||||
avatarColor = if (SignalStore.account.isPrimaryDevice) local.proto.avatarColor else remote.proto.avatarColor
|
||||
}.build().toSignalContactRecord(StorageId.forContact(keyGenerator.generate()))
|
||||
|
||||
val matchesRemote = doParamsMatch(remote, merged)
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.thoughtcrime.securesms.database.RecipientTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.RecipientRecord
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
@@ -58,7 +59,7 @@ class GroupV2RecordProcessor(private val recipientTable: RecipientTable, private
|
||||
dontNotifyForMentionsIfMuted = remote.proto.dontNotifyForMentionsIfMuted
|
||||
hideStory = remote.proto.hideStory
|
||||
storySendMode = remote.proto.storySendMode
|
||||
avatarColor = local.proto.avatarColor
|
||||
avatarColor = if (SignalStore.account.isPrimaryDevice) local.proto.avatarColor else remote.proto.avatarColor
|
||||
}.build().toSignalGroupV2Record(StorageId.forGroupV2(keyGenerator.generate()))
|
||||
|
||||
val matchesRemote = doParamsMatch(remote, merged)
|
||||
|
||||
@@ -390,6 +390,24 @@ object StorageSyncModels {
|
||||
}
|
||||
}
|
||||
|
||||
fun remoteToLocalAvatarColor(avatarColor: RemoteAvatarColor?): AvatarColor? {
|
||||
return when (avatarColor) {
|
||||
RemoteAvatarColor.A100 -> AvatarColor.A100
|
||||
RemoteAvatarColor.A110 -> AvatarColor.A110
|
||||
RemoteAvatarColor.A120 -> AvatarColor.A120
|
||||
RemoteAvatarColor.A130 -> AvatarColor.A130
|
||||
RemoteAvatarColor.A140 -> AvatarColor.A140
|
||||
RemoteAvatarColor.A150 -> AvatarColor.A150
|
||||
RemoteAvatarColor.A160 -> AvatarColor.A160
|
||||
RemoteAvatarColor.A170 -> AvatarColor.A170
|
||||
RemoteAvatarColor.A180 -> AvatarColor.A180
|
||||
RemoteAvatarColor.A190 -> AvatarColor.A190
|
||||
RemoteAvatarColor.A200 -> AvatarColor.A200
|
||||
RemoteAvatarColor.A210 -> AvatarColor.A210
|
||||
null -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun localToRemoteChatFolder(folder: ChatFolderRecord, rawStorageId: ByteArray?): SignalChatFolderRecord {
|
||||
if (folder.chatFolderId == null) {
|
||||
throw AssertionError("Chat folder must have a chat folder id.")
|
||||
|
||||
@@ -564,6 +564,15 @@ message LocalRegistrationMetadata {
|
||||
bytes profileKey = 15;
|
||||
string servicePassword = 16;
|
||||
bool reglockEnabled = 17;
|
||||
LinkedDeviceInfo linkedDeviceInfo = 18;
|
||||
}
|
||||
|
||||
message LinkedDeviceInfo {
|
||||
uint32 deviceId = 1;
|
||||
string deviceName = 2;
|
||||
optional bytes ephemeralBackupKey = 3;
|
||||
optional string accountEntropyPool = 4;
|
||||
optional bytes mediaRootBackupKey = 5;
|
||||
}
|
||||
|
||||
message RestoreDecisionState {
|
||||
|
||||
Reference in New Issue
Block a user