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