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