Merge pull request #191 from maxmoney21m/feature/biometric-backup-nsec

Require biometric with device lock fallback to copy nsec
This commit is contained in:
Vitor Pamplona
2023-03-04 17:11:35 -05:00
committed by GitHub
4 changed files with 91 additions and 12 deletions

View File

@@ -73,6 +73,9 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
// Biometrics
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
// Swipe Refresh // Swipe Refresh
implementation 'com.google.accompanist:accompanist-swiperefresh:0.29.1-alpha' implementation 'com.google.accompanist:accompanist-swiperefresh:0.29.1-alpha'

View File

@@ -2,12 +2,12 @@ package com.vitorpamplona.amethyst.ui
import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION.SDK_INT
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import coil.Coil import coil.Coil
import coil.ImageLoader 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.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
class MainActivity : ComponentActivity() { class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View File

@@ -1,6 +1,12 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.content.Context
import android.content.ContextWrapper
import android.widget.Toast 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.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -24,6 +30,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource 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.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.fragment.app.FragmentActivity
import com.halilibo.richtext.markdown.Markdown import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.ui.RichTextStyle import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material.MaterialRichText 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.R
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.actions.CloseButton import com.vitorpamplona.amethyst.ui.actions.CloseButton
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import nostr.postr.toNsec import nostr.postr.toNsec
@@ -98,16 +107,7 @@ private fun NSecCopyButton(
Button( Button(
modifier = Modifier.padding(horizontal = 3.dp), modifier = Modifier.padding(horizontal = 3.dp),
onClick = { onClick = {
account.loggedIn.privKey?.let { authenticatedCopyNSec(context, scope, account, clipboardManager)
clipboardManager.setText(AnnotatedString(it.toNsec()))
scope.launch {
Toast.makeText(
context,
context.getString(R.string.secret_key_copied_to_clipboard),
Toast.LENGTH_SHORT
).show()
}
}
}, },
shape = RoundedCornerShape(20.dp), colors = ButtonDefaults.buttonColors( shape = RoundedCornerShape(20.dp), colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary backgroundColor = MaterialTheme.colors.primary
@@ -121,3 +121,77 @@ private fun NSecCopyButton(
Text("Copy Secret Key", color = MaterialTheme.colors.onPrimary) 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()
}
}
}

View File

@@ -181,4 +181,6 @@
\n- **Do** keep a secure backup of your secret key for account recovery. We recommend using a password manager. \n- **Do** keep a secure backup of your secret key for account recovery. We recommend using a password manager.
</string> </string>
<string name="secret_key_copied_to_clipboard">Secret key (nsec) copied to clipboard</string> <string name="secret_key_copied_to_clipboard">Secret key (nsec) copied to clipboard</string>
<string name="copy_my_secret_key">Copy my secret key</string>
<string name="biometric_authentication_failed">Authentication failed</string>
</resources> </resources>