mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-07-21 13:05:00 +01:00
Ensure backups are re-enabled post-reg in regV5.
This commit is contained in:
+54
-5
@@ -6,6 +6,7 @@
|
||||
package org.thoughtcrime.securesms.registration.v2
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
@@ -43,10 +44,12 @@ import org.signal.registration.screens.localbackuprestore.LocalBackupInfo
|
||||
import org.signal.registration.screens.messagesync.LinkAndSyncProgress
|
||||
import org.signal.registration.screens.remotebackuprestore.RemoteBackupRestoreProgress
|
||||
import org.thoughtcrime.securesms.backup.BackupEvent
|
||||
import org.thoughtcrime.securesms.backup.BackupPassphrase
|
||||
import org.thoughtcrime.securesms.backup.FullBackupImporter
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.RemoteRestoreResult
|
||||
import org.thoughtcrime.securesms.backup.v2.RestoreV2Event
|
||||
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
|
||||
@@ -68,6 +71,8 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
|
||||
import org.thoughtcrime.securesms.registration.util.RegistrationUtil
|
||||
import org.thoughtcrime.securesms.service.LocalBackupListener
|
||||
import org.thoughtcrime.securesms.util.BackupUtil
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.api.link.TransferArchiveResponse
|
||||
import java.io.File
|
||||
@@ -290,14 +295,14 @@ class AppRegistrationStorageController(private val context: Context) : StorageCo
|
||||
}
|
||||
}
|
||||
|
||||
override fun restoreLocalBackupV1(uri: Uri, passphrase: String): Flow<LocalBackupRestoreProgress> = callbackFlow {
|
||||
Log.d(TAG, "Starting V1 local backup restore from: $uri")
|
||||
override fun restoreLocalBackupV1(rootUri: Uri, backupUri: Uri, passphrase: String): Flow<LocalBackupRestoreProgress> = callbackFlow {
|
||||
Log.d(TAG, "Starting V1 local backup restore from: $backupUri")
|
||||
|
||||
trySend(LocalBackupRestoreProgress.Preparing)
|
||||
|
||||
// The importer only reports a running frame count with no total, so we track bytes read from the backup file against
|
||||
// its size to produce a real progress fraction, sampling the counting stream on each frame-progress event.
|
||||
val totalBytes = context.contentResolver.getLength(uri) ?: 0L
|
||||
val totalBytes = context.contentResolver.getLength(backupUri) ?: 0L
|
||||
var countingStream: CountingInputStream? = null
|
||||
|
||||
val subscriber = object {
|
||||
@@ -313,14 +318,14 @@ class AppRegistrationStorageController(private val context: Context) : StorageCo
|
||||
|
||||
launch(Dispatchers.IO) {
|
||||
try {
|
||||
if (!FullBackupImporter.validatePassphrase(context, uri, passphrase)) {
|
||||
if (!FullBackupImporter.validatePassphrase(context, backupUri, passphrase)) {
|
||||
Log.w(TAG, "V1 restore failed: incorrect passphrase")
|
||||
trySend(LocalBackupRestoreProgress.IncorrectCredential)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val database = SignalDatabase.backupDatabase
|
||||
val inputStream = context.contentResolver.openInputStream(uri) ?: throw IOException("Unable to open backup stream for $uri")
|
||||
val inputStream = context.contentResolver.openInputStream(backupUri) ?: throw IOException("Unable to open backup stream for $backupUri")
|
||||
CountingInputStream(inputStream).use { counting ->
|
||||
countingStream = counting
|
||||
FullBackupImporter.importFile(
|
||||
@@ -335,6 +340,12 @@ class AppRegistrationStorageController(private val context: Context) : StorageCo
|
||||
|
||||
SignalDatabase.runPostBackupRestoreTasks(database)
|
||||
|
||||
// The importer writes the restored key-value store straight to disk, bypassing the in-memory SignalStore cache.
|
||||
// Reset it so the state we read below reflects the restored values rather than stale pre-restore ones.
|
||||
SignalStore.onPostBackupRestore()
|
||||
|
||||
reenableLegacyLocalBackups(rootUri, passphrase)
|
||||
|
||||
trySend(readRestoredLocalBackupState(includePreRegistrationKeys = true))
|
||||
Log.d(TAG, "V1 restore complete.")
|
||||
} catch (e: FullBackupImporter.DatabaseDowngradeException) {
|
||||
@@ -397,6 +408,20 @@ class AppRegistrationStorageController(private val context: Context) : StorageCo
|
||||
Log.i(TAG, "V2 local backup belongs to current account; adopting entered recovery key.")
|
||||
SignalStore.account.restoreAccountEntropyPool(aep)
|
||||
updateInProgressRegistrationData { this.accountEntropyPool = aep.value }
|
||||
|
||||
// Re-enable new-style local backups pointing at the restored location, so the user keeps getting backups.
|
||||
// Skip it if the folder is the SignalBackups directory itself, since it can't be reused as a destination.
|
||||
val archiveFileSystem = ArchiveFileSystem.openForRestore(context, rootUri)
|
||||
if (archiveFileSystem != null && !archiveFileSystem.isRootedAtSignalBackups) {
|
||||
val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
context.contentResolver.takePersistableUriPermission(rootUri, takeFlags)
|
||||
SignalStore.backup.newLocalBackupsDirectory = rootUri.toString()
|
||||
SignalStore.backup.newLocalBackupsEnabled = true
|
||||
LocalBackupListener.setNextBackupTimeToIntervalFromNow(context)
|
||||
LocalBackupListener.schedule(context)
|
||||
} else {
|
||||
Log.w(TAG, "V2 local backup directory can't be reused as a destination; not re-enabling local backups.")
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "V2 local backup does not belong to current account; keeping existing recovery key.")
|
||||
}
|
||||
@@ -414,6 +439,30 @@ class AppRegistrationStorageController(private val context: Context) : StorageCo
|
||||
}
|
||||
}.flowOn(Dispatchers.IO)
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
+3
-3
@@ -298,10 +298,10 @@ class DemoStorageController(private val context: Context) : StorageController {
|
||||
backups.sortedByDescending { it.date }
|
||||
}
|
||||
|
||||
override fun restoreLocalBackupV1(uri: Uri, passphrase: String): Flow<LocalBackupRestoreProgress> = flow {
|
||||
Log.d(TAG, "Starting simulated V1 local backup restore from: $uri")
|
||||
override fun restoreLocalBackupV1(rootUri: Uri, backupUri: Uri, passphrase: String): Flow<LocalBackupRestoreProgress> = flow {
|
||||
Log.d(TAG, "Starting simulated V1 local backup restore from: $backupUri")
|
||||
|
||||
require(DocumentFile.fromSingleUri(context, uri)?.exists() == true) { "Backup file does not exist: $uri" }
|
||||
require(DocumentFile.fromSingleUri(context, backupUri)?.exists() == true) { "Backup file does not exist: $backupUri" }
|
||||
|
||||
emit(LocalBackupRestoreProgress.Preparing)
|
||||
delay(SIMULATED_STAGE_DELAY_MS)
|
||||
|
||||
+2
-2
@@ -847,8 +847,8 @@ class RegistrationRepository(val context: Context, val networkController: Networ
|
||||
data.aci.isNotEmpty() && data.pni.isNotEmpty()
|
||||
}
|
||||
|
||||
fun restoreV1Backup(uri: Uri, passphrase: String): Flow<LocalBackupRestoreProgress> {
|
||||
return storageController.restoreLocalBackupV1(uri, passphrase)
|
||||
fun restoreV1Backup(rootUri: Uri, backupUri: Uri, passphrase: String): Flow<LocalBackupRestoreProgress> {
|
||||
return storageController.restoreLocalBackupV1(rootUri, backupUri, passphrase)
|
||||
}
|
||||
|
||||
fun restoreV2Backup(rootUri: Uri, backupUri: Uri, aep: AccountEntropyPool): Flow<LocalBackupRestoreProgress> {
|
||||
|
||||
@@ -97,12 +97,15 @@ interface StorageController {
|
||||
suspend fun setRestoreDecision(decision: RestoreDecision)
|
||||
|
||||
/**
|
||||
* Begins restoring from a V1 (.backup) file identified by the given [uri].
|
||||
* Begins restoring from a V1 (.backup) file identified by the given [backupUri].
|
||||
*
|
||||
* Returns a [Flow] of [LocalBackupRestoreProgress] that reports the state of the restore operation
|
||||
* from preparation through completion or error.
|
||||
* @param rootUri The backup directory that contains the [backupUri] file. Persisted as the backup directory so
|
||||
* local backups can be re-enabled after the restore.
|
||||
* @param backupUri The specific .backup file to restore from.
|
||||
* @return A [Flow] of [LocalBackupRestoreProgress] that reports the state of the restore operation
|
||||
* from preparation through completion or error.
|
||||
*/
|
||||
fun restoreLocalBackupV1(uri: Uri, passphrase: String): Flow<LocalBackupRestoreProgress>
|
||||
fun restoreLocalBackupV1(rootUri: Uri, backupUri: Uri, passphrase: String): Flow<LocalBackupRestoreProgress>
|
||||
|
||||
/**
|
||||
* Begins restoring from a V2 (folder-based) backup.
|
||||
|
||||
+1
-1
@@ -238,7 +238,7 @@ private fun PassphraseTextField(
|
||||
onValueChange = { newValue ->
|
||||
onPassphraseChange(newValue.filter { it.isDigit() })
|
||||
},
|
||||
label = { Text(stringResource(R.string.LocalBackupRestoreScreen__recovery_key)) },
|
||||
label = { Text(stringResource(R.string.LocalBackupRestoreScreen__passphrase)) },
|
||||
textStyle = MaterialTheme.typography.bodyLarge.copy(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
lineHeight = 36.sp
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ import org.signal.core.models.AccountEntropyPool
|
||||
* to the phone number entry screen via the result bus.
|
||||
*/
|
||||
sealed interface LocalBackupRestoreResult {
|
||||
/** The restore completed successfully. Contains the AEP if V2 backup, null if V1. */
|
||||
/** The restore completed successfully. Contains the AEP if V2 backup, or the restored AEP for V1. */
|
||||
data class Success(val aep: AccountEntropyPool?) : LocalBackupRestoreResult
|
||||
|
||||
/** The user canceled the restore flow. The pending restore option should be cleared. */
|
||||
|
||||
+1
-1
@@ -188,7 +188,7 @@ class LocalBackupRestoreViewModel(
|
||||
restoreJob = viewModelScope.launch {
|
||||
val currentState = _state.value
|
||||
val restoreFlow = when (backup.type) {
|
||||
LocalBackupInfo.BackupType.V1 -> repository.restoreV1Backup(backup.uri, passphrase = credential)
|
||||
LocalBackupInfo.BackupType.V1 -> repository.restoreV1Backup(rootUri = rootUri!!, backupUri = backup.uri, passphrase = credential)
|
||||
LocalBackupInfo.BackupType.V2 -> repository.restoreV2Backup(rootUri = rootUri!!, backupUri = backup.uri, aep = aep!!)
|
||||
}
|
||||
restoreFlow.collect { progress ->
|
||||
|
||||
@@ -203,6 +203,8 @@
|
||||
<string name="LocalBackupRestoreScreen__enter_the_30_digit_passphrase">Your local backup passphrase is a 30-digit code required to recover your account and data.</string>
|
||||
<!-- Label for the recovery key text field -->
|
||||
<string name="LocalBackupRestoreScreen__recovery_key">Recovery key</string>
|
||||
<!-- Label for the backup passphrase text field -->
|
||||
<string name="LocalBackupRestoreScreen__passphrase">Passphrase</string>
|
||||
<!-- Error text when passphrase is too long -->
|
||||
<string name="LocalBackupRestoreScreen__too_long">Too long (%1$d/%2$d)</string>
|
||||
<!-- Link for users who don\'t have their passphrase -->
|
||||
|
||||
+14
-14
@@ -135,7 +135,7 @@ class LocalBackupRestoreViewModelTest {
|
||||
name = "backup.backup",
|
||||
uri = mockk()
|
||||
)
|
||||
val initialState = LocalBackupRestoreState(backupInfo = backupInfo)
|
||||
val initialState = LocalBackupRestoreState(backupInfo = backupInfo, selectedFolderUri = mockk())
|
||||
|
||||
viewModel.applyEvent(initialState, LocalBackupRestoreEvents.RestoreBackup, stateEmitter)
|
||||
|
||||
@@ -157,7 +157,7 @@ class LocalBackupRestoreViewModelTest {
|
||||
name = "backup.bin",
|
||||
uri = mockk()
|
||||
)
|
||||
val initialState = LocalBackupRestoreState(backupInfo = backupInfo)
|
||||
val initialState = LocalBackupRestoreState(backupInfo = backupInfo, selectedFolderUri = mockk())
|
||||
|
||||
viewModel.applyEvent(initialState, LocalBackupRestoreEvents.RestoreBackup, stateEmitter)
|
||||
|
||||
@@ -279,9 +279,9 @@ class LocalBackupRestoreViewModelTest {
|
||||
name = "backup.backup",
|
||||
uri = mockk()
|
||||
)
|
||||
val initialState = LocalBackupRestoreState(backupInfo = backupInfo)
|
||||
val initialState = LocalBackupRestoreState(backupInfo = backupInfo, selectedFolderUri = mockk())
|
||||
|
||||
every { mockRepository.restoreV1Backup(any(), any()) } returns flowOf(LocalBackupRestoreProgress.Complete(restoredSvrPin = "1234", restoredProfileKey = null))
|
||||
every { mockRepository.restoreV1Backup(any(), any(), any()) } returns flowOf(LocalBackupRestoreProgress.Complete(restoredSvrPin = "1234", restoredProfileKey = null))
|
||||
|
||||
viewModel.applyEvent(initialState, LocalBackupRestoreEvents.PassphraseSubmitted("passphrase"), stateEmitter)
|
||||
|
||||
@@ -300,9 +300,9 @@ class LocalBackupRestoreViewModelTest {
|
||||
name = "backup.backup",
|
||||
uri = mockk()
|
||||
)
|
||||
val initialState = LocalBackupRestoreState(backupInfo = backupInfo)
|
||||
val initialState = LocalBackupRestoreState(backupInfo = backupInfo, selectedFolderUri = mockk())
|
||||
|
||||
every { mockRepository.restoreV1Backup(any(), any()) } returns flowOf(LocalBackupRestoreProgress.Complete(restoredSvrPin = null, restoredProfileKey = null))
|
||||
every { mockRepository.restoreV1Backup(any(), any(), any()) } returns flowOf(LocalBackupRestoreProgress.Complete(restoredSvrPin = null, restoredProfileKey = null))
|
||||
|
||||
viewModel.applyEvent(initialState, LocalBackupRestoreEvents.PassphraseSubmitted("passphrase"), stateEmitter)
|
||||
|
||||
@@ -324,9 +324,9 @@ class LocalBackupRestoreViewModelTest {
|
||||
name = "backup.backup",
|
||||
uri = mockk()
|
||||
)
|
||||
val initialState = LocalBackupRestoreState(backupInfo = backupInfo)
|
||||
val initialState = LocalBackupRestoreState(backupInfo = backupInfo, selectedFolderUri = mockk())
|
||||
|
||||
every { mockRepository.restoreV1Backup(any(), any()) } returns flowOf(LocalBackupRestoreProgress.Complete(restoredSvrPin = null, restoredProfileKey = null))
|
||||
every { mockRepository.restoreV1Backup(any(), any(), any()) } returns flowOf(LocalBackupRestoreProgress.Complete(restoredSvrPin = null, restoredProfileKey = null))
|
||||
|
||||
viewModel.applyEvent(initialState, LocalBackupRestoreEvents.PassphraseSubmitted("passphrase"), stateEmitter)
|
||||
|
||||
@@ -352,9 +352,9 @@ class LocalBackupRestoreViewModelTest {
|
||||
name = "backup.backup",
|
||||
uri = mockk()
|
||||
)
|
||||
val initialState = LocalBackupRestoreState(backupInfo = backupInfo)
|
||||
val initialState = LocalBackupRestoreState(backupInfo = backupInfo, selectedFolderUri = mockk())
|
||||
|
||||
every { mockRepository.restoreV1Backup(any(), any()) } returns flowOf(LocalBackupRestoreProgress.IncorrectCredential)
|
||||
every { mockRepository.restoreV1Backup(any(), any(), any()) } returns flowOf(LocalBackupRestoreProgress.IncorrectCredential)
|
||||
|
||||
viewModel.applyEvent(initialState, LocalBackupRestoreEvents.PassphraseSubmitted("passphrase"), stateEmitter)
|
||||
|
||||
@@ -394,13 +394,13 @@ class LocalBackupRestoreViewModelTest {
|
||||
name = "backup.backup",
|
||||
uri = mockk()
|
||||
)
|
||||
val initialState = LocalBackupRestoreState(backupInfo = backupInfo)
|
||||
val initialState = LocalBackupRestoreState(backupInfo = backupInfo, selectedFolderUri = mockk())
|
||||
|
||||
val profileKey = ProfileKey(ByteArray(32))
|
||||
val aciIdentityKey = IdentityKeyPair.generate()
|
||||
val pniIdentityKey = IdentityKeyPair.generate()
|
||||
|
||||
every { mockRepository.restoreV1Backup(any(), any()) } returns flowOf(
|
||||
every { mockRepository.restoreV1Backup(any(), any(), any()) } returns flowOf(
|
||||
LocalBackupRestoreProgress.Complete(
|
||||
restoredSvrPin = "1234",
|
||||
restoredProfileKey = profileKey,
|
||||
@@ -424,11 +424,11 @@ class LocalBackupRestoreViewModelTest {
|
||||
name = "backup.backup",
|
||||
uri = mockk()
|
||||
)
|
||||
val initialState = LocalBackupRestoreState(backupInfo = backupInfo)
|
||||
val initialState = LocalBackupRestoreState(backupInfo = backupInfo, selectedFolderUri = mockk())
|
||||
|
||||
val restoredAep = AccountEntropyPool(VALID_AEP)
|
||||
|
||||
every { mockRepository.restoreV1Backup(any(), any()) } returns flowOf(
|
||||
every { mockRepository.restoreV1Backup(any(), any(), any()) } returns flowOf(
|
||||
LocalBackupRestoreProgress.Complete(
|
||||
restoredSvrPin = null,
|
||||
restoredProfileKey = null,
|
||||
|
||||
Reference in New Issue
Block a user