mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-08-05 04:45:14 +01:00
Refactor regV5 account storage persistence.
This commit is contained in:
+264
-135
@@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.registration.v2
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.google.common.io.CountingInputStream
|
||||
@@ -18,13 +19,16 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
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.ServiceId
|
||||
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.models.backup.MessageBackupKey
|
||||
import org.signal.core.util.AppUtil
|
||||
import org.signal.core.util.Result
|
||||
@@ -32,11 +36,15 @@ import org.signal.core.util.StreamUtil
|
||||
import org.signal.core.util.crypto.AttachmentSecretProvider
|
||||
import org.signal.core.util.getLength
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
import org.signal.libsignal.protocol.state.KyberPreKeyRecord
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.signal.registration.PreExistingRegistrationData
|
||||
import org.signal.registration.RestoreDecision
|
||||
import org.signal.registration.StorageController
|
||||
import org.signal.registration.StoredProfileData
|
||||
import org.signal.registration.proto.AccountData
|
||||
import org.signal.registration.proto.RegistrationData
|
||||
import org.signal.registration.screens.localbackuprestore.LocalBackupInfo
|
||||
import org.signal.registration.screens.messagesync.LinkAndSyncProgress
|
||||
@@ -51,25 +59,37 @@ import org.thoughtcrime.securesms.backup.v2.local.ArchiveFileSystem
|
||||
import org.thoughtcrime.securesms.backup.v2.local.LocalArchiver
|
||||
import org.thoughtcrime.securesms.backup.v2.local.SnapshotFileSystem
|
||||
import org.thoughtcrime.securesms.crypto.AppAttachmentSecretStore
|
||||
import org.thoughtcrime.securesms.crypto.PreKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.SenderKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore
|
||||
import org.thoughtcrime.securesms.crypto.storage.SignalServiceAccountDataStoreImpl
|
||||
import org.thoughtcrime.securesms.database.IdentityTable
|
||||
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.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.runJobBlocking
|
||||
import org.thoughtcrime.securesms.jobs.CheckKeyTransparencyJob
|
||||
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob
|
||||
import org.thoughtcrime.securesms.jobs.LocalBackupRestoreMediaJob
|
||||
import org.thoughtcrime.securesms.jobs.PreKeysSyncJob
|
||||
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob
|
||||
import org.thoughtcrime.securesms.jobs.RotateCertificateJob
|
||||
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
|
||||
import org.thoughtcrime.securesms.notifications.NotificationIds
|
||||
import org.thoughtcrime.securesms.pin.SvrRepository
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.registration.util.RegistrationUtil
|
||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener
|
||||
import org.thoughtcrime.securesms.service.LocalBackupListener
|
||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener
|
||||
import org.thoughtcrime.securesms.util.BackupUtil
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.api.link.TransferArchiveResponse
|
||||
@@ -77,6 +97,7 @@ import java.io.File
|
||||
import java.io.IOException
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Implementation of [StorageController] that bridges to the app's existing storage infrastructure.
|
||||
@@ -182,100 +203,44 @@ class AppRegistrationStorageController(private val context: Context) : StorageCo
|
||||
|
||||
override suspend fun commitRegistrationData() = withContext(Dispatchers.IO) {
|
||||
val data = readInProgressRegistrationData()
|
||||
val accountData = data.accountData
|
||||
|
||||
// The account's master key is always the one derived from the AEP, which we expect to have by the time we commit.
|
||||
// Restore it up-front so any master-key-derived state we touch below resolves against the correct value rather
|
||||
// than lazily generating a new AEP.
|
||||
val accountEntropyPool: AccountEntropyPool? = data.accountEntropyPool.takeIf { it.isNotEmpty() }?.let { AccountEntropyPool(it) }
|
||||
if (accountEntropyPool != null) {
|
||||
if (data.linkedDeviceData != null) {
|
||||
SignalStore.account.setAccountEntropyPoolFromPrimaryDevice(accountEntropyPool)
|
||||
} else {
|
||||
SignalStore.account.restoreAccountEntropyPool(accountEntropyPool)
|
||||
}
|
||||
}
|
||||
val masterKey: MasterKey? = applyAccountEntropyPool(data)
|
||||
|
||||
val masterKey: MasterKey? = accountEntropyPool?.deriveMasterKey()
|
||||
|
||||
// Build LocalRegistrationMetadata if we have enough data for account setup
|
||||
if (data.e164.isNotEmpty() && data.aci.isNotEmpty() && data.pni.isNotEmpty() && data.servicePassword.isNotEmpty()) {
|
||||
val profileKey = RegistrationRepository.getProfileKey(data.e164)
|
||||
|
||||
val metadata = LocalRegistrationMetadata.Builder().apply {
|
||||
if (data.aciIdentityKeyPair.size > 0) {
|
||||
aciIdentityKeyPair = data.aciIdentityKeyPair
|
||||
}
|
||||
if (data.pniIdentityKeyPair.size > 0) {
|
||||
pniIdentityKeyPair = data.pniIdentityKeyPair
|
||||
}
|
||||
if (data.aciSignedPreKey.size > 0) {
|
||||
aciSignedPreKey = data.aciSignedPreKey
|
||||
}
|
||||
if (data.pniSignedPreKey.size > 0) {
|
||||
pniSignedPreKey = data.pniSignedPreKey
|
||||
}
|
||||
if (data.aciLastResortKyberPreKey.size > 0) {
|
||||
aciLastRestoreKyberPreKey = data.aciLastResortKyberPreKey
|
||||
}
|
||||
if (data.pniLastResortKyberPreKey.size > 0) {
|
||||
pniLastRestoreKyberPreKey = data.pniLastResortKyberPreKey
|
||||
}
|
||||
|
||||
aci = data.aci
|
||||
pni = data.pni
|
||||
e164 = data.e164
|
||||
this.servicePassword = data.servicePassword
|
||||
this.profileKey = profileKey.serialize().toByteString()
|
||||
hasPin = data.pin.isNotEmpty()
|
||||
if (data.pin.isNotEmpty()) {
|
||||
pin = data.pin
|
||||
masterKey?.let { this.masterKey = it.serialize().toByteString() }
|
||||
}
|
||||
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()
|
||||
|
||||
SignalStore.account.registrationId = data.aciRegistrationId
|
||||
SignalStore.account.pniRegistrationId = data.pniRegistrationId
|
||||
|
||||
// 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) }
|
||||
// We only want to apply account data a single time
|
||||
val svrStateUpdated = if (!data.accountDataCommitted && accountData != null && accountData.isComplete()) {
|
||||
applyAccountData(
|
||||
accountData = accountData,
|
||||
pin = data.pin,
|
||||
registrationLockEnabled = data.registrationLockEnabled,
|
||||
masterKey = masterKey
|
||||
)
|
||||
updateInProgressRegistrationData { accountDataCommitted = true }
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
// Handle PIN/master key
|
||||
if (data.pin.isNotEmpty() && masterKey != null && data.linkedDeviceData == null) {
|
||||
SvrRepository.onRegistrationComplete(
|
||||
masterKey,
|
||||
data.pin,
|
||||
true,
|
||||
data.registrationLockEnabled,
|
||||
data.accountEntropyPool.isNotEmpty()
|
||||
)
|
||||
} else if (data.pinOptedOut && data.linkedDeviceData == null) {
|
||||
if (data.pin.isNotEmpty() && masterKey != null && accountData?.linkedDeviceData == null) {
|
||||
// We call this same function in applyAccountData, so just avoiding double-calls
|
||||
if (!svrStateUpdated) {
|
||||
SvrRepository.onRegistrationComplete(
|
||||
masterKey = masterKey,
|
||||
userPin = data.pin,
|
||||
hasPinToRestore = true,
|
||||
setRegistrationLockEnabled = data.registrationLockEnabled,
|
||||
restoredAEP = data.accountEntropyPool.isNotEmpty()
|
||||
)
|
||||
}
|
||||
} else if (data.pinOptedOut && accountData?.linkedDeviceData == null) {
|
||||
Log.i(TAG, "[commitRegistrationData] User opted out of creating a PIN. Applying opt-out.")
|
||||
SvrRepository.optOutOfPin(rotateAep = false)
|
||||
}
|
||||
|
||||
// The temporaryMasterKey is the one-time key restored from SVR during re-registration. The account's own master key
|
||||
// is always the AEP-derived one above, so this is retained separately as the initial-restore key (used for the
|
||||
// first storage service sync + recovery password). It must be set last, as onRegistrationComplete will have cleared
|
||||
// the initial-restore key after recognizing the AEP-derived master key as our own.
|
||||
if (data.temporaryMasterKey.size > 0) {
|
||||
SignalStore.svr.masterKeyForInitialDataRestore = MasterKey(data.temporaryMasterKey.toByteArray())
|
||||
// This must be set last, as SvrRepository.onRegistrationComplete will have cleared the initial-restore key after recognizing the AEP-derived master key as our own.
|
||||
if (data.masterKeyForInitialDataRestore.size > 0) {
|
||||
SignalStore.svr.masterKeyForInitialDataRestore = MasterKey(data.masterKeyForInitialDataRestore.toByteArray())
|
||||
}
|
||||
|
||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||
@@ -322,6 +287,10 @@ class AppRegistrationStorageController(private val context: Context) : StorageCo
|
||||
return@launch
|
||||
}
|
||||
|
||||
// If this flow has already committed an account locally, keep its fresh key material out of the restore.
|
||||
val inProgressData = readInProgressRegistrationData()
|
||||
val excludeKeyTables = inProgressData.accountDataCommitted
|
||||
|
||||
val database = SignalDatabase.backupDatabase
|
||||
val inputStream = context.contentResolver.openInputStream(backupUri) ?: throw IOException("Unable to open backup stream for $backupUri")
|
||||
CountingInputStream(inputStream).use { counting ->
|
||||
@@ -332,7 +301,7 @@ class AppRegistrationStorageController(private val context: Context) : StorageCo
|
||||
database,
|
||||
counting,
|
||||
passphrase,
|
||||
SignalStore.registration.localRegistrationMetadata != null
|
||||
excludeKeyTables
|
||||
)
|
||||
}
|
||||
|
||||
@@ -342,6 +311,15 @@ class AppRegistrationStorageController(private val context: Context) : StorageCo
|
||||
// Reset it so the state we read below reflects the restored values rather than stale pre-restore ones.
|
||||
SignalStore.onPostBackupRestore()
|
||||
|
||||
// A post-registration restore clobbers parts of SignalStore.account with the backup's values -- V1 backups
|
||||
// carry the identity keys and AEP. Re-apply the frozen account data to heal the committed registration.
|
||||
val committedAccountData = inProgressData.accountData
|
||||
if (inProgressData.accountDataCommitted && committedAccountData != null) {
|
||||
Log.i(TAG, "V1 restore ran after an account was committed. Re-applying committed account data.")
|
||||
val masterKey = applyAccountEntropyPool(inProgressData)
|
||||
applyAccountData(committedAccountData, pin = inProgressData.pin, registrationLockEnabled = inProgressData.registrationLockEnabled, masterKey = masterKey)
|
||||
}
|
||||
|
||||
reenableLegacyLocalBackups(rootUri, passphrase)
|
||||
|
||||
trySend(readRestoredLocalBackupState(includePreRegistrationKeys = true))
|
||||
@@ -457,50 +435,6 @@ class AppRegistrationStorageController(private val context: Context) : StorageCo
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the restored backup folder as the backup directory and re-enables scheduled local backups, so the user
|
||||
* keeps getting backups after restoring. Best-effort: a failure here must not fail the restore itself.
|
||||
*/
|
||||
private fun reenableLegacyLocalBackups(rootUri: Uri, passphrase: String) {
|
||||
try {
|
||||
BackupPassphrase.set(context, passphrase)
|
||||
|
||||
val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
context.contentResolver.takePersistableUriPermission(rootUri, takeFlags)
|
||||
SignalStore.settings.setSignalBackupDirectory(rootUri)
|
||||
|
||||
if (BackupUtil.canUserAccessBackupDirectory(context)) {
|
||||
LocalBackupListener.setNextBackupTimeToIntervalFromNow(context)
|
||||
SignalStore.settings.isBackupEnabled = true
|
||||
LocalBackupListener.schedule(context)
|
||||
} else {
|
||||
Log.w(TAG, "Can't access restored backup directory; not re-enabling local backups.")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to re-enable local backups after V1 restore.", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readRestoredLocalBackupState(includePreRegistrationKeys: Boolean = false): LocalBackupRestoreProgress.Complete {
|
||||
val restoredPin = SignalStore.svr.pin?.takeIf { it.isNotBlank() }
|
||||
val restoredProfileKey = SignalStore.account.aci
|
||||
?.let { SignalDatabase.recipients.getByAci(it).getOrNull() }
|
||||
?.let { SignalDatabase.recipients.getRecord(it).profileKey }
|
||||
?.let { ProfileKey(it) }
|
||||
|
||||
val restoredAccountEntropyPool = if (includePreRegistrationKeys) SignalStore.account.accountEntropyPoolOrNull else null
|
||||
val restoredAciIdentityKey = if (includePreRegistrationKeys && SignalStore.account.hasAciIdentityKey()) SignalStore.account.aciIdentityKey else null
|
||||
val restoredPniIdentityKey = if (includePreRegistrationKeys && SignalStore.account.hasPniIdentityKey()) SignalStore.account.pniIdentityKey else null
|
||||
|
||||
return LocalBackupRestoreProgress.Complete(
|
||||
restoredSvrPin = restoredPin,
|
||||
restoredProfileKey = restoredProfileKey,
|
||||
restoredAccountEntropyPool = restoredAccountEntropyPool,
|
||||
restoredAciIdentityKey = restoredAciIdentityKey,
|
||||
restoredPniIdentityKey = restoredPniIdentityKey
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun scanLocalBackupFolder(folderUri: Uri): List<LocalBackupInfo> = withContext(Dispatchers.IO) {
|
||||
val folder = DocumentFile.fromTreeUri(context, folderUri) ?: return@withContext emptyList()
|
||||
val children = folder.listFiles()
|
||||
@@ -622,7 +556,7 @@ 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()
|
||||
val ephemeralBackupKeyBytes = readInProgressRegistrationData().accountData?.linkedDeviceData?.ephemeralBackupKey?.toByteArray()
|
||||
|
||||
if (ephemeralBackupKeyBytes == null) {
|
||||
Log.i(TAG, "[restoreLinkAndSyncBackup] No ephemeral backup key present; nothing to restore.")
|
||||
@@ -683,4 +617,199 @@ class AppRegistrationStorageController(private val context: Context) : StorageCo
|
||||
previousUri?.let { AppDependencies.blobs.delete(context, it) }
|
||||
Unit
|
||||
}
|
||||
|
||||
/**
|
||||
* The account's master key is always the one derived from the AEP, which we expect to have by the time we commit.
|
||||
* Restore it up-front so any master-key-derived state we touch afterwards resolves against the correct value rather
|
||||
* than lazily generating a new AEP. Returns the AEP-derived master key, if an AEP is present.
|
||||
*/
|
||||
private fun applyAccountEntropyPool(data: RegistrationData): MasterKey? {
|
||||
val accountEntropyPool = data.accountEntropyPool.takeIf { it.isNotEmpty() }?.let { AccountEntropyPool(it) } ?: return null
|
||||
|
||||
if (data.accountData?.linkedDeviceData != null) {
|
||||
SignalStore.account.setAccountEntropyPoolFromPrimaryDevice(accountEntropyPool)
|
||||
} else {
|
||||
SignalStore.account.restoreAccountEntropyPool(accountEntropyPool)
|
||||
}
|
||||
|
||||
return accountEntropyPool.deriveMasterKey()
|
||||
}
|
||||
|
||||
private fun AccountData.isComplete(): Boolean {
|
||||
return e164.isNotEmpty() && aci.isNotEmpty() && pni.isNotEmpty() && servicePassword.isNotEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the one-time [AccountData] to permanent storage, registering the account locally. This runs once per
|
||||
* registration, guarded by [RegistrationData.accountDataCommitted] -- the only re-application is after a
|
||||
* post-registration V1 backup restore, which clobbers parts of [SignalStore.account] and re-applies this frozen
|
||||
* data to heal them.
|
||||
*/
|
||||
private suspend fun applyAccountData(accountData: AccountData, pin: String, registrationLockEnabled: Boolean, masterKey: MasterKey?) {
|
||||
Log.i(TAG, "[applyAccountData] Registering account locally.")
|
||||
|
||||
SignalStore.account.registrationId = accountData.aciRegistrationId
|
||||
SignalStore.account.pniRegistrationId = accountData.pniRegistrationId
|
||||
|
||||
accountData.linkedDeviceData?.let {
|
||||
SignalStore.account.deviceId = it.deviceId
|
||||
SignalStore.account.deviceName = it.deviceName
|
||||
}
|
||||
|
||||
val aciIdentityKeyPair = IdentityKeyPair(accountData.aciIdentityKeyPair.toByteArray())
|
||||
val pniIdentityKeyPair = IdentityKeyPair(accountData.pniIdentityKeyPair.toByteArray())
|
||||
SignalStore.account.restoreAciIdentityKeyFromBackup(aciIdentityKeyPair.publicKey.serialize(), aciIdentityKeyPair.privateKey.serialize())
|
||||
SignalStore.account.restorePniIdentityKeyFromBackup(pniIdentityKeyPair.publicKey.serialize(), pniIdentityKeyPair.privateKey.serialize())
|
||||
|
||||
val aci = ACI.parseOrThrow(accountData.aci)
|
||||
val pni = PNI.parseOrThrow(accountData.pni)
|
||||
val isAciChanged = SignalStore.account.aci != aci
|
||||
|
||||
SignalStore.account.setAci(aci)
|
||||
SignalStore.account.setPni(pni)
|
||||
|
||||
AppDependencies.resetProtocolStores()
|
||||
|
||||
AppDependencies.protocolStore.aci().sessions().archiveAllSessions()
|
||||
AppDependencies.protocolStore.pni().sessions().archiveAllSessions()
|
||||
SenderKeyUtil.clearAllState()
|
||||
|
||||
val aciProtocolStore = AppDependencies.protocolStore.aci()
|
||||
val pniProtocolStore = AppDependencies.protocolStore.pni()
|
||||
|
||||
storeSignedAndLastResortPreKeys(aciProtocolStore, SignalStore.account.aciPreKeys, SignedPreKeyRecord(accountData.aciSignedPreKey.toByteArray()), KyberPreKeyRecord(accountData.aciLastResortKyberPreKey.toByteArray()))
|
||||
storeSignedAndLastResortPreKeys(pniProtocolStore, SignalStore.account.pniPreKeys, SignedPreKeyRecord(accountData.pniSignedPreKey.toByteArray()), KyberPreKeyRecord(accountData.pniLastResortKyberPreKey.toByteArray()))
|
||||
|
||||
val profileKey = getOrCreateProfileKey(accountData.e164)
|
||||
val recipientTable = SignalDatabase.recipients
|
||||
val selfId = Recipient.trustedPush(aci, pni, accountData.e164).id
|
||||
|
||||
recipientTable.setProfileSharing(selfId, true)
|
||||
recipientTable.markRegisteredOrThrow(selfId, aci)
|
||||
recipientTable.linkIdsForSelf(aci, pni, accountData.e164)
|
||||
recipientTable.setProfileKey(selfId, profileKey)
|
||||
|
||||
AppDependencies.recipientCache.clearSelf()
|
||||
|
||||
SignalStore.account.setE164(accountData.e164)
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
saveOwnIdentityKey(selfId, aci, aciProtocolStore, now)
|
||||
saveOwnIdentityKey(selfId, pni, pniProtocolStore, now)
|
||||
|
||||
accountData.linkedDeviceData?.mediaRootBackupKey?.let {
|
||||
SignalStore.backup.mediaRootBackupKey = MediaRootBackupKey(it.toByteArray())
|
||||
}
|
||||
|
||||
SignalStore.account.setServicePassword(accountData.servicePassword)
|
||||
SignalStore.account.setRegistered(registered = true, isAciChanged = isAciChanged)
|
||||
TextSecurePreferences.setPromptedPushRegistration(context, true)
|
||||
TextSecurePreferences.setUnauthorizedReceived(context, false)
|
||||
NotificationManagerCompat.from(context).cancel(NotificationIds.UNREGISTERED_NOTIFICATION_ID)
|
||||
|
||||
SvrRepository.onRegistrationComplete(
|
||||
masterKey = if (pin.isNotEmpty()) masterKey else null,
|
||||
userPin = pin.takeIf { it.isNotEmpty() },
|
||||
hasPinToRestore = pin.isNotEmpty(),
|
||||
setRegistrationLockEnabled = registrationLockEnabled,
|
||||
restoredAEP = SignalStore.account.restoredAccountEntropyPool
|
||||
)
|
||||
|
||||
AppDependencies.resetNetwork()
|
||||
AppDependencies.startNetwork()
|
||||
PreKeysSyncJob.enqueue()
|
||||
|
||||
recipientTable.clearSelfKeyTransparencyData()
|
||||
CheckKeyTransparencyJob.enqueueIfNecessary(addDelay = true)
|
||||
|
||||
val jobManager = AppDependencies.jobManager
|
||||
|
||||
if (accountData.linkedDeviceData == null) {
|
||||
jobManager.add(DirectoryRefreshJob(false))
|
||||
jobManager.add(RotateCertificateJob())
|
||||
|
||||
DirectoryRefreshListener.schedule(context)
|
||||
RotateSignedPreKeyListener.schedule(context)
|
||||
} else {
|
||||
SignalStore.account.isMultiDevice = true
|
||||
jobManager.runJobBlocking(RefreshOwnProfileJob(), 30.seconds)
|
||||
|
||||
jobManager.add(RotateCertificateJob())
|
||||
RotateSignedPreKeyListener.schedule(context)
|
||||
}
|
||||
|
||||
accountData.linkedDeviceData?.readReceipts?.let { TextSecurePreferences.setReadReceiptsEnabled(context, it) }
|
||||
}
|
||||
|
||||
private fun getOrCreateProfileKey(e164: String): ProfileKey {
|
||||
val existing = SignalDatabase.recipients.getByE164(e164).getOrNull()?.let { ProfileKeyUtil.profileKeyOrNull(Recipient.resolved(it).profileKey) }
|
||||
return existing ?: ProfileKeyUtil.createNew().also { Log.i(TAG, "[commitRegistrationData] No profile key found, created a new one") }
|
||||
}
|
||||
|
||||
private fun saveOwnIdentityKey(selfId: RecipientId, serviceId: ServiceId, protocolStore: SignalServiceAccountDataStoreImpl, now: Long) {
|
||||
protocolStore.identities().saveIdentityWithoutSideEffects(
|
||||
selfId,
|
||||
serviceId,
|
||||
protocolStore.identityKeyPair.publicKey,
|
||||
IdentityTable.VerifiedStatus.VERIFIED,
|
||||
true,
|
||||
now,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
private fun storeSignedAndLastResortPreKeys(protocolStore: SignalServiceAccountDataStoreImpl, metadataStore: PreKeyMetadataStore, signedPreKey: SignedPreKeyRecord, lastResortKyberPreKey: KyberPreKeyRecord) {
|
||||
PreKeyUtil.storeSignedPreKey(protocolStore, metadataStore, signedPreKey)
|
||||
metadataStore.isSignedPreKeyRegistered = true
|
||||
metadataStore.activeSignedPreKeyId = signedPreKey.id
|
||||
metadataStore.lastSignedPreKeyRotationTime = System.currentTimeMillis()
|
||||
|
||||
PreKeyUtil.storeLastResortKyberPreKey(protocolStore, metadataStore, lastResortKyberPreKey)
|
||||
metadataStore.lastResortKyberPreKeyId = lastResortKyberPreKey.id
|
||||
metadataStore.lastResortKyberPreKeyRotationTime = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the restored backup folder as the backup directory and re-enables scheduled local backups, so the user
|
||||
* keeps getting backups after restoring. Best-effort: a failure here must not fail the restore itself.
|
||||
*/
|
||||
private fun reenableLegacyLocalBackups(rootUri: Uri, passphrase: String) {
|
||||
try {
|
||||
BackupPassphrase.set(context, passphrase)
|
||||
|
||||
val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
context.contentResolver.takePersistableUriPermission(rootUri, takeFlags)
|
||||
SignalStore.settings.setSignalBackupDirectory(rootUri)
|
||||
|
||||
if (BackupUtil.canUserAccessBackupDirectory(context)) {
|
||||
LocalBackupListener.setNextBackupTimeToIntervalFromNow(context)
|
||||
SignalStore.settings.isBackupEnabled = true
|
||||
LocalBackupListener.schedule(context)
|
||||
} else {
|
||||
Log.w(TAG, "Can't access restored backup directory; not re-enabling local backups.")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to re-enable local backups after V1 restore.", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readRestoredLocalBackupState(includePreRegistrationKeys: Boolean = false): LocalBackupRestoreProgress.Complete {
|
||||
val restoredPin = SignalStore.svr.pin?.takeIf { it.isNotBlank() }
|
||||
val restoredProfileKey = SignalStore.account.aci
|
||||
?.let { SignalDatabase.recipients.getByAci(it).getOrNull() }
|
||||
?.let { SignalDatabase.recipients.getRecord(it).profileKey }
|
||||
?.let { ProfileKey(it) }
|
||||
|
||||
val restoredAccountEntropyPool = if (includePreRegistrationKeys) SignalStore.account.accountEntropyPoolOrNull else null
|
||||
val restoredAciIdentityKey = if (includePreRegistrationKeys && SignalStore.account.hasAciIdentityKey()) SignalStore.account.aciIdentityKey else null
|
||||
val restoredPniIdentityKey = if (includePreRegistrationKeys && SignalStore.account.hasPniIdentityKey()) SignalStore.account.pniIdentityKey else null
|
||||
|
||||
return LocalBackupRestoreProgress.Complete(
|
||||
restoredSvrPin = restoredPin,
|
||||
restoredProfileKey = restoredProfileKey,
|
||||
restoredAccountEntropyPool = restoredAccountEntropyPool,
|
||||
restoredAciIdentityKey = restoredAciIdentityKey,
|
||||
restoredPniIdentityKey = restoredPniIdentityKey
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+32
-30
@@ -39,6 +39,7 @@ import org.signal.registration.PreExistingRegistrationData
|
||||
import org.signal.registration.RestoreDecision
|
||||
import org.signal.registration.StorageController
|
||||
import org.signal.registration.StoredProfileData
|
||||
import org.signal.registration.proto.AccountData
|
||||
import org.signal.registration.proto.ProvisioningData
|
||||
import org.signal.registration.proto.RegistrationData
|
||||
import org.signal.registration.sample.RegistrationApplication
|
||||
@@ -123,22 +124,23 @@ class DemoStorageController(private val context: Context) : StorageController {
|
||||
override suspend fun commitRegistrationData() = withContext(Dispatchers.IO) {
|
||||
val file = File(context.filesDir, TEMP_PROTO_FILENAME)
|
||||
val data = RegistrationData.ADAPTER.decode(file.readBytes())
|
||||
val accountData = data.accountData ?: AccountData()
|
||||
|
||||
// Key material
|
||||
if (data.aciIdentityKeyPair.size > 0) {
|
||||
RegistrationPreferences.aciIdentityKeyPair = IdentityKeyPair(data.aciIdentityKeyPair.toByteArray())
|
||||
if (accountData.aciIdentityKeyPair.size > 0) {
|
||||
RegistrationPreferences.aciIdentityKeyPair = IdentityKeyPair(accountData.aciIdentityKeyPair.toByteArray())
|
||||
}
|
||||
if (data.pniIdentityKeyPair.size > 0) {
|
||||
RegistrationPreferences.pniIdentityKeyPair = IdentityKeyPair(data.pniIdentityKeyPair.toByteArray())
|
||||
if (accountData.pniIdentityKeyPair.size > 0) {
|
||||
RegistrationPreferences.pniIdentityKeyPair = IdentityKeyPair(accountData.pniIdentityKeyPair.toByteArray())
|
||||
}
|
||||
if (data.aciRegistrationId != 0) {
|
||||
RegistrationPreferences.aciRegistrationId = data.aciRegistrationId
|
||||
if (accountData.aciRegistrationId != 0) {
|
||||
RegistrationPreferences.aciRegistrationId = accountData.aciRegistrationId
|
||||
}
|
||||
if (data.pniRegistrationId != 0) {
|
||||
RegistrationPreferences.pniRegistrationId = data.pniRegistrationId
|
||||
if (accountData.pniRegistrationId != 0) {
|
||||
RegistrationPreferences.pniRegistrationId = accountData.pniRegistrationId
|
||||
}
|
||||
if (data.servicePassword.isNotEmpty()) {
|
||||
RegistrationPreferences.servicePassword = data.servicePassword
|
||||
if (accountData.servicePassword.isNotEmpty()) {
|
||||
RegistrationPreferences.servicePassword = accountData.servicePassword
|
||||
}
|
||||
if (data.accountEntropyPool.isNotEmpty()) {
|
||||
RegistrationPreferences.aep = AccountEntropyPool(data.accountEntropyPool)
|
||||
@@ -146,30 +148,30 @@ class DemoStorageController(private val context: Context) : StorageController {
|
||||
if (data.profileKey.size > 0) {
|
||||
RegistrationPreferences.profileKey = ProfileKey(data.profileKey.toByteArray())
|
||||
}
|
||||
RegistrationPreferences.fetchesMessages = data.fetchesMessages
|
||||
RegistrationPreferences.fetchesMessages = accountData.fetchesMessages
|
||||
|
||||
// Pre-keys
|
||||
if (data.aciSignedPreKey.size > 0) {
|
||||
db.signedPreKeys.insert(RegistrationDatabase.ACCOUNT_TYPE_ACI, SignedPreKeyRecord(data.aciSignedPreKey.toByteArray()))
|
||||
if (accountData.aciSignedPreKey.size > 0) {
|
||||
db.signedPreKeys.insert(RegistrationDatabase.ACCOUNT_TYPE_ACI, SignedPreKeyRecord(accountData.aciSignedPreKey.toByteArray()))
|
||||
}
|
||||
if (data.pniSignedPreKey.size > 0) {
|
||||
db.signedPreKeys.insert(RegistrationDatabase.ACCOUNT_TYPE_PNI, SignedPreKeyRecord(data.pniSignedPreKey.toByteArray()))
|
||||
if (accountData.pniSignedPreKey.size > 0) {
|
||||
db.signedPreKeys.insert(RegistrationDatabase.ACCOUNT_TYPE_PNI, SignedPreKeyRecord(accountData.pniSignedPreKey.toByteArray()))
|
||||
}
|
||||
if (data.aciLastResortKyberPreKey.size > 0) {
|
||||
db.kyberPreKeys.insert(RegistrationDatabase.ACCOUNT_TYPE_ACI, KyberPreKeyRecord(data.aciLastResortKyberPreKey.toByteArray()))
|
||||
if (accountData.aciLastResortKyberPreKey.size > 0) {
|
||||
db.kyberPreKeys.insert(RegistrationDatabase.ACCOUNT_TYPE_ACI, KyberPreKeyRecord(accountData.aciLastResortKyberPreKey.toByteArray()))
|
||||
}
|
||||
if (data.pniLastResortKyberPreKey.size > 0) {
|
||||
db.kyberPreKeys.insert(RegistrationDatabase.ACCOUNT_TYPE_PNI, KyberPreKeyRecord(data.pniLastResortKyberPreKey.toByteArray()))
|
||||
if (accountData.pniLastResortKyberPreKey.size > 0) {
|
||||
db.kyberPreKeys.insert(RegistrationDatabase.ACCOUNT_TYPE_PNI, KyberPreKeyRecord(accountData.pniLastResortKyberPreKey.toByteArray()))
|
||||
}
|
||||
|
||||
// Account identity
|
||||
if (data.e164.isNotEmpty() && data.aci.isNotEmpty() && data.pni.isNotEmpty() && data.servicePassword.isNotEmpty() && data.accountEntropyPool.isNotEmpty()) {
|
||||
if (accountData.e164.isNotEmpty() && accountData.aci.isNotEmpty() && accountData.pni.isNotEmpty() && accountData.servicePassword.isNotEmpty() && data.accountEntropyPool.isNotEmpty()) {
|
||||
RegistrationPreferences.saveRegistrationData(
|
||||
NewRegistrationData(
|
||||
e164 = data.e164,
|
||||
aci = ACI.parseOrThrow(data.aci),
|
||||
pni = PNI.parseOrThrow(data.pni),
|
||||
servicePassword = data.servicePassword,
|
||||
e164 = accountData.e164,
|
||||
aci = ACI.parseOrThrow(accountData.aci),
|
||||
pni = PNI.parseOrThrow(accountData.pni),
|
||||
servicePassword = accountData.servicePassword,
|
||||
aep = AccountEntropyPool(data.accountEntropyPool)
|
||||
)
|
||||
)
|
||||
@@ -177,7 +179,7 @@ class DemoStorageController(private val context: Context) : StorageController {
|
||||
|
||||
// Linked-device data (persisted so the link-and-sync step can authenticate as this device and the
|
||||
// home screen can show the linked account).
|
||||
data.linkedDeviceData?.let { linkData ->
|
||||
accountData.linkedDeviceData?.let { linkData ->
|
||||
RegistrationPreferences.linkedDeviceId = linkData.deviceId
|
||||
RegistrationPreferences.ephemeralBackupKey = linkData.ephemeralBackupKey?.toByteArray()
|
||||
}
|
||||
@@ -187,8 +189,8 @@ class DemoStorageController(private val context: Context) : StorageController {
|
||||
RegistrationPreferences.pin = data.pin
|
||||
RegistrationPreferences.pinAlphanumeric = data.pin.any { !it.isDigit() }
|
||||
}
|
||||
if (data.temporaryMasterKey.size > 0) {
|
||||
RegistrationPreferences.temporaryMasterKey = MasterKey(data.temporaryMasterKey.toByteArray())
|
||||
if (data.masterKeyForInitialDataRestore.size > 0) {
|
||||
RegistrationPreferences.temporaryMasterKey = MasterKey(data.masterKeyForInitialDataRestore.toByteArray())
|
||||
}
|
||||
RegistrationPreferences.registrationLockEnabled = data.registrationLockEnabled
|
||||
|
||||
@@ -204,10 +206,10 @@ class DemoStorageController(private val context: Context) : StorageController {
|
||||
RegistrationPreferences.saveProvisioningData(
|
||||
NetworkController.ProvisioningMessage(
|
||||
accountEntropyPool = data.accountEntropyPool,
|
||||
e164 = data.e164,
|
||||
e164 = accountData.e164,
|
||||
pin = data.pin.ifEmpty { null },
|
||||
aciIdentityKeyPair = IdentityKeyPair(data.aciIdentityKeyPair.toByteArray()),
|
||||
pniIdentityKeyPair = IdentityKeyPair(data.pniIdentityKeyPair.toByteArray()),
|
||||
aciIdentityKeyPair = IdentityKeyPair(accountData.aciIdentityKeyPair.toByteArray()),
|
||||
pniIdentityKeyPair = IdentityKeyPair(accountData.pniIdentityKeyPair.toByteArray()),
|
||||
platform = when (prov.platform) {
|
||||
ProvisioningData.Platform.ANDROID -> NetworkController.ProvisioningMessage.Platform.ANDROID
|
||||
ProvisioningData.Platform.IOS -> NetworkController.ProvisioningMessage.Platform.IOS
|
||||
|
||||
+33
-16
@@ -50,6 +50,7 @@ import org.signal.registration.NetworkController.RestoreMasterKeyError
|
||||
import org.signal.registration.NetworkController.SessionMetadata
|
||||
import org.signal.registration.NetworkController.SvrCredentials
|
||||
import org.signal.registration.NetworkController.UpdateSessionError
|
||||
import org.signal.registration.proto.AccountData
|
||||
import org.signal.registration.proto.LinkedDeviceData
|
||||
import org.signal.registration.proto.ProvisioningData
|
||||
import org.signal.registration.proto.SvrCredential
|
||||
@@ -206,7 +207,7 @@ class RegistrationRepository(val context: Context, val networkController: Networ
|
||||
if (it is RequestResult.Success) {
|
||||
storageController.updateInProgressRegistrationData {
|
||||
this.pin = pin
|
||||
this.temporaryMasterKey = it.result.masterKey.serialize().toByteString()
|
||||
this.masterKeyForInitialDataRestore = it.result.masterKey.serialize().toByteString()
|
||||
this.registrationLockEnabled = forRegistrationLock
|
||||
this.svrCredentials += SvrCredential(username = svrCredentials.username, password = svrCredentials.password)
|
||||
}
|
||||
@@ -341,6 +342,10 @@ class RegistrationRepository(val context: Context, val networkController: Networ
|
||||
)
|
||||
|
||||
storageController.updateInProgressRegistrationData {
|
||||
this.profileKey = keyMaterial.profileKey.toByteString()
|
||||
this.accountEntropyPool = keyMaterial.accountEntropyPool.value
|
||||
}
|
||||
updateAccountData {
|
||||
this.aciIdentityKeyPair = keyMaterial.aciIdentityKeyPair.serialize().toByteString()
|
||||
this.pniIdentityKeyPair = keyMaterial.pniIdentityKeyPair.serialize().toByteString()
|
||||
this.aciSignedPreKey = keyMaterial.aciSignedPreKey.serialize().toByteString()
|
||||
@@ -350,14 +355,12 @@ class RegistrationRepository(val context: Context, val networkController: Networ
|
||||
this.aciRegistrationId = keyMaterial.aciRegistrationId
|
||||
this.pniRegistrationId = keyMaterial.pniRegistrationId
|
||||
this.unidentifiedAccessKey = keyMaterial.unidentifiedAccessKey.toByteString()
|
||||
this.profileKey = keyMaterial.profileKey.toByteString()
|
||||
this.servicePassword = keyMaterial.servicePassword
|
||||
this.accountEntropyPool = keyMaterial.accountEntropyPool.value
|
||||
}
|
||||
|
||||
val fcmToken = networkController.getFcmToken()
|
||||
|
||||
storageController.updateInProgressRegistrationData {
|
||||
updateAccountData {
|
||||
this.fetchesMessages = fcmToken == null
|
||||
}
|
||||
|
||||
@@ -394,7 +397,7 @@ class RegistrationRepository(val context: Context, val networkController: Networ
|
||||
)
|
||||
|
||||
if (result is RequestResult.Success) {
|
||||
storageController.updateInProgressRegistrationData {
|
||||
updateAccountData {
|
||||
this.e164 = e164
|
||||
this.aci = aci.toString()
|
||||
this.pni = pni.toString()
|
||||
@@ -441,7 +444,7 @@ class RegistrationRepository(val context: Context, val networkController: Networ
|
||||
* Waits for the primary to make a link-and-sync archive available.
|
||||
*/
|
||||
suspend fun awaitLinkAndSyncArchive(): LinkAndSyncWaitResult = withContext(Dispatchers.IO) {
|
||||
val ephemeralBackupKey = storageController.readInProgressRegistrationData().linkedDeviceData?.ephemeralBackupKey
|
||||
val ephemeralBackupKey = storageController.readInProgressRegistrationData().accountData?.linkedDeviceData?.ephemeralBackupKey
|
||||
if (ephemeralBackupKey == null) {
|
||||
Log.i(TAG, "[awaitLinkAndSyncArchive] No ephemeral backup key in registration data; no archive expected.")
|
||||
return@withContext LinkAndSyncWaitResult.ContinueWithoutBackup
|
||||
@@ -550,8 +553,8 @@ class RegistrationRepository(val context: Context, val networkController: Networ
|
||||
Log.i(TAG, "[registerAccount] Starting registration for $e164. sessionId: ${sessionId != null}, recoveryPassword: ${recoveryPassword != null}, registrationLock: ${registrationLock != null}, skipDeviceTransfer: $skipDeviceTransfer, existingAep: ${existingAccountEntropyPool != null}")
|
||||
|
||||
val inProgressData = storageController.readInProgressRegistrationData()
|
||||
val resumedAciIdentityKeyPair = inProgressData.aciIdentityKeyPair.takeIf { it.size > 0 }?.let { IdentityKeyPair(it.toByteArray()) }
|
||||
val resumedPniIdentityKeyPair = inProgressData.pniIdentityKeyPair.takeIf { it.size > 0 }?.let { IdentityKeyPair(it.toByteArray()) }
|
||||
val resumedAciIdentityKeyPair = inProgressData.accountData?.aciIdentityKeyPair?.takeIf { it.size > 0 }?.let { IdentityKeyPair(it.toByteArray()) }
|
||||
val resumedPniIdentityKeyPair = inProgressData.accountData?.pniIdentityKeyPair?.takeIf { it.size > 0 }?.let { IdentityKeyPair(it.toByteArray()) }
|
||||
val resumedProfileKey = inProgressData.profileKey.takeIf { it.size > 0 }?.let { ProfileKey(it.toByteArray()) }
|
||||
|
||||
val keyMaterial = generateKeyMaterial(
|
||||
@@ -562,6 +565,10 @@ class RegistrationRepository(val context: Context, val networkController: Networ
|
||||
)
|
||||
|
||||
storageController.updateInProgressRegistrationData {
|
||||
this.profileKey = keyMaterial.profileKey.toByteString()
|
||||
this.accountEntropyPool = keyMaterial.accountEntropyPool.value
|
||||
}
|
||||
updateAccountData {
|
||||
this.aciIdentityKeyPair = keyMaterial.aciIdentityKeyPair.serialize().toByteString()
|
||||
this.pniIdentityKeyPair = keyMaterial.pniIdentityKeyPair.serialize().toByteString()
|
||||
this.aciSignedPreKey = keyMaterial.aciSignedPreKey.serialize().toByteString()
|
||||
@@ -571,14 +578,12 @@ class RegistrationRepository(val context: Context, val networkController: Networ
|
||||
this.aciRegistrationId = keyMaterial.aciRegistrationId
|
||||
this.pniRegistrationId = keyMaterial.pniRegistrationId
|
||||
this.unidentifiedAccessKey = keyMaterial.unidentifiedAccessKey.toByteString()
|
||||
this.profileKey = keyMaterial.profileKey.toByteString()
|
||||
this.servicePassword = keyMaterial.servicePassword
|
||||
this.accountEntropyPool = keyMaterial.accountEntropyPool.value
|
||||
}
|
||||
|
||||
val fcmToken = networkController.getFcmToken()
|
||||
|
||||
storageController.updateInProgressRegistrationData {
|
||||
updateAccountData {
|
||||
this.fetchesMessages = fcmToken == null
|
||||
}
|
||||
|
||||
@@ -628,11 +633,13 @@ class RegistrationRepository(val context: Context, val networkController: Networ
|
||||
|
||||
if (result is RequestResult.Success) {
|
||||
storageController.updateInProgressRegistrationData {
|
||||
this.accountEntropyPool = keyMaterial.accountEntropyPool.value
|
||||
}
|
||||
updateAccountData {
|
||||
this.e164 = result.result.e164
|
||||
this.aci = result.result.aci
|
||||
this.pni = result.result.pni
|
||||
this.servicePassword = keyMaterial.servicePassword
|
||||
this.accountEntropyPool = keyMaterial.accountEntropyPool.value
|
||||
}
|
||||
storageController.commitRegistrationData()
|
||||
}
|
||||
@@ -727,7 +734,7 @@ class RegistrationRepository(val context: Context, val networkController: Networ
|
||||
* upcoming registration reuses the device's existing identity rather than generating a fresh one.
|
||||
*/
|
||||
suspend fun persistRestoredIdentityKeys(restoredAciIdentityKey: IdentityKeyPair?, restoredPniIdentityKey: IdentityKeyPair?) {
|
||||
storageController.updateInProgressRegistrationData {
|
||||
updateAccountData {
|
||||
aciIdentityKeyPair = restoredAciIdentityKey?.serialize()?.toByteString() ?: aciIdentityKeyPair
|
||||
pniIdentityKeyPair = restoredPniIdentityKey?.serialize()?.toByteString() ?: pniIdentityKeyPair
|
||||
}
|
||||
@@ -776,7 +783,7 @@ class RegistrationRepository(val context: Context, val networkController: Networ
|
||||
val persisted = json.decodeFromString(PersistedFlowState.serializer(), data.flowStateJson)
|
||||
|
||||
val aep = data.accountEntropyPool.takeIf { it.isNotEmpty() }?.let { AccountEntropyPool(it) }
|
||||
val masterKey = data.temporaryMasterKey.takeIf { it.size > 0 }?.let { MasterKey(it.toByteArray()) }
|
||||
val masterKey = data.masterKeyForInitialDataRestore.takeIf { it.size > 0 }?.let { MasterKey(it.toByteArray()) }
|
||||
val preExisting = storageController.getPreExistingRegistrationData()
|
||||
|
||||
persisted.toRegistrationFlowState(
|
||||
@@ -851,8 +858,8 @@ class RegistrationRepository(val context: Context, val networkController: Networ
|
||||
* (i.e. both ACI and PNI have been saved).
|
||||
*/
|
||||
suspend fun isRegistered(): Boolean = withContext(Dispatchers.IO) {
|
||||
val data = storageController.readInProgressRegistrationData()
|
||||
data.aci.isNotEmpty() && data.pni.isNotEmpty()
|
||||
val accountData = storageController.readInProgressRegistrationData().accountData
|
||||
accountData != null && accountData.aci.isNotEmpty() && accountData.pni.isNotEmpty()
|
||||
}
|
||||
|
||||
fun restoreV1Backup(rootUri: Uri, backupUri: Uri, passphrase: String): Flow<LocalBackupRestoreProgress> {
|
||||
@@ -896,6 +903,16 @@ class RegistrationRepository(val context: Context, val networkController: Networ
|
||||
networkController.enqueueSvrGuessResetJobIfPossible()
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies [updater] to the one-time [AccountData] within the in-progress registration data. Only the registration
|
||||
* process itself should write account data -- it is frozen once committed.
|
||||
*/
|
||||
private suspend fun updateAccountData(updater: AccountData.Builder.() -> Unit) {
|
||||
storageController.updateInProgressRegistrationData {
|
||||
accountData = (accountData ?: AccountData()).newBuilder().apply(updater).build()
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateKeyMaterial(
|
||||
existingAccountEntropyPool: AccountEntropyPool? = null,
|
||||
existingAciIdentityKeyPair: IdentityKeyPair? = null,
|
||||
|
||||
@@ -72,6 +72,9 @@ interface StorageController {
|
||||
* Reads the persisted [RegistrationData] (that is currently in the process of being worked on),
|
||||
* applies the [updater] to its builder, and writes the result back to persistent storage.
|
||||
*
|
||||
* Note that [RegistrationData.accountData] must never be modified once [RegistrationData.accountDataCommitted] is
|
||||
* true -- it describes the account that was registered, and [commitRegistrationData] will not apply it again.
|
||||
*
|
||||
* Example usage:
|
||||
* ```
|
||||
* storageController.updateRegistrationData {
|
||||
@@ -87,6 +90,10 @@ interface StorageController {
|
||||
* for the currently-registered account. Commits can happen multiple times. For instance, we will commit data right after
|
||||
* successfully registering, but then there may be more operations we perform after registration that need to be
|
||||
* separately committed.
|
||||
*
|
||||
* The one-time [RegistrationData.accountData] is applied exactly once, on the first commit where it is complete;
|
||||
* it is frozen from then on (tracked via [RegistrationData.accountDataCommitted]). All other fields are mutable
|
||||
* state that is (re-)applied on every commit.
|
||||
*/
|
||||
suspend fun commitRegistrationData()
|
||||
|
||||
|
||||
@@ -10,6 +10,23 @@ package signal;
|
||||
option java_package = "org.signal.registration.proto";
|
||||
|
||||
message RegistrationData {
|
||||
// Should only be persisted in the app-layer a single time during registration
|
||||
AccountData accountData = 1;
|
||||
bool accountDataCommitted = 2;
|
||||
|
||||
string accountEntropyPool = 3;
|
||||
string pin = 4;
|
||||
bool pinOptedOut = 5;
|
||||
bool registrationLockEnabled = 6;
|
||||
bytes profileKey = 7;
|
||||
bytes masterKeyForInitialDataRestore = 8;
|
||||
repeated SvrCredential svrCredentials = 9;
|
||||
ProvisioningData provisioningData = 10;
|
||||
string flowStateJson = 11;
|
||||
int64 lastUpdatedMillis = 12;
|
||||
}
|
||||
|
||||
message AccountData {
|
||||
bytes aciIdentityKeyPair = 1;
|
||||
bytes pniIdentityKeyPair = 2;
|
||||
bytes aciSignedPreKey = 3;
|
||||
@@ -23,27 +40,11 @@ message RegistrationData {
|
||||
string pni = 10;
|
||||
string e164 = 11;
|
||||
string servicePassword = 12;
|
||||
string accountEntropyPool = 13;
|
||||
|
||||
string pin = 14;
|
||||
bool pinOptedOut = 15;
|
||||
bool registrationLockEnabled = 16;
|
||||
bytes unidentifiedAccessKey = 13;
|
||||
bool fetchesMessages = 14;
|
||||
|
||||
bytes profileKey = 17;
|
||||
bytes unidentifiedAccessKey = 18;
|
||||
bytes temporaryMasterKey = 19;
|
||||
|
||||
repeated SvrCredential svrCredentials = 20;
|
||||
ProvisioningData provisioningData = 21;
|
||||
LinkedDeviceData linkedDeviceData = 22;
|
||||
bool fetchesMessages = 23;
|
||||
|
||||
string flowStateJson = 24;
|
||||
|
||||
// Epoch milliseconds of the last write to this data. Used to detect data that has grown too stale to trust (e.g. an
|
||||
// expired verification session). Stamped on every write.
|
||||
int64 lastUpdatedMillis = 25;
|
||||
// Next: 26
|
||||
LinkedDeviceData linkedDeviceData = 15;
|
||||
}
|
||||
|
||||
message SvrCredential {
|
||||
|
||||
+15
-15
@@ -120,9 +120,9 @@ class RegistrationEndToEndTest {
|
||||
|
||||
val committed = storageController.committedData
|
||||
assert(committed != null) { "Expected registration data to be committed" }
|
||||
assert(committed!!.e164 == E164) { "Expected committed e164 $E164 but was ${committed.e164}" }
|
||||
assert(committed.aci.isNotEmpty()) { "Expected committed ACI to be populated" }
|
||||
assert(committed.pni.isNotEmpty()) { "Expected committed PNI to be populated" }
|
||||
assert(committed!!.accountData?.e164 == E164) { "Expected committed e164 $E164 but was ${committed.accountData?.e164}" }
|
||||
assert(committed.accountData?.aci?.isNotEmpty() == true) { "Expected committed ACI to be populated" }
|
||||
assert(committed.accountData?.pni?.isNotEmpty() == true) { "Expected committed PNI to be populated" }
|
||||
assert(committed.pin == PIN) { "Expected committed pin $PIN but was ${committed.pin}" }
|
||||
assert(committed.accountEntropyPool.isNotEmpty()) { "Expected committed AEP to be populated" }
|
||||
|
||||
@@ -179,8 +179,8 @@ class RegistrationEndToEndTest {
|
||||
|
||||
val committed = storageController.committedData
|
||||
assert(committed != null) { "Expected registration data to be committed" }
|
||||
assert(committed!!.e164 == E164) { "Expected committed e164 $E164 but was ${committed.e164}" }
|
||||
assert(committed.aci.isNotEmpty()) { "Expected committed ACI to be populated" }
|
||||
assert(committed!!.accountData?.e164 == E164) { "Expected committed e164 $E164 but was ${committed.accountData?.e164}" }
|
||||
assert(committed.accountData?.aci?.isNotEmpty() == true) { "Expected committed ACI to be populated" }
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -207,7 +207,7 @@ class RegistrationEndToEndTest {
|
||||
|
||||
val committed = storageController.committedData
|
||||
assert(committed != null) { "Expected registration data to be committed" }
|
||||
assert(committed!!.e164 == E164) { "Expected committed e164 $E164 but was ${committed.e164}" }
|
||||
assert(committed!!.accountData?.e164 == E164) { "Expected committed e164 $E164 but was ${committed.accountData?.e164}" }
|
||||
assert(committed.pin == PIN) { "Expected committed pin $PIN but was ${committed.pin}" }
|
||||
}
|
||||
|
||||
@@ -263,7 +263,7 @@ class RegistrationEndToEndTest {
|
||||
|
||||
val committed = storageController.committedData
|
||||
assert(committed != null) { "Expected registration data to be committed" }
|
||||
assert(committed!!.e164 == E164) { "Expected committed e164 $E164 but was ${committed.e164}" }
|
||||
assert(committed!!.accountData?.e164 == E164) { "Expected committed e164 $E164 but was ${committed.accountData?.e164}" }
|
||||
assert(committed.accountEntropyPool == aep.value) { "Expected the committed AEP to be the one the user entered" }
|
||||
assert(committed.pin == PIN) { "Expected the pin from the restored backup but was ${committed.pin}" }
|
||||
assert(storageController.restoreDecision == RestoreDecision.COMPLETED) { "Expected COMPLETED restore decision but was ${storageController.restoreDecision}" }
|
||||
@@ -292,7 +292,7 @@ class RegistrationEndToEndTest {
|
||||
|
||||
val committed = storageController.committedData
|
||||
assert(committed != null) { "Expected registration data to be committed" }
|
||||
assert(committed!!.e164 == E164) { "Expected committed e164 $E164 but was ${committed.e164}" }
|
||||
assert(committed!!.accountData?.e164 == E164) { "Expected committed e164 $E164 but was ${committed.accountData?.e164}" }
|
||||
assert(committed.accountEntropyPool == aep.value) { "Expected the committed AEP to be the one from the restored backup" }
|
||||
assert(committed.pin == PIN) { "Expected committed pin $PIN but was ${committed.pin}" }
|
||||
}
|
||||
@@ -356,7 +356,7 @@ class RegistrationEndToEndTest {
|
||||
|
||||
val committed = storageController.committedData
|
||||
assert(committed != null) { "Expected registration data to be committed" }
|
||||
assert(committed!!.e164 == E164) { "Expected committed e164 $E164 but was ${committed.e164}" }
|
||||
assert(committed!!.accountData?.e164 == E164) { "Expected committed e164 $E164 but was ${committed.accountData?.e164}" }
|
||||
assert(committed.pin == PIN) { "Expected committed pin $PIN but was ${committed.pin}" }
|
||||
assert(networkController.lastSetPinRequest?.pin == PIN) { "Expected pin $PIN on SVR but was ${networkController.lastSetPinRequest?.pin}" }
|
||||
}
|
||||
@@ -417,7 +417,7 @@ class RegistrationEndToEndTest {
|
||||
|
||||
val committed = storageController.committedData
|
||||
assert(committed != null) { "Expected registration data to be committed" }
|
||||
assert(committed!!.e164 == E164) { "Expected committed e164 $E164 but was ${committed.e164}" }
|
||||
assert(committed!!.accountData?.e164 == E164) { "Expected committed e164 $E164 but was ${committed.accountData?.e164}" }
|
||||
assert(committed.pin == PIN) { "Expected committed pin $PIN but was ${committed.pin}" }
|
||||
assert(networkController.lastSetPinRequest?.pin == PIN) { "Expected pin $PIN on SVR but was ${networkController.lastSetPinRequest?.pin}" }
|
||||
}
|
||||
@@ -503,7 +503,7 @@ class RegistrationEndToEndTest {
|
||||
|
||||
val committed = storageController.committedData
|
||||
assert(committed != null) { "Expected registration data to be committed" }
|
||||
assert(committed!!.e164 == E164) { "Expected committed e164 $E164 but was ${committed.e164}" }
|
||||
assert(committed!!.accountData?.e164 == E164) { "Expected committed e164 $E164 but was ${committed.accountData?.e164}" }
|
||||
assert(committed.pin == PIN) { "Expected committed pin $PIN but was ${committed.pin}" }
|
||||
}
|
||||
|
||||
@@ -538,7 +538,7 @@ class RegistrationEndToEndTest {
|
||||
|
||||
val committed = storageController.committedData
|
||||
assert(committed != null) { "Expected registration data to be committed" }
|
||||
assert(committed!!.e164 == E164) { "Expected committed e164 $E164 but was ${committed.e164}" }
|
||||
assert(committed!!.accountData?.e164 == E164) { "Expected committed e164 $E164 but was ${committed.accountData?.e164}" }
|
||||
assert(committed.accountEntropyPool == aep.value) { "Expected the committed AEP to be the provisioned one" }
|
||||
assert(committed.pin == PIN) { "Expected the provisioned pin $PIN but was ${committed.pin}" }
|
||||
assert(storageController.restoreDecision == RestoreDecision.COMPLETED) { "Expected COMPLETED restore decision but was ${storageController.restoreDecision}" }
|
||||
@@ -576,7 +576,7 @@ class RegistrationEndToEndTest {
|
||||
|
||||
val committed = storageController.committedData
|
||||
assert(committed != null) { "Expected registration data to be committed" }
|
||||
assert(committed!!.e164 == E164) { "Expected committed e164 $E164 but was ${committed.e164}" }
|
||||
assert(committed!!.accountData?.e164 == E164) { "Expected committed e164 $E164 but was ${committed.accountData?.e164}" }
|
||||
assert(committed.accountEntropyPool == aep.value) { "Expected the committed AEP to be the provisioned one" }
|
||||
assert(committed.pin == PIN) { "Expected committed pin $PIN but was ${committed.pin}" }
|
||||
}
|
||||
@@ -610,7 +610,7 @@ class RegistrationEndToEndTest {
|
||||
|
||||
val committed = storageController.committedData
|
||||
assert(committed != null) { "Expected registration data to be committed" }
|
||||
assert(committed!!.e164 == E164) { "Expected committed e164 $E164 but was ${committed.e164}" }
|
||||
assert(committed!!.accountData?.e164 == E164) { "Expected committed e164 $E164 but was ${committed.accountData?.e164}" }
|
||||
assert(committed.pin == PIN) { "Expected committed pin $PIN but was ${committed.pin}" }
|
||||
}
|
||||
|
||||
@@ -637,7 +637,7 @@ class RegistrationEndToEndTest {
|
||||
|
||||
val committed = storageController.committedData
|
||||
assert(committed != null) { "Expected registration data to be committed" }
|
||||
assert(committed!!.e164 == E164) { "Expected committed e164 $E164 but was ${committed.e164}" }
|
||||
assert(committed!!.accountData?.e164 == E164) { "Expected committed e164 $E164 but was ${committed.accountData?.e164}" }
|
||||
assert(committed.pin == PIN) { "Expected the provisioned pin $PIN but was ${committed.pin}" }
|
||||
assert(storageController.restoreDecision == RestoreDecision.SKIPPED) { "Expected SKIPPED restore decision but was ${storageController.restoreDecision}" }
|
||||
}
|
||||
|
||||
+5
@@ -83,6 +83,11 @@ class FakeStorageController : StorageController {
|
||||
}
|
||||
|
||||
override suspend fun commitRegistrationData() {
|
||||
val accountData = inProgressData.accountData
|
||||
val accountDataComplete = accountData != null && accountData.e164.isNotEmpty() && accountData.aci.isNotEmpty() && accountData.pni.isNotEmpty() && accountData.servicePassword.isNotEmpty()
|
||||
if (!inProgressData.accountDataCommitted && accountDataComplete) {
|
||||
inProgressData = inProgressData.newBuilder().accountDataCommitted(true).build()
|
||||
}
|
||||
committedData = inProgressData
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user