Ensure that we have the main key for v1 crypto

We ask the user to generate a new key, because actively asking for the old one is training bad security habits, but technically verifying the old key will also work.
This commit is contained in:
Torsten Grote 2021-09-08 10:34:42 +02:00 committed by Chirayu Desai
parent 7dceb98670
commit 0f241f7d25
8 changed files with 69 additions and 20 deletions

View file

@ -46,7 +46,7 @@ open class App : Application() {
factory { AppListRetriever(this@App, get(), get(), get()) } factory { AppListRetriever(this@App, get(), get(), get()) }
viewModel { SettingsViewModel(this@App, get(), get(), get(), get(), get(), get()) } viewModel { SettingsViewModel(this@App, get(), get(), get(), get(), get(), get()) }
viewModel { RecoveryCodeViewModel(this@App, get(), get(), get(), get(), get()) } viewModel { RecoveryCodeViewModel(this@App, get(), get(), get(), get(), get(), get()) }
viewModel { BackupStorageViewModel(this@App, get(), get(), get(), get()) } viewModel { BackupStorageViewModel(this@App, get(), get(), get(), get()) }
viewModel { RestoreStorageViewModel(this@App, get(), get()) } viewModel { RestoreStorageViewModel(this@App, get(), get()) }
viewModel { RestoreViewModel(this@App, get(), get(), get(), get(), get(), get()) } viewModel { RestoreViewModel(this@App, get(), get(), get(), get(), get(), get()) }

View file

@ -126,6 +126,8 @@ internal class RestoreViewModel(
@Throws(RemoteException::class) @Throws(RemoteException::class)
private fun getOrStartSession(): IRestoreSession { private fun getOrStartSession(): IRestoreSession {
// TODO consider not using the BackupManager for this, but our own API directly
// this is less error-prone (hanging sessions) and can provide more data
val session = this.session val session = this.session
?: backupManager.beginRestoreSessionForUser(UserHandle.myUserId(), null, TRANSPORT_ID) ?: backupManager.beginRestoreSessionForUser(UserHandle.myUserId(), null, TRANSPORT_ID)
?: throw RemoteException("beginRestoreSessionForUser returned null") ?: throw RemoteException("beginRestoreSessionForUser returned null")

View file

@ -54,12 +54,19 @@ class SettingsFragment : PreferenceFragmentCompat() {
backup = findPreference("backup")!! backup = findPreference("backup")!!
backup.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue -> backup.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue ->
val enabled = newValue as Boolean val enabled = newValue as Boolean
// don't enable if we don't have the main key
if (enabled && !viewModel.hasMainKey()) {
showCodeRegenerationNeededDialog()
backup.isChecked = false
return@OnPreferenceChangeListener false
}
// main key is present, so enable or disable normally
try { try {
backupManager.isBackupEnabled = enabled backupManager.isBackupEnabled = enabled
if (enabled) viewModel.enableCallLogBackup() if (enabled) viewModel.enableCallLogBackup()
return@OnPreferenceChangeListener true return@OnPreferenceChangeListener true
} catch (e: RemoteException) { } catch (e: RemoteException) {
e.printStackTrace() Log.e(TAG, "Error setting backup enabled to $enabled", e)
backup.isChecked = !enabled backup.isChecked = !enabled
return@OnPreferenceChangeListener false return@OnPreferenceChangeListener false
} }
@ -222,12 +229,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
.setTitle(R.string.settings_backup_storage_dialog_title) .setTitle(R.string.settings_backup_storage_dialog_title)
.setMessage(R.string.settings_backup_storage_dialog_message) .setMessage(R.string.settings_backup_storage_dialog_message)
.setPositiveButton(R.string.settings_backup_storage_dialog_ok) { dialog, _ -> .setPositiveButton(R.string.settings_backup_storage_dialog_ok) { dialog, _ ->
if (viewModel.hasMainKey()) {
viewModel.enableStorageBackup() viewModel.enableStorageBackup()
backupStorage.isChecked = true backupStorage.isChecked = true
} else {
showCodeVerificationNeededDialog()
}
dialog.dismiss() dialog.dismiss()
} }
.setNegativeButton(R.string.settings_backup_apk_dialog_cancel) { dialog, _ -> .setNegativeButton(R.string.settings_backup_apk_dialog_cancel) { dialog, _ ->
@ -236,12 +239,12 @@ class SettingsFragment : PreferenceFragmentCompat() {
.show() .show()
} }
private fun showCodeVerificationNeededDialog() { private fun showCodeRegenerationNeededDialog() {
AlertDialog.Builder(requireContext()) AlertDialog.Builder(requireContext())
.setIcon(R.drawable.ic_vpn_key) .setIcon(R.drawable.ic_vpn_key)
.setTitle(R.string.settings_backup_storage_code_dialog_title) .setTitle(R.string.settings_backup_new_code_dialog_title)
.setMessage(R.string.settings_backup_storage_code_dialog_message) .setMessage(R.string.settings_backup_new_code_dialog_message)
.setPositiveButton(R.string.settings_backup_storage_code_dialog_ok) { dialog, _ -> .setPositiveButton(R.string.settings_backup_new_code_code_dialog_ok) { dialog, _ ->
val callback = (requireActivity() as OnPreferenceStartFragmentCallback) val callback = (requireActivity() as OnPreferenceStartFragmentCallback)
callback.onPreferenceStartFragment(this, backupRecoveryCode) callback.onPreferenceStartFragment(this, backupRecoveryCode)
dialog.dismiss() dialog.dismiss()

View file

@ -10,6 +10,7 @@ import android.os.RemoteException
import android.util.Log import android.util.Log
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.BackupMonitor import com.stevesoltys.seedvault.BackupMonitor
import com.stevesoltys.seedvault.crypto.KeyManager
import com.stevesoltys.seedvault.transport.backup.PackageService import com.stevesoltys.seedvault.transport.backup.PackageService
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.ui.notification.NotificationBackupObserver import com.stevesoltys.seedvault.ui.notification.NotificationBackupObserver
@ -27,6 +28,8 @@ class ConfigurableBackupTransportService : Service(), KoinComponent {
private var transport: ConfigurableBackupTransport? = null private var transport: ConfigurableBackupTransport? = null
private val keyManager: KeyManager by inject()
private val backupManager: IBackupManager by inject()
private val notificationManager: BackupNotificationManager by inject() private val notificationManager: BackupNotificationManager by inject()
override fun onCreate() { override fun onCreate() {
@ -35,7 +38,13 @@ class ConfigurableBackupTransportService : Service(), KoinComponent {
Log.d(TAG, "Service created.") Log.d(TAG, "Service created.")
} }
override fun onBind(intent: Intent): IBinder { override fun onBind(intent: Intent): IBinder? {
// refuse to work until we have the main key
val noMainKey = keyManager.hasBackupKey() && !keyManager.hasMainKey()
if (noMainKey && backupManager.currentTransport == TRANSPORT_ID) {
notificationManager.onNoMainKeyError()
backupManager.isBackupEnabled = false
}
val transport = this.transport ?: throw IllegalStateException("no transport in onBind()") val transport = this.transport ?: throw IllegalStateException("no transport in onBind()")
return transport.binder.apply { return transport.binder.apply {
Log.d(TAG, "Transport bound.") Log.d(TAG, "Transport bound.")

View file

@ -7,6 +7,7 @@ import android.app.NotificationManager.IMPORTANCE_DEFAULT
import android.app.NotificationManager.IMPORTANCE_HIGH import android.app.NotificationManager.IMPORTANCE_HIGH
import android.app.NotificationManager.IMPORTANCE_LOW import android.app.NotificationManager.IMPORTANCE_LOW
import android.app.PendingIntent import android.app.PendingIntent
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -33,6 +34,7 @@ private const val NOTIFICATION_ID_OBSERVER = 1
private const val NOTIFICATION_ID_ERROR = 2 private const val NOTIFICATION_ID_ERROR = 2
private const val NOTIFICATION_ID_RESTORE_ERROR = 3 private const val NOTIFICATION_ID_RESTORE_ERROR = 3
private const val NOTIFICATION_ID_BACKGROUND = 4 private const val NOTIFICATION_ID_BACKGROUND = 4
private const val NOTIFICATION_ID_NO_MAIN_KEY_ERROR = 5
private val TAG = BackupNotificationManager::class.java.simpleName private val TAG = BackupNotificationManager::class.java.simpleName
@ -269,4 +271,28 @@ internal class BackupNotificationManager(private val context: Context) {
nm.cancel(NOTIFICATION_ID_RESTORE_ERROR) nm.cancel(NOTIFICATION_ID_RESTORE_ERROR)
} }
@SuppressLint("RestrictedApi")
fun onNoMainKeyError() {
val intent = Intent(context, SettingsActivity::class.java)
val pendingIntent = PendingIntent.getActivity(context, 0, intent, FLAG_IMMUTABLE)
val actionText = context.getString(R.string.notification_error_action)
val action = Action(0, actionText, pendingIntent)
val notification = Builder(context, CHANNEL_ID_ERROR).apply {
setSmallIcon(R.drawable.ic_cloud_error)
setContentTitle(context.getString(R.string.notification_error_no_main_key_title))
setContentText(context.getString(R.string.notification_error_no_main_key_text))
setWhen(System.currentTimeMillis())
setOnlyAlertOnce(true)
setAutoCancel(false)
setOngoing(true)
setContentIntent(pendingIntent)
mActions = arrayListOf(action)
}.build()
nm.notify(NOTIFICATION_ID_NO_MAIN_KEY_ERROR, notification)
}
fun onNoMainKeyErrorFixed() {
nm.cancel(NOTIFICATION_ID_NO_MAIN_KEY_ERROR)
}
} }

View file

@ -16,6 +16,7 @@ import com.stevesoltys.seedvault.transport.TRANSPORT_ID
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
import com.stevesoltys.seedvault.ui.LiveEvent import com.stevesoltys.seedvault.ui.LiveEvent
import com.stevesoltys.seedvault.ui.MutableLiveEvent import com.stevesoltys.seedvault.ui.MutableLiveEvent
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -33,6 +34,7 @@ internal class RecoveryCodeViewModel(
private val keyManager: KeyManager, private val keyManager: KeyManager,
private val backupManager: IBackupManager, private val backupManager: IBackupManager,
private val backupCoordinator: BackupCoordinator, private val backupCoordinator: BackupCoordinator,
private val notificationManager: BackupNotificationManager,
private val storageBackup: StorageBackup private val storageBackup: StorageBackup
) : AndroidViewModel(app) { ) : AndroidViewModel(app) {
@ -77,6 +79,7 @@ internal class RecoveryCodeViewModel(
// store main key at this opportunity if it is still missing // store main key at this opportunity if it is still missing
if (verified && !keyManager.hasMainKey()) keyManager.storeMainKey(seed) if (verified && !keyManager.hasMainKey()) keyManager.storeMainKey(seed)
mExistingCodeChecked.setEvent(verified) mExistingCodeChecked.setEvent(verified)
if (verified) notificationManager.onNoMainKeyErrorFixed()
} }
/** /**
@ -88,6 +91,7 @@ internal class RecoveryCodeViewModel(
keyManager.storeBackupKey(seed) keyManager.storeBackupKey(seed)
keyManager.storeMainKey(seed) keyManager.storeMainKey(seed)
mRecoveryCodeSaved.setEvent(true) mRecoveryCodeSaved.setEvent(true)
notificationManager.onNoMainKeyErrorFixed()
} }
/** /**
@ -109,7 +113,7 @@ internal class RecoveryCodeViewModel(
backupCoordinator.startNewRestoreSet() backupCoordinator.startNewRestoreSet()
// initialize the new location // initialize the new location
backupManager.initializeTransportsForUser( if (backupManager.isBackupEnabled) backupManager.initializeTransportsForUser(
UserHandle.myUserId(), UserHandle.myUserId(),
arrayOf(TRANSPORT_ID), arrayOf(TRANSPORT_ID),
null null

View file

@ -41,13 +41,15 @@ internal class BackupStorageViewModel(
// will also generate a new backup token for the new restore set // will also generate a new backup token for the new restore set
backupCoordinator.startNewRestoreSet() backupCoordinator.startNewRestoreSet()
// initialize the new location // initialize the new location (if backups are enabled)
backupManager.initializeTransportsForUser( if (backupManager.isBackupEnabled) backupManager.initializeTransportsForUser(
UserHandle.myUserId(), UserHandle.myUserId(),
arrayOf(TRANSPORT_ID), arrayOf(TRANSPORT_ID),
// if storage is on USB and this is not SetupWizard, do a backup right away // if storage is on USB and this is not SetupWizard, do a backup right away
InitializationObserver(isUsb && !isSetupWizard) InitializationObserver(isUsb && !isSetupWizard)
) ) else {
InitializationObserver(false).backupFinished(0)
}
} catch (e: IOException) { } catch (e: IOException) {
Log.e(TAG, "Error starting new RestoreSet", e) Log.e(TAG, "Error starting new RestoreSet", e)
onInitializationError() onInitializationError()

View file

@ -38,9 +38,9 @@
<string name="settings_backup_storage_dialog_title">Experimental feature</string> <string name="settings_backup_storage_dialog_title">Experimental feature</string>
<string name="settings_backup_storage_dialog_message">Backing up files is still experimental and might not work. Do not rely on it for important data.</string> <string name="settings_backup_storage_dialog_message">Backing up files is still experimental and might not work. Do not rely on it for important data.</string>
<string name="settings_backup_storage_dialog_ok">Enable anyway</string> <string name="settings_backup_storage_dialog_ok">Enable anyway</string>
<string name="settings_backup_storage_code_dialog_title">Recovery code verification required</string> <string name="settings_backup_new_code_dialog_title">New recovery code required</string>
<string name="settings_backup_storage_code_dialog_message">To enable storage backup, you need to first verify your recovery code or generate a new one.</string> <string name="settings_backup_new_code_dialog_message">To continue using app backups, you need to generate a new recovery code.\n\nWe are sorry for the inconvenience.</string>
<string name="settings_backup_storage_code_dialog_ok">Verify code</string> <string name="settings_backup_new_code_code_dialog_ok">New code</string>
<string name="settings_expert_title">Expert settings</string> <string name="settings_expert_title">Expert settings</string>
<string name="settings_expert_quota_title">Unlimited app quota</string> <string name="settings_expert_quota_title">Unlimited app quota</string>
@ -120,6 +120,9 @@
<string name="notification_restore_error_text">Plug in your %1$s before installing the app to restore its data from backup.</string> <string name="notification_restore_error_text">Plug in your %1$s before installing the app to restore its data from backup.</string>
<string name="notification_restore_error_action">Uninstall app</string> <string name="notification_restore_error_action">Uninstall app</string>
<string name="notification_error_no_main_key_title">Backups disabled</string>
<string name="notification_error_no_main_key_text">Generate a new recovery code to complete upgrade and continue to use backups.</string>
<!-- App Backup and Restore State --> <!-- App Backup and Restore State -->
<string name="backup_section_system">System apps</string> <string name="backup_section_system">System apps</string>