Ask for system authentication before storing a new recovery code

This will help to prevent data extraction via seedvault when somebody gets hold of an unlocked phone. However, it will not help against someone able to force you to provide fingerprints or other device secrets.
diff --git a/README.md b/README.md
index 49a97dc..f9f6aa4 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,7 @@
 * `android.permission.ACCESS_MEDIA_LOCATION` to backup original media files e.g. without stripped EXIF metadata.
 * `android.permission.FOREGROUND_SERVICE` to do periodic storage backups without interruption.
 * `android.permission.MANAGE_DOCUMENTS` to retrieve the available storage roots (optional) for better UX.
+* `android.permission.USE_BIOMETRIC` to authenticate saving a new recovery code
 
 ## Contributing
 Bug reports and pull requests are welcome on GitHub at https://github.com/seedvault-app/seedvault.
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c8a8da5..d3fa718 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -47,6 +47,9 @@
         android:name="android.permission.QUERY_ALL_PACKAGES"
         tools:ignore="QueryAllPackagesPermission" />
 
+    <!-- Used to authenticate saving a new recovery code -->
+    <uses-permission android:name="android.permission.USE_BIOMETRIC" />
+
     <application
         android:name=".App"
         android:allowBackup="false"
diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeInputFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeInputFragment.kt
index 8211a01..8cb7fd8 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeInputFragment.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/recoverycode/RecoveryCodeInputFragment.kt
@@ -1,8 +1,14 @@
 package com.stevesoltys.seedvault.ui.recoverycode
 
 import android.app.Activity.RESULT_OK
+import android.app.KeyguardManager
 import android.content.Intent
+import android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_STRONG
+import android.hardware.biometrics.BiometricManager.Authenticators.DEVICE_CREDENTIAL
+import android.hardware.biometrics.BiometricPrompt
+import android.os.Build.VERSION.SDK_INT
 import android.os.Bundle
+import android.os.CancellationSignal
 import android.view.LayoutInflater
 import android.view.View
 import android.view.View.GONE
@@ -16,8 +22,10 @@
 import android.widget.Toast
 import android.widget.Toast.LENGTH_LONG
 import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
+import androidx.annotation.RequiresApi
 import androidx.appcompat.app.AlertDialog
 import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.content.ContextCompat.getMainExecutor
 import androidx.fragment.app.Fragment
 import cash.z.ecc.android.bip39.Mnemonics
 import cash.z.ecc.android.bip39.Mnemonics.ChecksumException
@@ -143,12 +151,36 @@
             return
         }
         if (forStoringNewCode) {
-            viewModel.storeNewCode(input)
+            val keyguardManager = requireContext().getSystemService(KeyguardManager::class.java)
+            if (SDK_INT >= 30 && keyguardManager.isDeviceSecure) {
+                // if we have a lock-screen secret, we can ask for it before storing the code
+                storeNewCodeAfterAuth(input)
+            } else {
+                // user doesn't seem to care about security, store key without auth
+                viewModel.storeNewCode(input)
+            }
         } else {
             viewModel.verifyExistingCode(input)
         }
     }
 
+    @RequiresApi(30)
+    private fun storeNewCodeAfterAuth(input: List<CharSequence>) {
+        val biometricPrompt = BiometricPrompt.Builder(context)
+            .setConfirmationRequired(true)
+            .setTitle(getString(R.string.recovery_code_auth_title))
+            .setDescription(getString(R.string.recovery_code_auth_description))
+            // BIOMETRIC_STRONG could be made optional in the future, setting guarded by credentials
+            .setAllowedAuthenticators(DEVICE_CREDENTIAL or BIOMETRIC_STRONG)
+            .build()
+        val callback = object : BiometricPrompt.AuthenticationCallback() {
+            override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult?) {
+                viewModel.storeNewCode(input)
+            }
+        }
+        biometricPrompt.authenticate(CancellationSignal(), getMainExecutor(context), callback)
+    }
+
     private fun allFilledOut(input: List<CharSequence>): Boolean {
         for (i in input.indices) {
             if (input[i].isNotEmpty()) continue
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 7682402..046ee7a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -94,6 +94,8 @@
     <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_auth_title">Confirm it\'s really you</string>
+    <string name="recovery_code_auth_description">This ensures that nobody else can get your data when finding your phone unlocked.</string>
 
     <!-- Notification -->
     <string name="notification_channel_title">Backup notification</string>