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.
This commit is contained in:
parent
62991ed38b
commit
20ea0b332d
10 changed files with 75 additions and 25 deletions
|
@ -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,
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()) }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -139,8 +139,8 @@
|
|||
<string name="recovery_code_verification_try_again">Try again</string>
|
||||
<string name="recovery_code_verification_generate_new">Generate new code</string>
|
||||
<string name="recovery_code_verification_new_dialog_title">Wait one second…</string>
|
||||
<string name="recovery_code_verification_new_dialog_message">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?</string>
|
||||
<string name="recovery_code_recreated">New recovery code has been created successfully</string>
|
||||
<string name="recovery_code_verification_new_dialog_message">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?</string>
|
||||
<string name="recovery_code_recreated">New recovery code has been created successfully. Exiting…</string>
|
||||
<string name="recovery_code_auth_title">Re-enter your screen lock</string>
|
||||
<string name="recovery_code_auth_description">Enter your device credentials to continue</string>
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue