From 20ea0b332d09f15b9784599306211f67547aadd3 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 18 Sep 2024 11:52:43 -0300 Subject: [PATCH] Delete repo and exit process when key changes There is no easy way to re-initialize all data based on the old key, so to prevent usage of the old key we need to exit our process. When the app is started again, only the new key will be used. --- .../java/com/stevesoltys/seedvault/App.kt | 4 ++- .../stevesoltys/seedvault/crypto/Crypto.kt | 26 ++++++++++++------- .../seedvault/repo/AppBackupManager.kt | 11 ++++++++ .../stevesoltys/seedvault/repo/RepoModule.kt | 1 + .../ConfigurableBackupTransportService.kt | 3 ++- .../ui/recoverycode/RecoveryCodeActivity.kt | 8 +++--- .../ui/recoverycode/RecoveryCodeViewModel.kt | 25 +++++++++++++++--- .../seedvault/worker/WorkerModule.kt | 4 +-- app/src/main/res/values/strings.xml | 4 +-- .../seedvault/repo/AppBackupManagerTest.kt | 14 ++++++++++ 10 files changed, 75 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/stevesoltys/seedvault/App.kt b/app/src/main/java/com/stevesoltys/seedvault/App.kt index 7fbfd358..0abc535b 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/App.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/App.kt @@ -94,7 +94,9 @@ open class App : Application() { backupStateManager = get(), ) } - viewModel { RecoveryCodeViewModel(this@App, get(), get(), get(), get(), get(), get()) } + viewModel { + RecoveryCodeViewModel(this@App, get(), get(), get(), get(), get(), get(), get()) + } viewModel { BackupStorageViewModel( app = this@App, diff --git a/app/src/main/java/com/stevesoltys/seedvault/crypto/Crypto.kt b/app/src/main/java/com/stevesoltys/seedvault/crypto/Crypto.kt index 7d69a2d9..cd905036 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/crypto/Crypto.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/crypto/Crypto.kt @@ -190,17 +190,23 @@ internal class CryptoImpl( secureRandom.nextBytes(this) } - override val repoId: String - get() { // TODO maybe cache this, but what if main key changes during run-time? - @SuppressLint("HardwareIds") - val androidId = Settings.Secure.getString(context.contentResolver, ANDROID_ID) - val repoIdKey = - deriveKey(keyManager.getMainKey(), "app backup repoId key".toByteArray()) - val hmacHasher: Mac = Mac.getInstance(ALGORITHM_HMAC).apply { - init(SecretKeySpec(repoIdKey, ALGORITHM_HMAC)) - } - return hmacHasher.doFinal(androidId.toByteArrayFromHex()).toHexString() + /** + * The ID of the backup repository tied to this user/device via [ANDROID_ID] + * and the current [KeyManager.getMainKey]. + * + * Attention: If the main key ever changes, we need to kill our process, + * so all lazy values that depend on that key or the [gearTableKey] get reinitialized. + */ + override val repoId: String by lazy { + @SuppressLint("HardwareIds") + val androidId = Settings.Secure.getString(context.contentResolver, ANDROID_ID) + val repoIdKey = + deriveKey(keyManager.getMainKey(), "app backup repoId key".toByteArray()) + val hmacHasher: Mac = Mac.getInstance(ALGORITHM_HMAC).apply { + init(SecretKeySpec(repoIdKey, ALGORITHM_HMAC)) } + hmacHasher.doFinal(androidId.toByteArrayFromHex()).toHexString() + } override val gearTableKey: ByteArray get() = deriveKey(keyManager.getMainKey(), "app backup gear table key".toByteArray()) diff --git a/app/src/main/java/com/stevesoltys/seedvault/repo/AppBackupManager.kt b/app/src/main/java/com/stevesoltys/seedvault/repo/AppBackupManager.kt index 1e7e0781..dc35b05f 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/repo/AppBackupManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/repo/AppBackupManager.kt @@ -112,6 +112,17 @@ internal class AppBackupManager( } } + /** + * Careful, this removes the entire backup repository from the backend + * and clears local blob cache. + */ + @WorkerThread + @Throws(IOException::class) + suspend fun removeBackupRepo() { + blobCache.clearLocalCache() + backendManager.backend.remove(TopLevelFolder(crypto.repoId)) + } + private suspend fun keepTrying(n: Int = 3, block: suspend () -> Unit) { for (i in 1..n) { try { diff --git a/app/src/main/java/com/stevesoltys/seedvault/repo/RepoModule.kt b/app/src/main/java/com/stevesoltys/seedvault/repo/RepoModule.kt index 7860bdcc..c05ae656 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/repo/RepoModule.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/repo/RepoModule.kt @@ -10,6 +10,7 @@ import org.koin.dsl.module import java.io.File val repoModule = module { + single { AppBackupManager(get(), get(), get(), get(), get(), get()) } single { BackupReceiver(get(), get(), get()) } single { BlobCache(androidContext()) } single { BlobCreator(get(), get()) } diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt index 7d010e45..4627bd72 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt @@ -11,6 +11,7 @@ import android.content.Intent import android.os.IBinder import android.util.Log import com.stevesoltys.seedvault.crypto.KeyManager +import com.stevesoltys.seedvault.permitDiskReads import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -45,7 +46,7 @@ class ConfigurableBackupTransportService : Service(), KoinComponent { override fun onBind(intent: Intent): IBinder? { // refuse to work until we have the main key - val noMainKey = keyManager.hasBackupKey() && !keyManager.hasMainKey() + val noMainKey = permitDiskReads { keyManager.hasBackupKey() && !keyManager.hasMainKey() } if (noMainKey && backupManager.currentTransport == TRANSPORT_ID) { notificationManager.onNoMainKeyError() backupManager.isBackupEnabled = false diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeActivity.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeActivity.kt index bc05595c..c4f34435 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeActivity.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeActivity.kt @@ -26,15 +26,15 @@ class RecoveryCodeActivity : BackupActivity() { setContentView(R.layout.activity_recovery_code) viewModel.isRestore = isRestore() - viewModel.confirmButtonClicked.observeEvent(this, { clicked -> + viewModel.confirmButtonClicked.observeEvent(this) { clicked -> if (clicked) showInput(true) - }) - viewModel.recoveryCodeSaved.observeEvent(this, { saved -> + } + viewModel.recoveryCodeSaved.observeEvent(this) { saved -> if (saved) { setResult(RESULT_OK) finishAfterTransition() } - }) + } if (savedInstanceState == null) { if (viewModel.isRestore) showInput(false) diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeViewModel.kt index 3db20707..643d447c 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeViewModel.kt @@ -16,15 +16,18 @@ import cash.z.ecc.android.bip39.toSeed import com.stevesoltys.seedvault.App import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.crypto.KeyManager +import com.stevesoltys.seedvault.repo.AppBackupManager import com.stevesoltys.seedvault.transport.backup.BackupInitializer import com.stevesoltys.seedvault.ui.LiveEvent import com.stevesoltys.seedvault.ui.MutableLiveEvent import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.calyxos.backup.storage.api.StorageBackup import java.io.IOException +import kotlin.system.exitProcess internal const val WORD_NUM = 12 @@ -35,6 +38,7 @@ internal class RecoveryCodeViewModel( private val crypto: Crypto, private val keyManager: KeyManager, private val backupManager: IBackupManager, + private val appBackupManager: AppBackupManager, private val backupInitializer: BackupInitializer, private val notificationManager: BackupNotificationManager, private val storageBackup: StorageBackup, @@ -104,20 +108,33 @@ internal class RecoveryCodeViewModel( * The reason is that old backups won't be readable anymore with the new key. * We can't delete other backups safely, because we can't be sure * that they don't belong to a different device or user. + * + * Our process will be terminated at the end to ensure the old key isn't used anymore. */ + @OptIn(DelicateCoroutinesApi::class) fun reinitializeBackupLocation() { Log.d(TAG, "Re-initializing backup location...") - // TODO this code is almost identical to BackupStorageViewModel#onLocationSet(), unify? GlobalScope.launch(Dispatchers.IO) { + // remove old backup repository and clear local blob cache + try { + appBackupManager.removeBackupRepo() + } catch (e: IOException) { + Log.e(TAG, "Error removing backup repo: ", e) + } // remove old storage snapshots and clear cache storageBackup.init() + // we'll need to kill our process to not have references to the old key around + // trying to re-set all those references is complicated, so exiting the app is easier. + val exitApp = { + Log.w(TAG, "Shutting down app...") + exitProcess(0) + } try { // initialize the new location - if (backupManager.isBackupEnabled) backupInitializer.initialize({ }) { - // no-op - } + if (backupManager.isBackupEnabled) backupInitializer.initialize(exitApp, exitApp) } catch (e: IOException) { Log.e(TAG, "Error starting new RestoreSet", e) + exitApp() } } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/worker/WorkerModule.kt b/app/src/main/java/com/stevesoltys/seedvault/worker/WorkerModule.kt index 768822f7..c3f64cbb 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/worker/WorkerModule.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/worker/WorkerModule.kt @@ -5,7 +5,6 @@ package com.stevesoltys.seedvault.worker -import com.stevesoltys.seedvault.repo.AppBackupManager import org.koin.android.ext.koin.androidContext import org.koin.dsl.module @@ -27,7 +26,6 @@ val workerModule = module { appBackupManager = get(), ) } - single { AppBackupManager(get(), get(), get(), get(), get(), get()) } single { ApkBackup( pm = androidContext().packageManager, @@ -45,7 +43,7 @@ val workerModule = module { packageService = get(), apkBackup = get(), iconManager = get(), - nm = get() + nm = get(), ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8a5c2fff..bbaec387 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -139,8 +139,8 @@ Try again Generate new code Wait one second… - Generating a new code will make your existing backups inaccessible. We\'ll try to delete them if possible.\n\nAre you sure you want to do this? - New recovery code has been created successfully + Generating a new code will make your existing backups inaccessible. We\'ll try to delete them if possible. This app will close when done.\n\nAre you sure you want to do this? + New recovery code has been created successfully. Exiting… Re-enter your screen lock Enter your device credentials to continue diff --git a/app/src/test/java/com/stevesoltys/seedvault/repo/AppBackupManagerTest.kt b/app/src/test/java/com/stevesoltys/seedvault/repo/AppBackupManagerTest.kt index 6ceda99c..f999c073 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/repo/AppBackupManagerTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/repo/AppBackupManagerTest.kt @@ -123,6 +123,20 @@ internal class AppBackupManagerTest : TransportTest() { assertEquals(snapshot, appBackupManager.afterBackupFinished(true)) } + @Test + fun `removeBackupRepo deletes repo and local cache`() = runBlocking { + every { blobCache.clearLocalCache() } just Runs + every { crypto.repoId } returns repoId + coEvery { backendManager.backend.remove(TopLevelFolder(repoId)) } just Runs + + appBackupManager.removeBackupRepo() + + coVerify { + blobCache.clearLocalCache() + backendManager.backend.remove(TopLevelFolder(repoId)) + } + } + private suspend fun minimalBeforeBackup() { every { backendManager.backend } returns backend every { crypto.repoId } returns repoId