From d6b5af6d7722ccbe2d32cc3f7a28ecc5118a551a Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Sat, 4 Mar 2023 22:26:06 +0800 Subject: [PATCH] Require biometric with device lock fallback to copy nsec --- app/build.gradle | 3 + .../vitorpamplona/amethyst/ui/MainActivity.kt | 4 +- .../ui/screen/loggedIn/AccountBackupDialog.kt | 94 +++++++++++++++++-- app/src/main/res/values/strings.xml | 2 + 4 files changed, 91 insertions(+), 12 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 4c2169e58..ae3cb087f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -73,6 +73,9 @@ dependencies { implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version" + // Biometrics + implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05" + // Swipe Refresh implementation 'com.google.accompanist:accompanist-swiperefresh:0.29.1-alpha' diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 3b2b81287..cd622f88c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -2,12 +2,12 @@ package com.vitorpamplona.amethyst.ui import android.os.Build.VERSION.SDK_INT import android.os.Bundle -import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.ui.Modifier +import androidx.fragment.app.FragmentActivity import androidx.lifecycle.viewmodel.compose.viewModel import coil.Coil import coil.ImageLoader @@ -22,7 +22,7 @@ import com.vitorpamplona.amethyst.ui.screen.AccountScreen import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.theme.AmethystTheme -class MainActivity : ComponentActivity() { +class MainActivity : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt index e56431e08..eedfc1355 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt @@ -1,6 +1,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn +import android.content.Context +import android.content.ContextWrapper import android.widget.Toast +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG +import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL +import androidx.biometric.BiometricPrompt import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -24,6 +30,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -31,6 +38,7 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties +import androidx.fragment.app.FragmentActivity import com.halilibo.richtext.markdown.Markdown import com.halilibo.richtext.ui.RichTextStyle import com.halilibo.richtext.ui.material.MaterialRichText @@ -38,6 +46,7 @@ import com.halilibo.richtext.ui.resolveDefaults import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.actions.CloseButton +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import nostr.postr.toNsec @@ -98,16 +107,7 @@ private fun NSecCopyButton( Button( modifier = Modifier.padding(horizontal = 3.dp), onClick = { - account.loggedIn.privKey?.let { - clipboardManager.setText(AnnotatedString(it.toNsec())) - scope.launch { - Toast.makeText( - context, - context.getString(R.string.secret_key_copied_to_clipboard), - Toast.LENGTH_SHORT - ).show() - } - } + authenticatedCopyNSec(context, scope, account, clipboardManager) }, shape = RoundedCornerShape(20.dp), colors = ButtonDefaults.buttonColors( backgroundColor = MaterialTheme.colors.primary @@ -121,3 +121,77 @@ private fun NSecCopyButton( Text("Copy Secret Key", color = MaterialTheme.colors.onPrimary) } } + +fun Context.getFragmentActivity(): FragmentActivity? { + var currentContext = this + while (currentContext is ContextWrapper) { + if (currentContext is FragmentActivity) { + return currentContext + } + currentContext = currentContext.baseContext + } + return null +} + +private fun authenticatedCopyNSec( + context: Context, + scope: CoroutineScope, + account: Account, + clipboardManager: ClipboardManager, +) { + val fragmentContext = context.getFragmentActivity()!! + val authenticators = BIOMETRIC_STRONG or DEVICE_CREDENTIAL + val biometricManager = BiometricManager.from(context) + + val promptInfo = BiometricPrompt.PromptInfo.Builder() + .setTitle(context.getString(R.string.app_name_release)) + .setSubtitle(context.getString(R.string.copy_my_secret_key)) + .setAllowedAuthenticators(authenticators) + .build() + + val biometricPrompt = BiometricPrompt( + fragmentContext, + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + scope.launch { + Toast.makeText( + context, + context.getString(R.string.biometric_authentication_failed), + Toast.LENGTH_SHORT + ).show() + } + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + copyNSec(context, scope, account, clipboardManager) + } + } + ) + + val canAuth = biometricManager.canAuthenticate(authenticators) + if (canAuth == BiometricManager.BIOMETRIC_SUCCESS) { + biometricPrompt.authenticate(promptInfo) + } else { + copyNSec(context, scope, account, clipboardManager) + } +} + +private fun copyNSec( + context: Context, + scope: CoroutineScope, + account: Account, + clipboardManager: ClipboardManager, +) { + account.loggedIn.privKey?.let { + clipboardManager.setText(AnnotatedString(it.toNsec())) + scope.launch { + Toast.makeText( + context, + context.getString(R.string.secret_key_copied_to_clipboard), + Toast.LENGTH_SHORT + ).show() + } + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 205ab9cb2..876d4103d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -181,4 +181,6 @@ \n- **Do** keep a secure backup of your secret key for account recovery. We recommend using a password manager. Secret key (nsec) copied to clipboard + Copy my secret key + Authentication failed \ No newline at end of file