support for nip 49

This commit is contained in:
greenart7c3
2024-02-21 14:52:10 -03:00
parent d79c0ffb9d
commit 2e5f00d5b8
5 changed files with 517 additions and 117 deletions
@@ -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<Intent, ActivityResult>,
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)
@@ -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())
@@ -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)
@@ -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<TextFieldValue>
) {
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<TextFieldValue>
) {
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<TextFieldValue>,
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()
}
}
}
}
}
+10
View File
@@ -76,4 +76,14 @@
<string name="made_possible_by">Made possible by</string>
<string name="show_qr_code">Show QR Code</string>
<string name="shows_qr_code_with_you_private_key">Shows QR Code with you private key</string>
<string name="account_backup_tips3_md" tools:ignore="Typos">
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.
</string>
<string name="ncryptsec_password">password to open the key</string>
<string name="password_is_required">Password is required</string>
<string name="failed_to_encrypt_key">Failed to encrypt your private key</string>
<string name="biometric_authentication_failed_explainer">Biometrics failed to authenticate the owner of this phone</string>
<string name="biometric_authentication_failed_explainer_with_error">Biometrics failed to authenticate the owner of this phone. Error: %1$s</string>
<string name="encrypt_and_copy_my_secret_key">Encrypt and copy my secret key</string>
</resources>