From e71ec7c6d9b8e94d6d64e3f16dcc52e7b9a2e2e5 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Tue, 14 Jul 2026 11:05:44 -0400 Subject: [PATCH] Refactor regV5 account storage persistence. --- .../v2/AppRegistrationStorageController.kt | 399 ++++++++++++------ .../dependencies/DemoStorageController.kt | 62 +-- .../registration/RegistrationRepository.kt | 49 ++- .../signal/registration/StorageController.kt | 7 + .../src/main/protowire/Registration.proto | 39 +- .../registration/RegistrationEndToEndTest.kt | 30 +- .../fakes/FakeStorageController.kt | 5 + 7 files changed, 376 insertions(+), 215 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/AppRegistrationStorageController.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/AppRegistrationStorageController.kt index 71ede7af2d..c8d9ad5b1d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/AppRegistrationStorageController.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/AppRegistrationStorageController.kt @@ -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 = 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 = 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 + ) + } } diff --git a/demo/registration/src/main/java/org/signal/registration/sample/dependencies/DemoStorageController.kt b/demo/registration/src/main/java/org/signal/registration/sample/dependencies/DemoStorageController.kt index be1982b03a..40fa0f25ba 100644 --- a/demo/registration/src/main/java/org/signal/registration/sample/dependencies/DemoStorageController.kt +++ b/demo/registration/src/main/java/org/signal/registration/sample/dependencies/DemoStorageController.kt @@ -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 diff --git a/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt b/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt index 4b2b8227c0..adb6671fce 100644 --- a/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt +++ b/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt @@ -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 { @@ -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, diff --git a/feature/registration/src/main/java/org/signal/registration/StorageController.kt b/feature/registration/src/main/java/org/signal/registration/StorageController.kt index 502694e519..17d2ed58f3 100644 --- a/feature/registration/src/main/java/org/signal/registration/StorageController.kt +++ b/feature/registration/src/main/java/org/signal/registration/StorageController.kt @@ -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() diff --git a/feature/registration/src/main/protowire/Registration.proto b/feature/registration/src/main/protowire/Registration.proto index 6b27b42ff9..cc25c0ed9b 100644 --- a/feature/registration/src/main/protowire/Registration.proto +++ b/feature/registration/src/main/protowire/Registration.proto @@ -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 { diff --git a/feature/registration/src/test/java/org/signal/registration/RegistrationEndToEndTest.kt b/feature/registration/src/test/java/org/signal/registration/RegistrationEndToEndTest.kt index 89a94fbe65..2c0b5f69b1 100644 --- a/feature/registration/src/test/java/org/signal/registration/RegistrationEndToEndTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/RegistrationEndToEndTest.kt @@ -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}" } } diff --git a/feature/registration/src/test/java/org/signal/registration/fakes/FakeStorageController.kt b/feature/registration/src/test/java/org/signal/registration/fakes/FakeStorageController.kt index d8e17493fe..16a4e6d41c 100644 --- a/feature/registration/src/test/java/org/signal/registration/fakes/FakeStorageController.kt +++ b/feature/registration/src/test/java/org/signal/registration/fakes/FakeStorageController.kt @@ -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 }