Refactor regV5 account storage persistence.

This commit is contained in:
Greyson Parrelli
2026-07-14 11:05:44 -04:00
parent 20d24d177d
commit e71ec7c6d9
7 changed files with 376 additions and 215 deletions
@@ -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
)
}
}
@@ -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
@@ -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 {
@@ -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}" }
}
@@ -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
}