mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-08-07 13:55:56 +01:00
Add device linking support to RegV5.
This commit is contained in:
committed by
Michelle Tang
parent
55b5997a4e
commit
91965d205a
@@ -2322,7 +2322,7 @@ object BackupRepository {
|
||||
return RemoteRestoreResult.Success
|
||||
}
|
||||
|
||||
suspend fun restoreLinkAndSyncBackup(response: TransferArchiveResponse, ephemeralBackupKey: MessageBackupKey) {
|
||||
suspend fun restoreLinkAndSyncBackup(response: TransferArchiveResponse, ephemeralBackupKey: MessageBackupKey): RemoteRestoreResult {
|
||||
val context = AppDependencies.application
|
||||
ArchiveRestoreProgress.onRestorePending()
|
||||
|
||||
@@ -2354,9 +2354,16 @@ object BackupRepository {
|
||||
override fun shouldCancel() = cancellationSignal()
|
||||
}
|
||||
|
||||
val cdn = response.cdn
|
||||
val key = response.key
|
||||
if (cdn == null || key == null) {
|
||||
Log.w(TAG, "[restoreLinkAndSyncBackup] Response has no archive location (error=${response.error}); nothing to download.")
|
||||
return RemoteRestoreResult.Failure
|
||||
}
|
||||
|
||||
Log.i(TAG, "[restoreLinkAndSyncBackup] Downloading backup")
|
||||
val tempBackupFile = AppDependencies.blobs.forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
|
||||
when (val result = AppDependencies.signalServiceMessageReceiver.retrieveLinkAndSyncBackup(response.cdn, response.key, tempBackupFile, progressListener)) {
|
||||
when (val result = AppDependencies.signalServiceMessageReceiver.retrieveLinkAndSyncBackup(cdn, key, tempBackupFile, progressListener)) {
|
||||
is NetworkResult.Success -> Log.i(TAG, "[restoreLinkAndSyncBackup] Download successful")
|
||||
else -> {
|
||||
Log.w(TAG, "[restoreLinkAndSyncBackup] Failed to download backup file", result.getCause())
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.crypto.DeviceName
|
||||
import org.signal.core.util.crypto.DeviceNameCipher
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.roundedString
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.devicelist.protos.DeviceName
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.keyvalue.protos.LeastActiveLinkedDevice
|
||||
import org.thoughtcrime.securesms.registration.secondary.DeviceNameCipher
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import java.io.IOException
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
@@ -6,7 +6,9 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.Base64;
|
||||
import org.signal.core.util.crypto.DeviceNameCipher;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.network.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.thoughtcrime.securesms.AppCapabilities;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
@@ -16,13 +18,11 @@ import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues.PhoneNumberD
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.keyvalue.SvrValues;
|
||||
import org.thoughtcrime.securesms.net.SignalNetwork;
|
||||
import org.thoughtcrime.securesms.registration.secondary.DeviceNameCipher;
|
||||
import org.thoughtcrime.securesms.registration.data.RegistrationRepository;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.signalservice.api.NetworkResultUtil;
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
import org.signal.network.exceptions.NonSuccessfulResponseCodeException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@@ -86,7 +86,7 @@ class RefreshOwnProfileJob private constructor(parameters: Parameters) : BaseJob
|
||||
return
|
||||
}
|
||||
|
||||
if ((SignalStore.svr.hasPin() || SignalStore.account.restoredAccountEntropyPool) && !SignalStore.svr.hasOptedOut() && SignalStore.storageService.lastSyncTime == 0L) {
|
||||
if ((SignalStore.svr.hasPin() || SignalStore.account.restoredAccountEntropyPool || SignalStore.account.restoredAccountEntropyPoolFromPrimary) && !SignalStore.svr.hasOptedOut() && SignalStore.storageService.lastSyncTime == 0L) {
|
||||
Log.i(TAG, "Registered with PIN or AEP but haven't completed storage sync yet.")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.net.Uri
|
||||
import org.signal.core.models.backup.MessageBackupKey
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.Stopwatch
|
||||
import org.signal.core.util.crypto.DeviceName
|
||||
import org.signal.core.util.crypto.DeviceNameCipher
|
||||
import org.signal.core.util.isNotNullOrBlank
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.logging.logD
|
||||
@@ -20,14 +22,12 @@ import org.thoughtcrime.securesms.backup.v2.ArchiveValidator
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.devicelist.protos.DeviceName
|
||||
import org.thoughtcrime.securesms.jobs.DeviceNameChangeJob
|
||||
import org.thoughtcrime.securesms.jobs.E164FormattingJob
|
||||
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.linkdevice.LinkDeviceRepository.createAndUploadArchive
|
||||
import org.thoughtcrime.securesms.net.SignalNetwork
|
||||
import org.thoughtcrime.securesms.registration.secondary.DeviceNameCipher
|
||||
import org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse
|
||||
import org.whispersystems.signalservice.api.link.TransferArchiveError
|
||||
import org.whispersystems.signalservice.api.link.WaitForLinkedDeviceResponse
|
||||
|
||||
+47
-23
@@ -26,6 +26,7 @@ import org.signal.core.models.ServiceId.ACI
|
||||
import org.signal.core.models.ServiceId.PNI
|
||||
import org.signal.core.models.backup.MediaRootBackupKey
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.crypto.DeviceNameCipher
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
import org.signal.libsignal.protocol.util.KeyHelper
|
||||
@@ -70,13 +71,13 @@ 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.SvrNoDataException
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes
|
||||
import org.whispersystems.signalservice.api.account.DeviceAttributes
|
||||
import org.whispersystems.signalservice.api.account.PreKeyCollection
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
|
||||
import org.whispersystems.signalservice.api.kbs.PinHashUtil
|
||||
@@ -97,6 +98,7 @@ import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.coroutines.coroutineContext
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
@@ -488,23 +490,46 @@ object RegistrationRepository {
|
||||
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)
|
||||
return registerAsLinkedDevice(
|
||||
context = context,
|
||||
deviceName = deviceName,
|
||||
number = message.number!!,
|
||||
provisioningCode = message.provisioningCode!!,
|
||||
aci = aci,
|
||||
pni = pni,
|
||||
accountEntropyPool = AccountEntropyPool(message.accountEntropyPool!!),
|
||||
registrationData = registrationData,
|
||||
aciIdentityKeyPair = aciIdentityKeyPair,
|
||||
pniIdentityKeyPair = pniIdentityKeyPair
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers this device as a linked (secondary) device using discrete fields rather than a raw
|
||||
* [ProvisionMessage]. This is the form used by the regv5 registration module, which works with a
|
||||
* decoupled provisioning message.
|
||||
*/
|
||||
@WorkerThread
|
||||
fun registerAsLinkedDevice(
|
||||
context: Context,
|
||||
deviceName: String,
|
||||
number: String,
|
||||
provisioningCode: String,
|
||||
aci: ACI,
|
||||
pni: PNI,
|
||||
accountEntropyPool: AccountEntropyPool,
|
||||
registrationData: RegistrationData,
|
||||
aciIdentityKeyPair: IdentityKeyPair,
|
||||
pniIdentityKeyPair: IdentityKeyPair
|
||||
): NetworkResult<RegisterAsLinkedDeviceResponse> {
|
||||
val encryptedDeviceName = DeviceNameCipher.encryptDeviceName(deviceName.toByteArray(StandardCharsets.UTF_8), aciIdentityKeyPair)
|
||||
|
||||
val accountAttributes = AccountAttributes(
|
||||
signalingKey = null,
|
||||
registrationId = getRegistrationId(),
|
||||
val deviceAttributes = DeviceAttributes(
|
||||
fetchesMessages = registrationData.fcmToken == null,
|
||||
registrationLock = null,
|
||||
unidentifiedAccessKey = unidentifiedAccessKey,
|
||||
unrestrictedUnidentifiedAccess = universalUnidentifiedAccess,
|
||||
capabilities = AppCapabilities.getCapabilities(false),
|
||||
discoverableByPhoneNumber = false,
|
||||
name = Base64.encodeWithPadding(encryptedDeviceName),
|
||||
registrationId = getRegistrationId(),
|
||||
pniRegistrationId = getPniRegistrationId(),
|
||||
recoveryPassword = null
|
||||
name = Base64.encodeWithPadding(encryptedDeviceName),
|
||||
capabilities = AppCapabilities.getCapabilities(false)
|
||||
)
|
||||
|
||||
val aciPreKeys = generateSignedAndLastResortPreKeys(aciIdentityKeyPair, SignalStore.account.aciPreKeys)
|
||||
@@ -512,20 +537,18 @@ object RegistrationRepository {
|
||||
|
||||
return AccountManagerFactory
|
||||
.getInstance()
|
||||
.createUnauthenticated(context, message.number!!, -1, registrationData.password)
|
||||
.createUnauthenticated(context, number, -1, registrationData.password)
|
||||
.registrationApi
|
||||
.registerAsSecondaryDevice(message.provisioningCode!!, accountAttributes, aciPreKeys, pniPreKeys, registrationData.fcmToken)
|
||||
.map { respone ->
|
||||
val aep = AccountEntropyPool(message.accountEntropyPool!!)
|
||||
|
||||
.registerAsSecondaryDevice(provisioningCode, deviceAttributes, aciPreKeys, pniPreKeys, registrationData.fcmToken)
|
||||
.map { response ->
|
||||
RegisterAsLinkedDeviceResponse(
|
||||
deviceId = respone.deviceId.toInt(),
|
||||
deviceId = response.deviceId.toInt(),
|
||||
accountRegistrationResult = AccountRegistrationResult(
|
||||
uuid = aci.toString(),
|
||||
pni = pni.toString(),
|
||||
storageCapable = false,
|
||||
number = message.number!!,
|
||||
masterKey = aep.deriveMasterKey(),
|
||||
number = number,
|
||||
masterKey = accountEntropyPool.deriveMasterKey(),
|
||||
pin = null,
|
||||
aciPreKeyCollection = aciPreKeys,
|
||||
pniPreKeyCollection = pniPreKeys,
|
||||
@@ -674,7 +697,7 @@ object RegistrationRepository {
|
||||
return Recipient.self().profileName.isEmpty || !AvatarHelper.hasAvatar(AppDependencies.application, Recipient.self().id)
|
||||
}
|
||||
|
||||
suspend fun waitForLinkAndSyncBackupDetails(maxWaitTime: Duration = 60.seconds): TransferArchiveResponse? {
|
||||
suspend fun waitForLinkAndSyncBackupDetails(maxWaitTime: Duration = 1.hours): TransferArchiveResponse? {
|
||||
val startTime = System.currentTimeMillis()
|
||||
var timeRemaining = maxWaitTime.inWholeMilliseconds
|
||||
|
||||
@@ -683,7 +706,8 @@ object RegistrationRepository {
|
||||
|
||||
when (val result = SignalNetwork.linkDevice.waitForPrimaryDevice(timeout = 60.seconds)) {
|
||||
is NetworkResult.Success -> {
|
||||
Log.i(TAG, "[waitForLinkAndSyncBackupDetails] Transfer archive data provided by primary")
|
||||
// The primary has responded: either with an archive location, or an error telling us not to expect one.
|
||||
Log.i(TAG, "[waitForLinkAndSyncBackupDetails] Primary responded (hasArchive=${result.result.hasArchive}, error=${result.result.error})")
|
||||
return result.result
|
||||
}
|
||||
is NetworkResult.ApplicationError -> {
|
||||
|
||||
-121
@@ -1,121 +0,0 @@
|
||||
package org.thoughtcrime.securesms.registration.secondary
|
||||
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
import org.signal.libsignal.protocol.InvalidKeyException
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair
|
||||
import org.signal.libsignal.protocol.ecc.ECPrivateKey
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey
|
||||
import org.signal.libsignal.protocol.util.ByteUtil
|
||||
import org.thoughtcrime.securesms.devicelist.protos.DeviceName
|
||||
import java.nio.charset.Charset
|
||||
import java.security.GeneralSecurityException
|
||||
import java.security.MessageDigest
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
/**
|
||||
* Use to encrypt a secondary/linked device name.
|
||||
*/
|
||||
object DeviceNameCipher {
|
||||
|
||||
private val TAG = Log.tag(DeviceNameCipher::class.java)
|
||||
|
||||
private const val SYNTHETIC_IV_LENGTH = 16
|
||||
|
||||
@JvmStatic
|
||||
fun encryptDeviceName(plaintext: ByteArray, identityKeyPair: IdentityKeyPair): ByteArray {
|
||||
val ephemeralKeyPair: ECKeyPair = ECKeyPair.generate()
|
||||
val masterSecret: ByteArray = ephemeralKeyPair.privateKey.calculateAgreement(identityKeyPair.publicKey.publicKey)
|
||||
|
||||
val syntheticIv: ByteArray = computeSyntheticIv(masterSecret, plaintext)
|
||||
val cipherKey: ByteArray = computeCipherKey(masterSecret, syntheticIv)
|
||||
|
||||
val cipher = Cipher.getInstance("AES/CTR/NoPadding")
|
||||
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(createEmptyByteArray(16)))
|
||||
val cipherText = cipher.doFinal(plaintext)
|
||||
|
||||
return DeviceName(
|
||||
ephemeralPublic = ephemeralKeyPair.publicKey.serialize().toByteString(),
|
||||
syntheticIv = syntheticIv.toByteString(),
|
||||
ciphertext = cipherText.toByteString()
|
||||
).encode()
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts a [DeviceName]. Returns null if data is invalid/undecryptable.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun decryptDeviceName(deviceName: DeviceName, identityKeyPair: IdentityKeyPair): ByteArray? {
|
||||
if (deviceName.ephemeralPublic == null || deviceName.syntheticIv == null || deviceName.ciphertext == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return try {
|
||||
val syntheticIv = deviceName.syntheticIv.toByteArray()
|
||||
val cipherText = deviceName.ciphertext.toByteArray()
|
||||
val identityKey: ECPrivateKey = identityKeyPair.privateKey
|
||||
val ephemeralPublic = ECPublicKey(deviceName.ephemeralPublic.toByteArray())
|
||||
val masterSecret = identityKey.calculateAgreement(ephemeralPublic)
|
||||
|
||||
val mac = Mac.getInstance("HmacSHA256")
|
||||
mac.init(SecretKeySpec(masterSecret, "HmacSHA256"))
|
||||
val cipherKeyPart1 = mac.doFinal("cipher".toByteArray())
|
||||
|
||||
mac.init(SecretKeySpec(cipherKeyPart1, "HmacSHA256"))
|
||||
val cipherKey = mac.doFinal(syntheticIv)
|
||||
|
||||
val cipher = Cipher.getInstance("AES/CTR/NoPadding")
|
||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(ByteArray(16)))
|
||||
val plaintext = cipher.doFinal(cipherText)
|
||||
|
||||
mac.init(SecretKeySpec(masterSecret, "HmacSHA256"))
|
||||
val verificationPart1 = mac.doFinal("auth".toByteArray())
|
||||
|
||||
mac.init(SecretKeySpec(verificationPart1, "HmacSHA256"))
|
||||
val verificationPart2 = mac.doFinal(plaintext)
|
||||
val ourSyntheticIv = ByteUtil.trim(verificationPart2, 16)
|
||||
|
||||
if (!MessageDigest.isEqual(ourSyntheticIv, syntheticIv)) {
|
||||
throw GeneralSecurityException("The computed syntheticIv didn't match the actual syntheticIv.")
|
||||
}
|
||||
|
||||
plaintext
|
||||
} catch (e: GeneralSecurityException) {
|
||||
Log.w(TAG, "Failed to decrypt device name.", e)
|
||||
null
|
||||
} catch (e: InvalidKeyException) {
|
||||
Log.w(TAG, "Failed to decrypt device name.", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeCipherKey(masterSecret: ByteArray, syntheticIv: ByteArray): ByteArray {
|
||||
val input = "cipher".toByteArray(Charset.forName("UTF-8"))
|
||||
|
||||
val keyMac = Mac.getInstance("HmacSHA256")
|
||||
keyMac.init(SecretKeySpec(masterSecret, "HmacSHA256"))
|
||||
val cipherKeyKey: ByteArray = keyMac.doFinal(input)
|
||||
|
||||
val cipherMac = Mac.getInstance("HmacSHA256")
|
||||
cipherMac.init(SecretKeySpec(cipherKeyKey, "HmacSHA256"))
|
||||
return cipherMac.doFinal(syntheticIv)
|
||||
}
|
||||
|
||||
private fun computeSyntheticIv(masterSecret: ByteArray, plaintext: ByteArray): ByteArray {
|
||||
val input = "auth".toByteArray(Charset.forName("UTF-8"))
|
||||
|
||||
val keyMac = Mac.getInstance("HmacSHA256")
|
||||
keyMac.init(SecretKeySpec(masterSecret, "HmacSHA256"))
|
||||
val syntheticIvKey: ByteArray = keyMac.doFinal(input)
|
||||
|
||||
val ivMac = Mac.getInstance("HmacSHA256")
|
||||
ivMac.init(SecretKeySpec(syntheticIvKey, "HmacSHA256"))
|
||||
return ivMac.doFinal(plaintext).sliceArray(0 until SYNTHETIC_IV_LENGTH)
|
||||
}
|
||||
|
||||
private fun createEmptyByteArray(length: Int): ByteArray = ByteArray(length)
|
||||
}
|
||||
+283
-3
@@ -6,8 +6,10 @@
|
||||
package org.thoughtcrime.securesms.registration.v2
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.isActive
|
||||
@@ -16,25 +18,33 @@ import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.signal.core.models.AccountEntropyPool
|
||||
import org.signal.core.models.MasterKey
|
||||
import org.signal.core.models.ServiceId.ACI
|
||||
import org.signal.core.models.ServiceId.PNI
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.net.RequestResult
|
||||
import org.signal.libsignal.protocol.IdentityKey
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
import org.signal.libsignal.protocol.ecc.ECPrivateKey
|
||||
import org.signal.network.NetworkResult
|
||||
import org.signal.registration.LinkAndSyncWaitResult
|
||||
import org.signal.registration.NetworkController
|
||||
import org.signal.registration.NetworkController.AccountAttributes
|
||||
import org.signal.registration.NetworkController.BackupMasterKeyError
|
||||
import org.signal.registration.NetworkController.CheckSvrCredentialsError
|
||||
import org.signal.registration.NetworkController.CheckSvrCredentialsResponse
|
||||
import org.signal.registration.NetworkController.CreateSessionError
|
||||
import org.signal.registration.NetworkController.DeviceAttributes
|
||||
import org.signal.registration.NetworkController.GetSessionStatusError
|
||||
import org.signal.registration.NetworkController.GetSvrCredentialsError
|
||||
import org.signal.registration.NetworkController.LinkDeviceProvisioningEvent
|
||||
import org.signal.registration.NetworkController.LinkDeviceProvisioningMessage
|
||||
import org.signal.registration.NetworkController.LinkDeviceResponse
|
||||
import org.signal.registration.NetworkController.PreKeyCollection
|
||||
import org.signal.registration.NetworkController.ProvisioningEvent
|
||||
import org.signal.registration.NetworkController.ProvisioningMessage
|
||||
import org.signal.registration.NetworkController.RegisterAccountError
|
||||
import org.signal.registration.NetworkController.RegisterAccountResponse
|
||||
import org.signal.registration.NetworkController.RegisterAsLinkedDeviceError
|
||||
import org.signal.registration.NetworkController.RegistrationLockResponse
|
||||
import org.signal.registration.NetworkController.RequestVerificationCodeError
|
||||
import org.signal.registration.NetworkController.RestoreAccountRecordError
|
||||
@@ -68,21 +78,36 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.registration.fcm.PushChallengeRequest
|
||||
import org.thoughtcrime.securesms.registration.ui.restore.StorageServiceRestore
|
||||
import org.thoughtcrime.securesms.registration.util.RegistrationUtil
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.whispersystems.signalservice.api.SvrNoDataException
|
||||
import org.whispersystems.signalservice.api.archive.ArchiveServiceAccess
|
||||
import org.whispersystems.signalservice.api.link.TransferArchiveResponse
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage
|
||||
import org.whispersystems.signalservice.api.provisioning.ProvisioningSocket
|
||||
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery.BackupResponse
|
||||
import org.whispersystems.signalservice.internal.crypto.SecondaryProvisioningCipher
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
import org.whispersystems.signalservice.internal.push.GcmRegistrationId
|
||||
import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity
|
||||
import org.whispersystems.signalservice.internal.push.ProvisionMessage
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket
|
||||
import org.whispersystems.signalservice.internal.push.RegisterAsSecondaryDeviceRequest
|
||||
import org.whispersystems.signalservice.internal.push.SyncMessage
|
||||
import java.io.Closeable
|
||||
import java.io.IOException
|
||||
import java.util.Locale
|
||||
import kotlin.coroutines.coroutineContext
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes as ServiceAccountAttributes
|
||||
import org.whispersystems.signalservice.api.account.DeviceAttributes as ServiceDeviceAttributes
|
||||
import org.whispersystems.signalservice.api.account.PreKeyCollection as ServicePreKeyCollection
|
||||
import org.whispersystems.signalservice.api.provisioning.RestoreMethod as ServiceRestoreMethod
|
||||
|
||||
@@ -97,6 +122,7 @@ class AppRegistrationNetworkController(
|
||||
companion object {
|
||||
private val TAG = Log.tag(AppRegistrationNetworkController::class)
|
||||
private val PUSH_REQUEST_TIMEOUT = 5.seconds.inWholeMilliseconds
|
||||
private val RETRY_BACKOFF = 5.seconds
|
||||
}
|
||||
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
@@ -740,7 +766,7 @@ class AppRegistrationNetworkController(
|
||||
}
|
||||
|
||||
override fun startProvisioning(): Flow<ProvisioningEvent> = callbackFlow {
|
||||
val socketHandles = mutableListOf<java.io.Closeable>()
|
||||
val socketHandles = mutableListOf<Closeable>()
|
||||
val configuration = AppDependencies.signalServiceNetworkAccess.getConfiguration()
|
||||
|
||||
fun startSocket() {
|
||||
@@ -804,7 +830,7 @@ class AppRegistrationNetworkController(
|
||||
val rotationJob = launch {
|
||||
var count = 0
|
||||
while (count < 5 && isActive) {
|
||||
kotlinx.coroutines.delay(ProvisioningSocket.LIFESPAN / 2)
|
||||
delay(ProvisioningSocket.LIFESPAN / 2)
|
||||
if (isActive) {
|
||||
startSocket()
|
||||
count++
|
||||
@@ -822,6 +848,250 @@ class AppRegistrationNetworkController(
|
||||
}
|
||||
}
|
||||
|
||||
override fun startLinkDeviceProvisioning(): Flow<LinkDeviceProvisioningEvent> = callbackFlow {
|
||||
val socketHandles = mutableListOf<Closeable>()
|
||||
val configuration = AppDependencies.signalServiceNetworkAccess.getConfiguration()
|
||||
|
||||
fun startSocket() {
|
||||
val handle = ProvisioningSocket.start<ProvisionMessage>(
|
||||
mode = ProvisioningSocket.Mode.LINK,
|
||||
identityKeyPair = IdentityKeyPair.generate(),
|
||||
configuration = configuration,
|
||||
handler = { id, t ->
|
||||
Log.w(TAG, "[startLinkDeviceProvisioning] Socket [$id] failed", t)
|
||||
trySend(LinkDeviceProvisioningEvent.Error(t))
|
||||
}
|
||||
) { socket ->
|
||||
val url = socket.getProvisioningUrl()
|
||||
trySend(LinkDeviceProvisioningEvent.QrCodeReady(url))
|
||||
|
||||
val result = socket.getProvisioningMessageDecryptResult()
|
||||
|
||||
if (result is SecondaryProvisioningCipher.ProvisioningDecryptResult.Success) {
|
||||
val msg = result.message
|
||||
val aci = msg.aciBinary?.let { ACI.parseOrThrow(it) } ?: ACI.parseOrThrow(msg.aci)
|
||||
val pni = msg.pniBinary?.let { PNI.parseOrThrow(it) } ?: PNI.parseOrThrow(msg.pni)
|
||||
|
||||
trySend(
|
||||
LinkDeviceProvisioningEvent.MessageReceived(
|
||||
LinkDeviceProvisioningMessage(
|
||||
e164 = msg.number!!,
|
||||
provisioningCode = msg.provisioningCode!!,
|
||||
aci = aci.toString(),
|
||||
pni = pni.toString(),
|
||||
aciIdentityKeyPair = IdentityKeyPair(IdentityKey(msg.aciIdentityKeyPublic!!.toByteArray()), ECPrivateKey(msg.aciIdentityKeyPrivate!!.toByteArray())),
|
||||
pniIdentityKeyPair = IdentityKeyPair(IdentityKey(msg.pniIdentityKeyPublic!!.toByteArray()), ECPrivateKey(msg.pniIdentityKeyPrivate!!.toByteArray())),
|
||||
profileKey = msg.profileKey!!.toByteArray(),
|
||||
ephemeralBackupKey = msg.ephemeralBackupKey,
|
||||
accountEntropyPool = msg.accountEntropyPool,
|
||||
mediaRootBackupKey = msg.mediaRootBackupKey,
|
||||
readReceipts = msg.readReceipts
|
||||
)
|
||||
)
|
||||
)
|
||||
channel.close()
|
||||
} else {
|
||||
Log.w(TAG, "[startLinkDeviceProvisioning] Failed to decrypt provisioning message")
|
||||
trySend(LinkDeviceProvisioningEvent.Error(IOException("Failed to decrypt provisioning message")))
|
||||
}
|
||||
}
|
||||
|
||||
synchronized(socketHandles) {
|
||||
socketHandles += handle
|
||||
if (socketHandles.size > 2) {
|
||||
socketHandles.removeAt(0).close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startSocket()
|
||||
|
||||
val rotationJob = launch {
|
||||
var count = 0
|
||||
while (count < 5 && isActive) {
|
||||
delay(ProvisioningSocket.LIFESPAN / 2)
|
||||
if (isActive) {
|
||||
startSocket()
|
||||
count++
|
||||
Log.d(TAG, "[startLinkDeviceProvisioning] Rotated socket, count: $count")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
awaitClose {
|
||||
rotationJob.cancel()
|
||||
synchronized(socketHandles) {
|
||||
socketHandles.forEach { it.close() }
|
||||
socketHandles.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun registerAsLinkedDevice(
|
||||
e164: String,
|
||||
password: String,
|
||||
provisioningCode: String,
|
||||
deviceAttributes: DeviceAttributes,
|
||||
aciPreKeys: PreKeyCollection,
|
||||
pniPreKeys: PreKeyCollection,
|
||||
fcmToken: String?
|
||||
): RequestResult<LinkDeviceResponse, RegisterAsLinkedDeviceError> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
pushServiceSocket.registerAsSecondaryDevice(
|
||||
e164,
|
||||
password,
|
||||
RegisterAsSecondaryDeviceRequest(
|
||||
verificationCode = provisioningCode,
|
||||
accountAttributes = deviceAttributes.toServiceDeviceAttributes(),
|
||||
aciSignedPreKey = SignedPreKeyEntity(aciPreKeys.signedPreKey.id.toLong(), aciPreKeys.signedPreKey.keyPair.publicKey, aciPreKeys.signedPreKey.signature),
|
||||
pniSignedPreKey = SignedPreKeyEntity(pniPreKeys.signedPreKey.id.toLong(), pniPreKeys.signedPreKey.keyPair.publicKey, pniPreKeys.signedPreKey.signature),
|
||||
aciPqLastResortPreKey = KyberPreKeyEntity(aciPreKeys.lastResortKyberPreKey.id.toLong(), aciPreKeys.lastResortKyberPreKey.keyPair.publicKey, aciPreKeys.lastResortKyberPreKey.signature),
|
||||
pniPqLastResortPreKey = KyberPreKeyEntity(pniPreKeys.lastResortKyberPreKey.id.toLong(), pniPreKeys.lastResortKyberPreKey.keyPair.publicKey, pniPreKeys.lastResortKyberPreKey.signature),
|
||||
gcmToken = fcmToken?.let { GcmRegistrationId(it, true) }
|
||||
)
|
||||
).use { response ->
|
||||
when (response.code) {
|
||||
200 -> RequestResult.Success(json.decodeFromString<LinkDeviceResponse>(response.body.string()))
|
||||
403 -> RequestResult.NonSuccess(RegisterAsLinkedDeviceError.IncorrectVerification)
|
||||
409 -> RequestResult.NonSuccess(RegisterAsLinkedDeviceError.MissingCapability)
|
||||
411 -> RequestResult.NonSuccess(RegisterAsLinkedDeviceError.MaxLinkedDevices)
|
||||
422 -> RequestResult.NonSuccess(RegisterAsLinkedDeviceError.InvalidRequest(response.body.string()))
|
||||
429 -> RequestResult.NonSuccess(RegisterAsLinkedDeviceError.RateLimited(response.retryAfter()))
|
||||
else -> RequestResult.ApplicationError(IllegalStateException("Unexpected response code: ${response.code}, body: ${response.body.string()}"))
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
RequestResult.RetryableNetworkError(e)
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
RequestResult.ApplicationError(e)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onLinkedDeviceRegistered() = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
RemoteConfig.refreshSync()
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "[onLinkedDeviceRegistered] Failed to refresh remote config.", e)
|
||||
}
|
||||
|
||||
for (type in SyncMessage.Request.Type.entries) {
|
||||
if (type == SyncMessage.Request.Type.UNKNOWN) {
|
||||
continue
|
||||
}
|
||||
|
||||
Log.i(TAG, "[onLinkedDeviceRegistered] Sending sync request for $type")
|
||||
try {
|
||||
retryWithBackoff {
|
||||
AppDependencies.signalServiceMessageSender.sendSyncMessage(
|
||||
SignalServiceSyncMessage.forRequest(RequestMessage(SyncMessage.Request(type = type)))
|
||||
)
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "[onLinkedDeviceRegistered] Failed to send sync request for $type after retries; continuing.", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T> retryWithBackoff(maxAttempts: Int = 3, initialDelay: Duration = 1.seconds, block: suspend () -> T): T {
|
||||
var attempt = 0
|
||||
while (true) {
|
||||
try {
|
||||
return block()
|
||||
} catch (e: IOException) {
|
||||
attempt++
|
||||
if (attempt >= maxAttempts) {
|
||||
throw e
|
||||
}
|
||||
val backoff = initialDelay * attempt
|
||||
Log.w(TAG, "[retryWithBackoff] Attempt $attempt failed; retrying in $backoff.", e)
|
||||
delay(backoff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun restoreLinkedDeviceFromStorageService() = withContext(Dispatchers.IO) {
|
||||
if (SignalStore.account.restoredAccountEntropyPoolFromPrimary) {
|
||||
Log.i(TAG, "[restoreLinkedDeviceFromStorageService] Restoring account data from storage service.")
|
||||
try {
|
||||
StorageServiceRestore.restore()
|
||||
} catch (e: CancellationException) {
|
||||
Log.i(TAG, "[restoreLinkedDeviceFromStorageService] Restoring account cancelled.", e)
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "[restoreLinkedDeviceFromStorageService] Storage service restore failed.", e)
|
||||
}
|
||||
} else {
|
||||
Log.i(TAG, "[restoreLinkedDeviceFromStorageService] No account entropy pool from primary; skipping storage service restore.")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun awaitLinkAndSyncArchive(): LinkAndSyncWaitResult = withContext(Dispatchers.IO) {
|
||||
val response = awaitTransferArchiveFromPrimary()
|
||||
val result = when {
|
||||
response == null -> LinkAndSyncWaitResult.ContinueWithoutBackup
|
||||
response.error == TransferArchiveResponse.ERROR_RELINK_REQUESTED -> LinkAndSyncWaitResult.RelinkRequired
|
||||
response.error == TransferArchiveResponse.ERROR_CONTINUE_WITHOUT_UPLOAD -> LinkAndSyncWaitResult.ContinueWithoutBackup
|
||||
response.hasArchive -> LinkAndSyncWaitResult.ArchiveAvailable(cdn = response.cdn!!, key = response.key!!)
|
||||
else -> LinkAndSyncWaitResult.ContinueWithoutBackup
|
||||
}
|
||||
Log.i(TAG, "[awaitLinkAndSyncArchive] Result: $result")
|
||||
result
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the primary device to make a link-and-sync transfer archive available, long-polling
|
||||
* [org.signal.network.api.LinkDeviceApi.waitForPrimaryDevice] and retrying transient errors until
|
||||
* [maxWaitTime] elapses. Returns null if no archive becomes available.
|
||||
*/
|
||||
private suspend fun awaitTransferArchiveFromPrimary(maxWaitTime: Duration = 1.hours): TransferArchiveResponse? {
|
||||
val startTime = System.currentTimeMillis()
|
||||
var timeRemaining = maxWaitTime.inWholeMilliseconds
|
||||
|
||||
while (timeRemaining > 0 && coroutineContext.isActive) {
|
||||
Log.d(TAG, "[awaitTransferArchiveFromPrimary] Willing to wait for $timeRemaining ms...")
|
||||
|
||||
when (val result = SignalNetwork.linkDevice.waitForPrimaryDevice(timeout = 60.seconds)) {
|
||||
is NetworkResult.Success -> {
|
||||
Log.i(TAG, "[awaitTransferArchiveFromPrimary] Primary responded (hasArchive=${result.result.hasArchive}, error=${result.result.error})")
|
||||
return result.result
|
||||
}
|
||||
is NetworkResult.ApplicationError -> {
|
||||
Log.w(TAG, "[awaitTransferArchiveFromPrimary] Error processing response", result.throwable)
|
||||
return null
|
||||
}
|
||||
is NetworkResult.NetworkError -> {
|
||||
Log.w(TAG, "[awaitTransferArchiveFromPrimary] Network error while waiting; will retry after $RETRY_BACKOFF.", result.exception)
|
||||
delay(RETRY_BACKOFF)
|
||||
}
|
||||
is NetworkResult.StatusCodeError -> {
|
||||
when (result.code) {
|
||||
400 -> {
|
||||
Log.w(TAG, "[awaitTransferArchiveFromPrimary] Invalid timeout.")
|
||||
return null
|
||||
}
|
||||
429 -> {
|
||||
Log.w(TAG, "[awaitTransferArchiveFromPrimary] Rate-limited; will retry after ${result.retryAfter()}.")
|
||||
result.retryAfter()?.let { delay(it) }
|
||||
}
|
||||
else -> {
|
||||
Log.w(TAG, "[awaitTransferArchiveFromPrimary] Unexpected status ${result.code}; will retry after $RETRY_BACKOFF.")
|
||||
delay(RETRY_BACKOFF)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
timeRemaining = maxWaitTime.inWholeMilliseconds - (System.currentTimeMillis() - startTime)
|
||||
}
|
||||
|
||||
Log.w(TAG, "[awaitTransferArchiveFromPrimary] No transfer archive from primary within $maxWaitTime.")
|
||||
return null
|
||||
}
|
||||
|
||||
private fun AccountAttributes.toServiceAccountAttributes(): ServiceAccountAttributes {
|
||||
return ServiceAccountAttributes(
|
||||
signalingKey,
|
||||
@@ -832,7 +1102,7 @@ class AppRegistrationNetworkController(
|
||||
unrestrictedUnidentifiedAccess,
|
||||
capabilities?.toServiceCapabilities(),
|
||||
discoverableByPhoneNumber,
|
||||
name,
|
||||
null,
|
||||
pniRegistrationId,
|
||||
recoveryPassword
|
||||
)
|
||||
@@ -848,6 +1118,16 @@ class AppRegistrationNetworkController(
|
||||
)
|
||||
}
|
||||
|
||||
private fun DeviceAttributes.toServiceDeviceAttributes(): ServiceDeviceAttributes {
|
||||
return ServiceDeviceAttributes(
|
||||
fetchesMessages = fetchesMessages,
|
||||
registrationId = registrationId,
|
||||
pniRegistrationId = pniRegistrationId,
|
||||
name = name,
|
||||
capabilities = capabilities?.toServiceCapabilities()
|
||||
)
|
||||
}
|
||||
|
||||
private fun PreKeyCollection.toServicePreKeyCollection(): ServicePreKeyCollection {
|
||||
return ServicePreKeyCollection(
|
||||
identityKey = identityKey,
|
||||
|
||||
+86
-7
@@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.registration.v2
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -23,6 +24,9 @@ import org.greenrobot.eventbus.ThreadMode
|
||||
import org.signal.archive.LocalBackupRestoreProgress
|
||||
import org.signal.core.models.AccountEntropyPool
|
||||
import org.signal.core.models.MasterKey
|
||||
import org.signal.core.models.backup.MessageBackupKey
|
||||
import org.signal.core.util.AppUtil
|
||||
import org.signal.core.util.Result
|
||||
import org.signal.core.util.StreamUtil
|
||||
import org.signal.core.util.crypto.AttachmentSecretProvider
|
||||
import org.signal.core.util.logging.Log
|
||||
@@ -32,6 +36,7 @@ import org.signal.registration.StorageController
|
||||
import org.signal.registration.StoredProfileData
|
||||
import org.signal.registration.proto.RegistrationData
|
||||
import org.signal.registration.screens.localbackuprestore.LocalBackupInfo
|
||||
import org.signal.registration.screens.messagesync.LinkAndSyncProgress
|
||||
import org.signal.registration.screens.remotebackuprestore.RemoteBackupRestoreProgress
|
||||
import org.thoughtcrime.securesms.backup.FullBackupImporter
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
@@ -42,10 +47,12 @@ import org.thoughtcrime.securesms.backup.v2.local.SnapshotFileSystem
|
||||
import org.thoughtcrime.securesms.crypto.AppAttachmentSecretStore
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.LinkedDeviceInfo
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.LocalRegistrationMetadata
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.RestoreDecisionState
|
||||
import org.thoughtcrime.securesms.keyvalue.Completed
|
||||
import org.thoughtcrime.securesms.keyvalue.NewAccount
|
||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.keyvalue.Skipped
|
||||
import org.thoughtcrime.securesms.keyvalue.isDecisionPending
|
||||
@@ -55,6 +62,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
|
||||
import org.thoughtcrime.securesms.registration.util.RegistrationUtil
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.api.link.TransferArchiveResponse
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.time.LocalDateTime
|
||||
@@ -105,6 +113,11 @@ class AppRegistrationStorageController(private val context: Context) : StorageCo
|
||||
Unit
|
||||
}
|
||||
|
||||
override suspend fun clearLocalDataAndRestart() = withContext(Dispatchers.Main) {
|
||||
Log.w(TAG, "[clearLocalDataAndRestart] Wiping all local app data and attempting to relaunch.")
|
||||
AppUtil.clearAllDataAndRestart(context)
|
||||
}
|
||||
|
||||
override suspend fun getStoredProfileData(): StoredProfileData = withContext(Dispatchers.IO) {
|
||||
if (!SignalStore.account.isRegistered) {
|
||||
return@withContext StoredProfileData()
|
||||
@@ -125,9 +138,9 @@ class AppRegistrationStorageController(private val context: Context) : StorageCo
|
||||
}
|
||||
|
||||
val discoverable: Boolean? = when (SignalStore.phoneNumberPrivacy.phoneNumberDiscoverabilityMode) {
|
||||
org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode.DISCOVERABLE -> true
|
||||
org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE -> false
|
||||
org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode.UNDECIDED -> null
|
||||
PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode.DISCOVERABLE -> true
|
||||
PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE -> false
|
||||
PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode.UNDECIDED -> null
|
||||
}
|
||||
|
||||
StoredProfileData(
|
||||
@@ -173,7 +186,11 @@ class AppRegistrationStorageController(private val context: Context) : StorageCo
|
||||
// than lazily generating a new AEP.
|
||||
val accountEntropyPool: AccountEntropyPool? = data.accountEntropyPool.takeIf { it.isNotEmpty() }?.let { AccountEntropyPool(it) }
|
||||
if (accountEntropyPool != null) {
|
||||
SignalStore.account.restoreAccountEntropyPool(accountEntropyPool)
|
||||
if (data.linkedDeviceData != null) {
|
||||
SignalStore.account.setAccountEntropyPoolFromPrimaryDevice(accountEntropyPool)
|
||||
} else {
|
||||
SignalStore.account.restoreAccountEntropyPool(accountEntropyPool)
|
||||
}
|
||||
}
|
||||
|
||||
val masterKey: MasterKey? = accountEntropyPool?.deriveMasterKey()
|
||||
@@ -215,15 +232,27 @@ class AppRegistrationStorageController(private val context: Context) : StorageCo
|
||||
fcmEnabled = SignalStore.account.fcmEnabled
|
||||
fcmToken = SignalStore.account.fcmToken ?: ""
|
||||
reglockEnabled = data.registrationLockEnabled
|
||||
|
||||
data.linkedDeviceData?.let { linkData ->
|
||||
linkedDeviceInfo = LinkedDeviceInfo(
|
||||
deviceId = linkData.deviceId,
|
||||
deviceName = linkData.deviceName,
|
||||
ephemeralBackupKey = linkData.ephemeralBackupKey,
|
||||
accountEntropyPool = data.accountEntropyPool,
|
||||
mediaRootBackupKey = linkData.mediaRootBackupKey
|
||||
)
|
||||
}
|
||||
}.build()
|
||||
|
||||
// TODO [greyson] Should probably move this stuff into this file as we get closer to being done
|
||||
RegistrationRepository.registerAccountLocally(context, metadata)
|
||||
SignalStore.registration.localRegistrationMetadata = metadata
|
||||
|
||||
data.linkedDeviceData?.readReceipts?.let { TextSecurePreferences.setReadReceiptsEnabled(context, it) }
|
||||
}
|
||||
|
||||
// Handle PIN/master key
|
||||
if (data.pin.isNotEmpty() && masterKey != null) {
|
||||
if (data.pin.isNotEmpty() && masterKey != null && data.linkedDeviceData == null) {
|
||||
SvrRepository.onRegistrationComplete(
|
||||
masterKey,
|
||||
data.pin,
|
||||
@@ -321,11 +350,11 @@ class AppRegistrationStorageController(private val context: Context) : StorageCo
|
||||
val snapshotFileSystem = SnapshotFileSystem(context, backupDir)
|
||||
|
||||
when (val result = LocalArchiver.import(snapshotFileSystem, selfData, messageBackupKey)) {
|
||||
is org.signal.core.util.Result.Success -> {
|
||||
is Result.Success -> {
|
||||
emit(LocalBackupRestoreProgress.Complete)
|
||||
Log.d(TAG, "V2 restore complete.")
|
||||
}
|
||||
is org.signal.core.util.Result.Failure -> {
|
||||
is Result.Failure -> {
|
||||
Log.w(TAG, "V2 restore failed: ${result.failure}")
|
||||
emit(LocalBackupRestoreProgress.Error(IOException("V2 restore failed: ${result.failure}")))
|
||||
}
|
||||
@@ -451,6 +480,56 @@ class AppRegistrationStorageController(private val context: Context) : StorageCo
|
||||
}
|
||||
}
|
||||
|
||||
override fun restoreLinkAndSyncBackup(cdn: Int, key: String): Flow<LinkAndSyncProgress> = callbackFlow {
|
||||
val ephemeralBackupKeyBytes = SignalStore.registration.localRegistrationMetadata?.linkedDeviceInfo?.ephemeralBackupKey?.toByteArray()
|
||||
|
||||
if (ephemeralBackupKeyBytes == null) {
|
||||
Log.i(TAG, "[restoreLinkAndSyncBackup] No ephemeral backup key present; nothing to restore.")
|
||||
trySend(LinkAndSyncProgress.Complete)
|
||||
channel.close()
|
||||
return@callbackFlow
|
||||
}
|
||||
|
||||
val subscriber = object {
|
||||
@Subscribe(threadMode = ThreadMode.POSTING)
|
||||
fun onRestoreEvent(event: RestoreV2Event) {
|
||||
val progress = when (event.type) {
|
||||
RestoreV2Event.Type.PROGRESS_DOWNLOAD -> LinkAndSyncProgress.Downloading(event.count, event.estimatedTotalCount)
|
||||
RestoreV2Event.Type.PROGRESS_RESTORE -> LinkAndSyncProgress.Restoring
|
||||
RestoreV2Event.Type.PROGRESS_FINALIZING -> LinkAndSyncProgress.Restoring
|
||||
}
|
||||
trySend(progress)
|
||||
}
|
||||
}
|
||||
|
||||
EventBus.getDefault().register(subscriber)
|
||||
|
||||
launch(Dispatchers.IO) {
|
||||
try {
|
||||
when (val result = BackupRepository.restoreLinkAndSyncBackup(TransferArchiveResponse(cdn = cdn, key = key), MessageBackupKey(ephemeralBackupKeyBytes))) {
|
||||
RemoteRestoreResult.Success -> send(LinkAndSyncProgress.Complete)
|
||||
RemoteRestoreResult.Canceled -> Log.i(TAG, "[restoreLinkAndSyncBackup] Restore canceled.")
|
||||
else -> {
|
||||
Log.w(TAG, "[restoreLinkAndSyncBackup] Link-and-sync restore did not succeed: $result")
|
||||
send(LinkAndSyncProgress.Failed())
|
||||
}
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
Log.d(TAG, "[restoreLinkAndSyncBackup] Restore cancelled, aborting.")
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "[restoreLinkAndSyncBackup] Link-and-sync restore failed.", e)
|
||||
send(LinkAndSyncProgress.Failed(e))
|
||||
} finally {
|
||||
channel.close()
|
||||
}
|
||||
}
|
||||
|
||||
awaitClose {
|
||||
EventBus.getDefault().unregister(subscriber)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun writeRegistrationData(data: RegistrationData) = withContext(Dispatchers.IO) {
|
||||
val file = File(context.cacheDir, TEMP_PROTO_FILENAME)
|
||||
file.writeBytes(RegistrationData.ADAPTER.encode(data))
|
||||
|
||||
Reference in New Issue
Block a user