diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/Biometrics.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/Biometrics.kt index fbcdb18d..61f61c09 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/Biometrics.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/Biometrics.kt @@ -5,23 +5,20 @@ import android.content.Context import android.content.ContextWrapper import android.content.Intent import android.os.Build -import android.widget.Toast import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.result.ActivityResult import androidx.appcompat.app.AppCompatActivity import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt import com.greenart7c3.nostrsigner.R -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch object Biometrics { fun authenticate( title: String, context: Context, - scope: CoroutineScope, keyguardLauncher: ManagedActivityResultLauncher, - onApproved: () -> Unit + onApproved: () -> Unit, + onError: (String, String) -> Unit ) { val fragmentContext = context.getAppCompatActivity()!! val keyguardManager = @@ -34,10 +31,11 @@ object Biometrics { @Suppress("DEPRECATION") fun keyguardPrompt() { - val intent = keyguardManager.createConfirmDeviceCredentialIntent( - context.getString(R.string.app_name), - title - ) + val intent = + keyguardManager.createConfirmDeviceCredentialIntent( + context.getString(R.string.app_name_release), + title + ) keyguardLauncher.launch(intent) } @@ -50,49 +48,51 @@ object Biometrics { val biometricManager = BiometricManager.from(context) val authenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL - val promptInfo = BiometricPrompt.PromptInfo.Builder() - .setTitle(context.getString(R.string.app_name)) - .setSubtitle(title) - .setAllowedAuthenticators(authenticators) - .build() + val promptInfo = + BiometricPrompt.PromptInfo.Builder() + .setTitle(context.getString(R.string.app_name_release)) + .setSubtitle(title) + .setAllowedAuthenticators(authenticators) + .build() - val biometricPrompt = BiometricPrompt( - fragmentContext, - object : BiometricPrompt.AuthenticationCallback() { - override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { - super.onAuthenticationError(errorCode, errString) + val biometricPrompt = + BiometricPrompt( + fragmentContext, + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError( + errorCode: Int, + errString: CharSequence + ) { + super.onAuthenticationError(errorCode, errString) - when (errorCode) { - BiometricPrompt.ERROR_NEGATIVE_BUTTON -> keyguardPrompt() - BiometricPrompt.ERROR_LOCKOUT -> keyguardPrompt() - else -> - scope.launch { - Toast.makeText( - context, - "${context.getString(R.string.biometric_error)}: $errString", - Toast.LENGTH_SHORT - ).show() - } + when (errorCode) { + BiometricPrompt.ERROR_NEGATIVE_BUTTON -> keyguardPrompt() + BiometricPrompt.ERROR_LOCKOUT -> keyguardPrompt() + else -> + onError( + context.getString(R.string.biometric_authentication_failed), + context.getString( + R.string.biometric_authentication_failed_explainer_with_error, + errString + ) + ) + } } - } - override fun onAuthenticationFailed() { - super.onAuthenticationFailed() - scope.launch { - Toast.makeText( - context, + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + onError( context.getString(R.string.biometric_authentication_failed), - Toast.LENGTH_SHORT - ).show() + context.getString(R.string.biometric_authentication_failed_explainer) + ) + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + onApproved() } } - - override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { - super.onAuthenticationSucceeded(result) - onApproved() - } - } - ) + ) when (biometricManager.canAuthenticate(authenticators)) { BiometricManager.BIOMETRIC_SUCCESS -> biometricPrompt.authenticate(promptInfo) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt index 73eb7f35..494ce49f 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import com.greenart7c3.nostrsigner.LocalPreferences import com.greenart7c3.nostrsigner.models.Account +import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.bechToBytes import fr.acinq.secp256k1.Hex @@ -60,8 +61,12 @@ class AccountStateViewModel(npub: String?) : ViewModel() { tryLoginExistingAccount(route, npub) } - fun startUI(key: String, route: String?) { - val account = if (key.startsWith("nsec")) { + fun startUI(key: String, password: String, route: String?) { + val account = if (key.startsWith("ncryptsec")) { + val newKey = CryptoUtils.decryptNIP49(key, password) + ?: throw Exception("Could not decrypt key with provided password") + Account(KeyPair(Hex.decode(newKey)), name = "", savedApps = mutableMapOf()) + } else if (key.startsWith("nsec")) { Account(KeyPair(privKey = key.bechToBytes()), name = "", savedApps = mutableMapOf()) } else { Account(KeyPair(Hex.decode(key)), name = "", savedApps = mutableMapOf()) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt index e9a1aaaa..d6710a53 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt @@ -28,6 +28,7 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -65,6 +66,13 @@ fun LoginPage( mutableStateOf(false) } val context = LocalContext.current + val password = remember { mutableStateOf(TextFieldValue("")) } + val needsPassword = + remember { + derivedStateOf { + key.value.text.startsWith("ncryptsec1") + } + } Column( modifier = Modifier @@ -90,24 +98,35 @@ fun LoginPage( mutableStateOf(false) } - val autofillNode = AutofillNode( - autofillTypes = listOf(AutofillType.Password), - onFill = { key.value = TextFieldValue(it) } - ) + var showCharsPassword by remember { mutableStateOf(false) } + + val autofillNodeKey = + AutofillNode( + autofillTypes = listOf(AutofillType.Password), + onFill = { key.value = TextFieldValue(it) } + ) + + val autofillNodePassword = + AutofillNode( + autofillTypes = listOf(AutofillType.Password), + onFill = { key.value = TextFieldValue(it) } + ) + val autofill = LocalAutofill.current - LocalAutofillTree.current += autofillNode + LocalAutofillTree.current += autofillNodeKey + LocalAutofillTree.current += autofillNodePassword OutlinedTextField( modifier = Modifier .onGloballyPositioned { coordinates -> - autofillNode.boundingBox = coordinates.boundsInWindow() + autofillNodeKey.boundingBox = coordinates.boundsInWindow() } .onFocusChanged { focusState -> autofill?.run { if (focusState.isFocused) { - requestAutofillForNode(autofillNode) + requestAutofillForNode(autofillNodeKey) } else { - cancelAutofillForNode(autofillNode) + cancelAutofillForNode(autofillNodeKey) } } }, @@ -158,10 +177,21 @@ fun LoginPage( visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), keyboardActions = KeyboardActions( onGo = { - try { - accountViewModel.startUI(key.value.text, null) - } catch (e: Exception) { - errorMessage = context.getString(R.string.invalid_key) + if (key.value.text.isBlank()) { + errorMessage = context.getString(R.string.key_is_required) + } + + if (needsPassword.value && password.value.text.isBlank()) { + errorMessage = context.getString(R.string.password_is_required) + } + + if (key.value.text.isNotBlank() && !(needsPassword.value && password.value.text.isBlank())) { + try { + accountViewModel.startUI(key.value.text, password.value.text, null) + } catch (e: Exception) { + Log.e("Login", "Could not sign in", e) + errorMessage = context.getString(R.string.invalid_key) + } } } ) @@ -176,6 +206,86 @@ fun LoginPage( Spacer(modifier = Modifier.height(20.dp)) + if (needsPassword.value) { + OutlinedTextField( + modifier = + Modifier + .onGloballyPositioned { coordinates -> + autofillNodePassword.boundingBox = coordinates.boundsInWindow() + } + .onFocusChanged { focusState -> + autofill?.run { + if (focusState.isFocused) { + requestAutofillForNode(autofillNodePassword) + } else { + cancelAutofillForNode(autofillNodePassword) + } + } + }, + value = password.value, + onValueChange = { + password.value = it + if (errorMessage.isNotEmpty()) { + errorMessage = "" + } + }, + keyboardOptions = + KeyboardOptions( + autoCorrect = false, + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Go + ), + placeholder = { + Text( + text = stringResource(R.string.ncryptsec_password) + ) + }, + trailingIcon = { + Row { + IconButton(onClick = { showCharsPassword = !showCharsPassword }) { + Icon( + imageVector = + if (showCharsPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility, + contentDescription = + if (showCharsPassword) { + stringResource(R.string.show_password) + } else { + stringResource( + R.string.hide_password + ) + } + ) + } + } + }, + visualTransformation = + if (showCharsPassword) VisualTransformation.None else PasswordVisualTransformation(), + keyboardActions = + KeyboardActions( + onGo = { + if (key.value.text.isBlank()) { + errorMessage = context.getString(R.string.key_is_required) + } + + if (needsPassword.value && password.value.text.isBlank()) { + errorMessage = context.getString(R.string.password_is_required) + } + + if (key.value.text.isNotBlank() && !(needsPassword.value && password.value.text.isBlank())) { + try { + accountViewModel.startUI(key.value.text, password.value.text, null) + } catch (e: Exception) { + Log.e("Login", "Could not sign in", e) + errorMessage = context.getString(R.string.invalid_key) + } + } + } + ) + ) + } + + Spacer(modifier = Modifier.height(10.dp)) + Box(modifier = Modifier.padding(40.dp, 0.dp, 40.dp, 0.dp)) { Button( onClick = { @@ -183,9 +293,13 @@ fun LoginPage( errorMessage = context.getString(R.string.key_is_required) } - if (key.value.text.isNotBlank()) { + if (needsPassword.value && password.value.text.isBlank()) { + errorMessage = context.getString(R.string.password_is_required) + } + + if (key.value.text.isNotBlank() && !(needsPassword.value && password.value.text.isBlank())) { try { - accountViewModel.startUI(key.value.text, null) + accountViewModel.startUI(key.value.text, password.value.text, null) } catch (e: Exception) { Log.e("Login", "Could not sign in", e) errorMessage = context.getString(R.string.invalid_key) @@ -201,6 +315,8 @@ fun LoginPage( } } + Spacer(modifier = Modifier.height(10.dp)) + TextButton( modifier = Modifier .padding(30.dp) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupDialog.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupDialog.kt index 8d7293b4..f75def41 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupDialog.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupDialog.kt @@ -16,33 +16,55 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Key import androidx.compose.material.icons.filled.QrCode +import androidx.compose.material.icons.outlined.Visibility +import androidx.compose.material.icons.outlined.VisibilityOff import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.autofill.AutofillNode +import androidx.compose.ui.autofill.AutofillType +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.ClipboardManager +import androidx.compose.ui.platform.LocalAutofill +import androidx.compose.ui.platform.LocalAutofillTree import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.greenart7c3.nostrsigner.R import com.greenart7c3.nostrsigner.models.Account -import com.greenart7c3.nostrsigner.service.Biometrics +import com.greenart7c3.nostrsigner.service.Biometrics.authenticate import com.greenart7c3.nostrsigner.ui.QrCodeDrawer import com.greenart7c3.nostrsigner.ui.components.CloseButton import com.greenart7c3.nostrsigner.ui.theme.ButtonBorder @@ -51,18 +73,23 @@ import com.halilibo.richtext.markdown.Markdown import com.halilibo.richtext.ui.RichTextStyle import com.halilibo.richtext.ui.material3.Material3RichText import com.halilibo.richtext.ui.resolveDefaults -import com.vitorpamplona.quartz.encoders.toNpub +import com.vitorpamplona.quartz.crypto.CryptoUtils +import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.encoders.toNsec import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +@OptIn(ExperimentalComposeUiApi::class) @Composable fun AccountBackupDialog(account: Account, onClose: () -> Unit) { Dialog( onDismissRequest = onClose, properties = DialogProperties(usePlatformDefaultWidth = false) ) { - Surface(modifier = Modifier.fillMaxSize()) { + Surface( + modifier = Modifier + .fillMaxSize() + ) { Column( modifier = Modifier .background(MaterialTheme.colorScheme.background) @@ -80,10 +107,9 @@ fun AccountBackupDialog(account: Account, onClose: () -> Unit) { Column( modifier = Modifier - .fillMaxSize() - .padding(horizontal = 30.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + .padding(horizontal = 30.dp) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally ) { Material3RichText( style = RichTextStyle().resolveDefaults() @@ -97,7 +123,98 @@ fun AccountBackupDialog(account: Account, onClose: () -> Unit) { NSecCopyButton(account) NSecQrButton(account) - NPubCopyButton(account) + + Spacer(modifier = Modifier.height(30.dp)) + + Material3RichText( + style = RichTextStyle().resolveDefaults() + ) { + Markdown( + content = stringResource(R.string.account_backup_tips3_md) + ) + } + + Spacer(modifier = Modifier.height(10.dp)) + + val password = remember { mutableStateOf(TextFieldValue("")) } + var errorMessage by remember { mutableStateOf("") } + var showCharsPassword by remember { mutableStateOf(false) } + + val autofillNode = + AutofillNode( + autofillTypes = listOf(AutofillType.Password), + onFill = { password.value = TextFieldValue(it) } + ) + val autofill = LocalAutofill.current + LocalAutofillTree.current += autofillNode + + OutlinedTextField( + modifier = + Modifier + .onGloballyPositioned { coordinates -> + autofillNode.boundingBox = coordinates.boundsInWindow() + } + .onFocusChanged { focusState -> + autofill?.run { + if (focusState.isFocused) { + requestAutofillForNode(autofillNode) + } else { + cancelAutofillForNode(autofillNode) + } + } + }, + value = password.value, + onValueChange = { + password.value = it + if (errorMessage.isNotEmpty()) { + errorMessage = "" + } + }, + keyboardOptions = + KeyboardOptions( + autoCorrect = false, + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Go + ), + placeholder = { + Text( + text = stringResource(R.string.ncryptsec_password) + ) + }, + trailingIcon = { + Row { + IconButton(onClick = { showCharsPassword = !showCharsPassword }) { + Icon( + imageVector = + if (showCharsPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility, + contentDescription = + if (showCharsPassword) { + stringResource(R.string.show_password) + } else { + stringResource( + R.string.hide_password + ) + } + ) + } + } + }, + visualTransformation = + if (showCharsPassword) VisualTransformation.None else PasswordVisualTransformation() + ) + + if (errorMessage.isNotBlank()) { + Text( + text = errorMessage, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall + ) + } + + Spacer(modifier = Modifier.height(10.dp)) + + EncryptNSecCopyButton(account, password) + EncryptNSecQRButton(account, password) } } } @@ -132,14 +249,23 @@ private fun NSecQrButton( Button( modifier = Modifier.padding(horizontal = 3.dp), onClick = { - Biometrics.authenticate( + authenticate( title = context.getString(R.string.show_qr_code), context = context, - scope = scope, - keyguardLauncher = keyguardLauncher - ) { - showDialog = true - } + keyguardLauncher = keyguardLauncher, + onApproved = { + showDialog = true + }, + onError = { _, message -> + scope.launch { + Toast.makeText( + context, + message, + Toast.LENGTH_SHORT + ).show() + } + } + ) }, shape = ButtonBorder, contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp) @@ -218,14 +344,23 @@ private fun NSecCopyButton( Button( modifier = Modifier.padding(horizontal = 3.dp), onClick = { - Biometrics.authenticate( + authenticate( title = context.getString(R.string.copy_my_secret_key), context = context, - scope = scope, - keyguardLauncher = keyguardLauncher - ) { - copyNSec(context, scope, account, clipboardManager) - } + keyguardLauncher = keyguardLauncher, + onApproved = { + copyNSec(context, scope, account, clipboardManager) + }, + onError = { _, message -> + scope.launch { + Toast.makeText( + context, + message, + Toast.LENGTH_SHORT + ).show() + } + } + ) }, shape = ButtonBorder, contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp) @@ -240,42 +375,6 @@ private fun NSecCopyButton( } } -@Composable -private fun NPubCopyButton( - account: Account -) { - val clipboardManager = LocalClipboardManager.current - val context = LocalContext.current - val scope = rememberCoroutineScope() - - Button( - modifier = Modifier.padding(horizontal = 3.dp), - onClick = { - clipboardManager.setText(AnnotatedString(account.keyPair.pubKey.toNpub())) - scope.launch { - Toast.makeText( - context, - context.getString(R.string.public_key_npub_copied_to_clipboard), - Toast.LENGTH_SHORT - ).show() - } - }, - shape = ButtonBorder, - contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp) - ) { - Icon( - tint = MaterialTheme.colorScheme.onPrimary, - imageVector = Icons.Default.Key, - contentDescription = stringResource(R.string.copies_the_npub_id_your_user_to_the_clipboard_for_backup), - modifier = Modifier.padding(end = 5.dp) - ) - Text( - stringResource(R.string.copy_my_public_key), - color = MaterialTheme.colorScheme.onPrimary - ) - } -} - private fun copyNSec( context: Context, scope: CoroutineScope, @@ -293,3 +392,173 @@ private fun copyNSec( } } } + +@Composable +private fun EncryptNSecCopyButton( + account: Account, + password: MutableState +) { + val clipboardManager = LocalClipboardManager.current + val context = LocalContext.current + val scope = rememberCoroutineScope() + + val keyguardLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { + result: ActivityResult -> + if (result.resultCode == Activity.RESULT_OK) { + encryptCopyNSec(password, context, scope, account, clipboardManager) + } + } + + Button( + modifier = Modifier.padding(horizontal = 3.dp), + onClick = { + authenticate( + title = context.getString(R.string.copy_my_secret_key), + context = context, + keyguardLauncher = keyguardLauncher, + onApproved = { encryptCopyNSec(password, context, scope, account, clipboardManager) }, + onError = { _, message -> + scope.launch { + Toast.makeText( + context, + message, + Toast.LENGTH_SHORT + ).show() + } + } + ) + }, + shape = ButtonBorder, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary + ), + contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp), + enabled = password.value.text.isNotBlank() + ) { + Icon( + tint = MaterialTheme.colorScheme.onPrimary, + imageVector = Icons.Default.Key, + contentDescription = + stringResource(R.string.copies_the_nsec_id_your_password_to_the_clipboard_for_backup), + modifier = Modifier.padding(end = 5.dp) + ) + Text( + stringResource(id = R.string.encrypt_and_copy_my_secret_key), + color = MaterialTheme.colorScheme.onPrimary + ) + } +} + +@Composable +private fun EncryptNSecQRButton( + account: Account, + password: MutableState +) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + + var showDialog by remember { + mutableStateOf(false) + } + + if (showDialog) { + QrCodeDialog( + content = CryptoUtils.encryptNIP49(account.keyPair.privKey!!.toHexKey(), password.value.text) ?: "" + ) { + showDialog = false + } + } + + val keyguardLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { + result: ActivityResult -> + if (result.resultCode == Activity.RESULT_OK) { + showDialog = true + } + } + + Button( + modifier = Modifier.padding(horizontal = 3.dp), + onClick = { + authenticate( + title = context.getString(R.string.copy_my_secret_key), + context = context, + keyguardLauncher = keyguardLauncher, + onApproved = { showDialog = true }, + onError = { _, message -> + scope.launch { + Toast.makeText( + context, + message, + Toast.LENGTH_SHORT + ).show() + } + } + ) + }, + shape = ButtonBorder, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary + ), + contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp), + enabled = password.value.text.isNotBlank() + ) { + Icon( + tint = MaterialTheme.colorScheme.onPrimary, + imageVector = Icons.Default.QrCode, + contentDescription = + stringResource(R.string.shows_qr_code_with_you_private_key), + modifier = Modifier.padding(end = 5.dp) + ) + Text( + stringResource(id = R.string.show_qr_code), + color = MaterialTheme.colorScheme.onPrimary + ) + } +} + +private fun encryptCopyNSec( + password: MutableState, + context: Context, + scope: CoroutineScope, + account: Account, + clipboardManager: ClipboardManager +) { + if (password.value.text.isBlank()) { + scope.launch { + Toast.makeText( + context, + context.getString(R.string.password_is_required), + Toast.LENGTH_SHORT + ) + .show() + } + } else { + account.keyPair.privKey?.let { + val key = CryptoUtils.encryptNIP49(it.toHexKey(), password.value.text) + if (key != null) { + clipboardManager.setText(AnnotatedString(key)) + scope.launch { + Toast.makeText( + context, + context.getString(R.string.secret_key_copied_to_clipboard), + Toast.LENGTH_SHORT + ) + .show() + } + } else { + scope.launch { + Toast.makeText( + context, + context.getString(R.string.failed_to_encrypt_key), + Toast.LENGTH_SHORT + ) + .show() + } + } + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d13c0ca9..6500861f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -76,4 +76,14 @@ Made possible by Show QR Code Shows QR Code with you private key + + For additional security, you can encrypt your key with a password. This key starts with **ncryptsec1** and cannot be used without your password. + \n\nIf you lose your password, you will not be able to recover your key. + + password to open the key + Password is required + Failed to encrypt your private key + Biometrics failed to authenticate the owner of this phone + Biometrics failed to authenticate the owner of this phone. Error: %1$s + Encrypt and copy my secret key \ No newline at end of file