Adds NIP49 to login and back up key screens
This commit is contained in:
@@ -30,6 +30,7 @@ import com.vitorpamplona.amethyst.ServiceManager
|
|||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.service.HttpClientManager
|
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||||
import com.vitorpamplona.amethyst.service.relays.Client
|
import com.vitorpamplona.amethyst.service.relays.Client
|
||||||
|
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||||
import com.vitorpamplona.quartz.encoders.Hex
|
import com.vitorpamplona.quartz.encoders.Hex
|
||||||
import com.vitorpamplona.quartz.encoders.Nip19
|
import com.vitorpamplona.quartz.encoders.Nip19
|
||||||
@@ -193,6 +194,42 @@ class AccountStateViewModel() : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun login(
|
||||||
|
key: String,
|
||||||
|
password: String,
|
||||||
|
useProxy: Boolean,
|
||||||
|
proxyPort: Int,
|
||||||
|
loginWithExternalSigner: Boolean = false,
|
||||||
|
packageName: String = "",
|
||||||
|
onError: (String?) -> Unit,
|
||||||
|
) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
if (key.startsWith("ncryptsec")) {
|
||||||
|
val newKey =
|
||||||
|
try {
|
||||||
|
CryptoUtils.decryptNIP49(key, password)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
|
onError(e.message)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newKey == null) {
|
||||||
|
onError("Could not decrypt key with provided password")
|
||||||
|
Log.e("Login", "Could not decrypt ncryptsec")
|
||||||
|
} else {
|
||||||
|
loginSync(newKey, useProxy, proxyPort, loginWithExternalSigner, packageName) {
|
||||||
|
onError(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loginSync(key, useProxy, proxyPort, loginWithExternalSigner, packageName) {
|
||||||
|
onError(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun login(
|
fun login(
|
||||||
key: String,
|
key: String,
|
||||||
useProxy: Boolean,
|
useProxy: Boolean,
|
||||||
@@ -202,13 +239,24 @@ class AccountStateViewModel() : ViewModel() {
|
|||||||
onError: () -> Unit,
|
onError: () -> Unit,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
loginSync(key, useProxy, proxyPort, loginWithExternalSigner, packageName, onError)
|
||||||
loginAndStartUI(key, useProxy, proxyPort, loginWithExternalSigner, packageName)
|
}
|
||||||
} catch (e: Exception) {
|
}
|
||||||
if (e is CancellationException) throw e
|
|
||||||
Log.e("Login", "Could not sign in", e)
|
suspend fun loginSync(
|
||||||
onError()
|
key: String,
|
||||||
}
|
useProxy: Boolean,
|
||||||
|
proxyPort: Int,
|
||||||
|
loginWithExternalSigner: Boolean = false,
|
||||||
|
packageName: String = "",
|
||||||
|
onError: () -> Unit,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
loginAndStartUI(key, useProxy, proxyPort, loginWithExternalSigner, packageName)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
|
Log.e("Login", "Could not sign in", e)
|
||||||
|
onError()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+234
-6
@@ -36,23 +36,48 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
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.Icons
|
||||||
import androidx.compose.material.icons.filled.Key
|
import androidx.compose.material.icons.filled.Key
|
||||||
|
import androidx.compose.material.icons.outlined.Visibility
|
||||||
|
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
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.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
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.ClipboardManager
|
||||||
|
import androidx.compose.ui.platform.LocalAutofill
|
||||||
|
import androidx.compose.ui.platform.LocalAutofillTree
|
||||||
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
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
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.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
|
||||||
@@ -67,10 +92,14 @@ import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
|||||||
import com.vitorpamplona.amethyst.ui.note.authenticate
|
import com.vitorpamplona.amethyst.ui.note.authenticate
|
||||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||||
import com.vitorpamplona.amethyst.ui.theme.ButtonPadding
|
import com.vitorpamplona.amethyst.ui.theme.ButtonPadding
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
|
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||||
|
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||||
import com.vitorpamplona.quartz.encoders.toNsec
|
import com.vitorpamplona.quartz.encoders.toNsec
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AccountBackupDialog(
|
fun AccountBackupDialog(
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
@@ -80,12 +109,23 @@ fun AccountBackupDialog(
|
|||||||
onDismissRequest = onClose,
|
onDismissRequest = onClose,
|
||||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||||
) {
|
) {
|
||||||
Surface(modifier = Modifier.fillMaxSize()) {
|
Surface(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.background(MaterialTheme.colorScheme.background).fillMaxSize(),
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.background(MaterialTheme.colorScheme.background)
|
||||||
|
.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(10.dp),
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(10.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
@@ -93,7 +133,10 @@ fun AccountBackupDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize().padding(horizontal = 30.dp),
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 30.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
@@ -101,13 +144,105 @@ fun AccountBackupDialog(
|
|||||||
style = RichTextStyle().resolveDefaults(),
|
style = RichTextStyle().resolveDefaults(),
|
||||||
) {
|
) {
|
||||||
Markdown(
|
Markdown(
|
||||||
content = stringResource(R.string.account_backup_tips_md),
|
content = stringResource(R.string.account_backup_tips2_md),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(30.dp))
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
NSecCopyButton(accountViewModel)
|
NSecCopyButton(accountViewModel)
|
||||||
|
|
||||||
|
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),
|
||||||
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
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(accountViewModel, password)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,6 +295,56 @@ private fun NSecCopyButton(accountViewModel: AccountViewModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EncryptNSecCopyButton(
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
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, accountViewModel, 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, accountViewModel, clipboardManager) },
|
||||||
|
onError = { title, message -> accountViewModel.toast(title, message) },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
shape = ButtonBorder,
|
||||||
|
colors =
|
||||||
|
ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
contentPadding = ButtonPadding,
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun Context.getFragmentActivity(): FragmentActivity? {
|
fun Context.getFragmentActivity(): FragmentActivity? {
|
||||||
var currentContext = this
|
var currentContext = this
|
||||||
while (currentContext is ContextWrapper) {
|
while (currentContext is ContextWrapper) {
|
||||||
@@ -189,3 +374,46 @@ private fun copyNSec(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun encryptCopyNSec(
|
||||||
|
password: MutableState<TextFieldValue>,
|
||||||
|
context: Context,
|
||||||
|
scope: CoroutineScope,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
clipboardManager: ClipboardManager,
|
||||||
|
) {
|
||||||
|
if (password.value.text.isBlank()) {
|
||||||
|
scope.launch {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
context.getString(R.string.password_is_required),
|
||||||
|
Toast.LENGTH_SHORT,
|
||||||
|
)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
accountViewModel.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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -90,10 +91,12 @@ import com.vitorpamplona.amethyst.Amethyst
|
|||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.service.PackageUtils
|
import com.vitorpamplona.amethyst.service.PackageUtils
|
||||||
import com.vitorpamplona.amethyst.ui.MainActivity
|
import com.vitorpamplona.amethyst.ui.MainActivity
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
|
||||||
import com.vitorpamplona.amethyst.ui.components.getActivity
|
import com.vitorpamplona.amethyst.ui.components.getActivity
|
||||||
import com.vitorpamplona.amethyst.ui.qrcode.SimpleQrCodeScanner
|
import com.vitorpamplona.amethyst.ui.qrcode.SimpleQrCodeScanner
|
||||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
||||||
@@ -142,6 +145,16 @@ fun LoginPage(
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var loginWithExternalSigner by remember { mutableStateOf(false) }
|
var loginWithExternalSigner by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
var processingLogin by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val password = remember { mutableStateOf(TextFieldValue("")) }
|
||||||
|
val needsPassword =
|
||||||
|
remember {
|
||||||
|
derivedStateOf {
|
||||||
|
key.value.text.startsWith("ncryptsec1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (loginWithExternalSigner) {
|
if (loginWithExternalSigner) {
|
||||||
val externalSignerLauncher = remember { ExternalSignerLauncher("", signerPackageName = "") }
|
val externalSignerLauncher = remember { ExternalSignerLauncher("", signerPackageName = "") }
|
||||||
val id = remember { UUID.randomUUID().toString() }
|
val id = remember { UUID.randomUUID().toString() }
|
||||||
@@ -243,33 +256,47 @@ fun LoginPage(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(40.dp))
|
Spacer(modifier = Modifier.height(40.dp))
|
||||||
|
|
||||||
var showPassword by remember { mutableStateOf(false) }
|
var showCharsKey by remember { mutableStateOf(false) }
|
||||||
|
var showCharsPassword by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val autofillNode =
|
val autofillNodeKey =
|
||||||
AutofillNode(
|
AutofillNode(
|
||||||
autofillTypes = listOf(AutofillType.Password),
|
autofillTypes = listOf(AutofillType.Password),
|
||||||
onFill = { key.value = TextFieldValue(it) },
|
onFill = { key.value = TextFieldValue(it) },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val autofillNodePassword =
|
||||||
|
AutofillNode(
|
||||||
|
autofillTypes = listOf(AutofillType.Password),
|
||||||
|
onFill = { key.value = TextFieldValue(it) },
|
||||||
|
)
|
||||||
|
|
||||||
val autofill = LocalAutofill.current
|
val autofill = LocalAutofill.current
|
||||||
LocalAutofillTree.current += autofillNode
|
LocalAutofillTree.current += autofillNodeKey
|
||||||
|
LocalAutofillTree.current += autofillNodePassword
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.onGloballyPositioned { coordinates ->
|
.onGloballyPositioned { coordinates ->
|
||||||
autofillNode.boundingBox = coordinates.boundsInWindow()
|
autofillNodeKey.boundingBox = coordinates.boundsInWindow()
|
||||||
}
|
}
|
||||||
.onFocusChanged { focusState ->
|
.onFocusChanged { focusState ->
|
||||||
autofill?.run {
|
autofill?.run {
|
||||||
if (focusState.isFocused) {
|
if (focusState.isFocused) {
|
||||||
requestAutofillForNode(autofillNode)
|
requestAutofillForNode(autofillNodeKey)
|
||||||
} else {
|
} else {
|
||||||
cancelAutofillForNode(autofillNode)
|
cancelAutofillForNode(autofillNodeKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
value = key.value,
|
value = key.value,
|
||||||
onValueChange = { key.value = it },
|
onValueChange = {
|
||||||
|
key.value = it
|
||||||
|
if (errorMessage.isNotEmpty()) {
|
||||||
|
errorMessage = ""
|
||||||
|
}
|
||||||
|
},
|
||||||
keyboardOptions =
|
keyboardOptions =
|
||||||
KeyboardOptions(
|
KeyboardOptions(
|
||||||
autoCorrect = false,
|
autoCorrect = false,
|
||||||
@@ -284,12 +311,12 @@ fun LoginPage(
|
|||||||
},
|
},
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
Row {
|
Row {
|
||||||
IconButton(onClick = { showPassword = !showPassword }) {
|
IconButton(onClick = { showCharsKey = !showCharsKey }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (showPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
if (showCharsKey) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
||||||
contentDescription =
|
contentDescription =
|
||||||
if (showPassword) {
|
if (showCharsKey) {
|
||||||
stringResource(R.string.show_password)
|
stringResource(R.string.show_password)
|
||||||
} else {
|
} else {
|
||||||
stringResource(
|
stringResource(
|
||||||
@@ -322,22 +349,32 @@ fun LoginPage(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
visualTransformation =
|
visualTransformation =
|
||||||
if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
if (showCharsKey) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
keyboardActions =
|
keyboardActions =
|
||||||
KeyboardActions(
|
KeyboardActions(
|
||||||
onGo = {
|
onGo = {
|
||||||
if (!acceptedTerms.value) {
|
if (!acceptedTerms.value) {
|
||||||
termsAcceptanceIsRequired =
|
termsAcceptanceIsRequired = context.getString(R.string.acceptance_of_terms_is_required)
|
||||||
context.getString(R.string.acceptance_of_terms_is_required)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.value.text.isBlank()) {
|
if (key.value.text.isBlank()) {
|
||||||
errorMessage = context.getString(R.string.key_is_required)
|
errorMessage = context.getString(R.string.key_is_required)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (acceptedTerms.value && key.value.text.isNotBlank()) {
|
if (needsPassword.value && password.value.text.isBlank()) {
|
||||||
accountStateViewModel.login(key.value.text, useProxy.value, proxyPort.value.toInt()) {
|
errorMessage = context.getString(R.string.password_is_required)
|
||||||
errorMessage = context.getString(R.string.invalid_key)
|
}
|
||||||
|
|
||||||
|
if (acceptedTerms.value && key.value.text.isNotBlank() && !(needsPassword.value && password.value.text.isBlank())) {
|
||||||
|
processingLogin = true
|
||||||
|
accountStateViewModel.login(key.value.text, password.value.text, useProxy.value, proxyPort.value.toInt()) {
|
||||||
|
processingLogin = false
|
||||||
|
errorMessage =
|
||||||
|
if (it != null) {
|
||||||
|
context.getString(R.string.invalid_key_with_message, it)
|
||||||
|
} else {
|
||||||
|
context.getString(R.string.invalid_key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -353,39 +390,128 @@ fun LoginPage(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
if (PackageUtils.isOrbotInstalled(context)) {
|
if (needsPassword.value) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
OutlinedTextField(
|
||||||
Checkbox(
|
modifier =
|
||||||
checked = useProxy.value,
|
Modifier
|
||||||
onCheckedChange = {
|
.onGloballyPositioned { coordinates ->
|
||||||
if (it) {
|
autofillNodePassword.boundingBox = coordinates.boundsInWindow()
|
||||||
connectOrbotDialogOpen = true
|
|
||||||
}
|
}
|
||||||
},
|
.onFocusChanged { focusState ->
|
||||||
)
|
autofill?.run {
|
||||||
|
if (focusState.isFocused) {
|
||||||
Text(stringResource(R.string.connect_via_tor))
|
requestAutofillForNode(autofillNodePassword)
|
||||||
}
|
} else {
|
||||||
|
cancelAutofillForNode(autofillNodePassword)
|
||||||
if (connectOrbotDialogOpen) {
|
}
|
||||||
ConnectOrbotDialog(
|
}
|
||||||
onClose = { connectOrbotDialogOpen = false },
|
},
|
||||||
onPost = {
|
value = password.value,
|
||||||
connectOrbotDialogOpen = false
|
onValueChange = {
|
||||||
useProxy.value = true
|
password.value = it
|
||||||
},
|
if (errorMessage.isNotEmpty()) {
|
||||||
onError = {
|
errorMessage = ""
|
||||||
scope.launch {
|
}
|
||||||
Toast.makeText(
|
},
|
||||||
context,
|
keyboardOptions =
|
||||||
it,
|
KeyboardOptions(
|
||||||
Toast.LENGTH_LONG,
|
autoCorrect = false,
|
||||||
|
keyboardType = KeyboardType.Password,
|
||||||
|
imeAction = ImeAction.Go,
|
||||||
|
),
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.ncryptsec_password),
|
||||||
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.show()
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
proxyPort,
|
},
|
||||||
)
|
visualTransformation =
|
||||||
|
if (showCharsPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
|
keyboardActions =
|
||||||
|
KeyboardActions(
|
||||||
|
onGo = {
|
||||||
|
if (!acceptedTerms.value) {
|
||||||
|
termsAcceptanceIsRequired = context.getString(R.string.acceptance_of_terms_is_required)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (acceptedTerms.value && key.value.text.isNotBlank() && !(needsPassword.value && password.value.text.isBlank())) {
|
||||||
|
processingLogin = true
|
||||||
|
accountStateViewModel.login(key.value.text, password.value.text, useProxy.value, proxyPort.value.toInt()) {
|
||||||
|
processingLogin = false
|
||||||
|
errorMessage =
|
||||||
|
if (it != null) {
|
||||||
|
context.getString(R.string.invalid_key_with_message, it)
|
||||||
|
} else {
|
||||||
|
context.getString(R.string.invalid_key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
if (PackageUtils.isOrbotInstalled(context)) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Checkbox(
|
||||||
|
checked = useProxy.value,
|
||||||
|
onCheckedChange = {
|
||||||
|
if (it) {
|
||||||
|
connectOrbotDialogOpen = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(stringResource(R.string.connect_via_tor))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectOrbotDialogOpen) {
|
||||||
|
ConnectOrbotDialog(
|
||||||
|
onClose = { connectOrbotDialogOpen = false },
|
||||||
|
onPost = {
|
||||||
|
connectOrbotDialogOpen = false
|
||||||
|
useProxy.value = true
|
||||||
|
},
|
||||||
|
onError = {
|
||||||
|
scope.launch {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
it,
|
||||||
|
Toast.LENGTH_LONG,
|
||||||
|
)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
proxyPort,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,19 +574,33 @@ fun LoginPage(
|
|||||||
errorMessage = context.getString(R.string.key_is_required)
|
errorMessage = context.getString(R.string.key_is_required)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (acceptedTerms.value && key.value.text.isNotBlank()) {
|
if (needsPassword.value && password.value.text.isBlank()) {
|
||||||
accountStateViewModel.login(key.value.text, useProxy.value, proxyPort.value.toInt()) {
|
errorMessage = context.getString(R.string.password_is_required)
|
||||||
errorMessage = context.getString(R.string.invalid_key)
|
}
|
||||||
|
|
||||||
|
if (acceptedTerms.value && key.value.text.isNotBlank() && !(needsPassword.value && password.value.text.isBlank())) {
|
||||||
|
processingLogin = true
|
||||||
|
accountStateViewModel.login(key.value.text, password.value.text, useProxy.value, proxyPort.value.toInt()) {
|
||||||
|
processingLogin = false
|
||||||
|
errorMessage =
|
||||||
|
if (it != null) {
|
||||||
|
context.getString(R.string.invalid_key_with_message, it)
|
||||||
|
} else {
|
||||||
|
context.getString(R.string.invalid_key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
shape = RoundedCornerShape(Size35dp),
|
shape = RoundedCornerShape(Size35dp),
|
||||||
modifier = Modifier.height(50.dp),
|
modifier = Modifier.height(50.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Row(modifier = Modifier.padding(horizontal = 40.dp)) {
|
||||||
text = stringResource(R.string.login),
|
if (processingLogin) {
|
||||||
modifier = Modifier.padding(horizontal = 40.dp),
|
LoadingAnimation()
|
||||||
)
|
Spacer(modifier = DoubleHorzSpacer)
|
||||||
|
}
|
||||||
|
Text(stringResource(R.string.login))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -142,12 +142,15 @@
|
|||||||
<string name="clear">Clear</string>
|
<string name="clear">Clear</string>
|
||||||
<string name="app_logo">App Logo</string>
|
<string name="app_logo">App Logo</string>
|
||||||
<string name="nsec_npub_hex_private_key">nsec.. or npub..</string>
|
<string name="nsec_npub_hex_private_key">nsec.. or npub..</string>
|
||||||
|
<string name="ncryptsec_password">password to open the key</string>
|
||||||
<string name="show_password">Show Password</string>
|
<string name="show_password">Show Password</string>
|
||||||
<string name="hide_password">Hide Password</string>
|
<string name="hide_password">Hide Password</string>
|
||||||
<string name="invalid_key">Invalid key</string>
|
<string name="invalid_key">Invalid key</string>
|
||||||
|
<string name="invalid_key_with_message">Invalid key: %1$s</string>
|
||||||
<string name="i_accept_the">"I accept the "</string>
|
<string name="i_accept_the">"I accept the "</string>
|
||||||
<string name="terms_of_use">terms of use</string>
|
<string name="terms_of_use">terms of use</string>
|
||||||
<string name="acceptance_of_terms_is_required">Acceptance of terms is required</string>
|
<string name="acceptance_of_terms_is_required">Acceptance of terms is required</string>
|
||||||
|
<string name="password_is_required">Password is required</string>
|
||||||
<string name="key_is_required">Key is required</string>
|
<string name="key_is_required">Key is required</string>
|
||||||
<string name="name_is_required">A name is required</string>
|
<string name="name_is_required">A name is required</string>
|
||||||
<string name="login">Login</string>
|
<string name="login">Login</string>
|
||||||
@@ -200,15 +203,22 @@
|
|||||||
<string name="mark_all_new_as_read">Mark all New as read</string>
|
<string name="mark_all_new_as_read">Mark all New as read</string>
|
||||||
<string name="mark_all_as_read">Mark all as read</string>
|
<string name="mark_all_as_read">Mark all as read</string>
|
||||||
<string name="backup_keys">Backup Keys</string>
|
<string name="backup_keys">Backup Keys</string>
|
||||||
<string name="account_backup_tips_md" tools:ignore="Typos">
|
<string name="account_backup_tips2_md" tools:ignore="Typos">
|
||||||
## Key Backup and Safety Tips
|
## Key Backup and Safety Tips
|
||||||
\n\nYour account is secured by a secret key. The key is long random string starting with **nsec1**. Anyone who has access to your secret key can publish content using your identity.
|
\n\nYour account is secured by a secret key. The key is a long sequence of characters starting with **nsec1**. Anyone who has access to this secret key can post and change your identity.
|
||||||
\n\n- Do **not** put your secret key in any website or software you do not trust.
|
\n\n- Do **not** put your secret key in any website or software you do not trust.
|
||||||
\n- Amethyst developers will **never** ask you for your secret key.
|
\n- Amethyst developers will **never** ask for your secret key.
|
||||||
\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="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="failed_to_encrypt_key">Failed to encrypt your private key</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="copy_my_secret_key">Copy my secret key</string>
|
||||||
|
<string name="encrypt_and_copy_my_secret_key">Encrypt and copy my secret key</string>
|
||||||
<string name="biometric_authentication_failed">Authentication failed</string>
|
<string name="biometric_authentication_failed">Authentication failed</string>
|
||||||
<string name="biometric_authentication_failed_explainer">Biometrics failed to authenticate the owner of this phone</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="biometric_authentication_failed_explainer_with_error">Biometrics failed to authenticate the owner of this phone. Error: %1$s</string>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
package com.vitorpamplona.quartz.crypto
|
package com.vitorpamplona.quartz.crypto
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.vitorpamplona.quartz.encoders.HexKey
|
||||||
import com.vitorpamplona.quartz.events.Event
|
import com.vitorpamplona.quartz.events.Event
|
||||||
import fr.acinq.secp256k1.Secp256k1
|
import fr.acinq.secp256k1.Secp256k1
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
@@ -34,6 +35,7 @@ object CryptoUtils {
|
|||||||
private val nip04 = Nip04(secp256k1, random)
|
private val nip04 = Nip04(secp256k1, random)
|
||||||
private val nip44v1 = Nip44v1(secp256k1, random)
|
private val nip44v1 = Nip44v1(secp256k1, random)
|
||||||
private val nip44v2 = Nip44v2(secp256k1, random)
|
private val nip44v2 = Nip44v2(secp256k1, random)
|
||||||
|
private val nip49 = Nip49(secp256k1, random)
|
||||||
|
|
||||||
fun clearCache() {
|
fun clearCache() {
|
||||||
nip04.clearCache()
|
nip04.clearCache()
|
||||||
@@ -262,6 +264,21 @@ object CryptoUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun decryptNIP49(
|
||||||
|
payload: String,
|
||||||
|
password: String,
|
||||||
|
): String? {
|
||||||
|
if (payload.isEmpty() || password.isEmpty()) return null
|
||||||
|
return nip49.decrypt(payload, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encryptNIP49(
|
||||||
|
key: HexKey,
|
||||||
|
password: String,
|
||||||
|
): String? {
|
||||||
|
return nip49.encrypt(key, password, 16, Nip49.EncryptedInfo.CLIENT_DOES_NOT_TRACK)
|
||||||
|
}
|
||||||
|
|
||||||
class EncryptedInfoString(
|
class EncryptedInfoString(
|
||||||
val ciphertext: String,
|
val ciphertext: String,
|
||||||
val nonce: String,
|
val nonce: String,
|
||||||
|
|||||||
@@ -39,14 +39,14 @@ class Nip49(val secp256k1: Secp256k1, val random: SecureRandom) {
|
|||||||
fun decrypt(
|
fun decrypt(
|
||||||
nCryptSec: String,
|
nCryptSec: String,
|
||||||
password: String,
|
password: String,
|
||||||
): HexKey? {
|
): HexKey {
|
||||||
return decrypt(EncryptedInfo.decodePayload(nCryptSec), password)
|
return decrypt(EncryptedInfo.decodePayload(nCryptSec), password)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun decrypt(
|
fun decrypt(
|
||||||
encryptedInfo: EncryptedInfo?,
|
encryptedInfo: EncryptedInfo?,
|
||||||
password: String = "",
|
password: String = "",
|
||||||
): String? {
|
): String {
|
||||||
check(encryptedInfo != null) { "Couldn't decode key" }
|
check(encryptedInfo != null) { "Couldn't decode key" }
|
||||||
check(encryptedInfo.version == EncryptedInfo.V) { "invalid version" }
|
check(encryptedInfo.version == EncryptedInfo.V) { "invalid version" }
|
||||||
|
|
||||||
@@ -66,6 +66,8 @@ class Nip49(val secp256k1: Secp256k1, val random: SecureRandom) {
|
|||||||
key,
|
key,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
check(m.any { it > 0 }) { "Incorrect password" }
|
||||||
|
|
||||||
return m.toHexKey()
|
return m.toHexKey()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,9 +131,9 @@ class Nip49(val secp256k1: Secp256k1, val random: SecureRandom) {
|
|||||||
companion object {
|
companion object {
|
||||||
const val V: Byte = 0x02
|
const val V: Byte = 0x02
|
||||||
|
|
||||||
const val UNSAFE_HANDLING = 0x00
|
const val UNSAFE_HANDLING: Byte = 0x00
|
||||||
const val SAFE_HANDLING = 0x01
|
const val SAFE_HANDLING: Byte = 0x01
|
||||||
const val CLIENT_DOES_NOT_TRACK = 0x02
|
const val CLIENT_DOES_NOT_TRACK: Byte = 0x02
|
||||||
|
|
||||||
fun decodePayload(nCryptSec: String): EncryptedInfo? {
|
fun decodePayload(nCryptSec: String): EncryptedInfo? {
|
||||||
val byteArray =
|
val byteArray =
|
||||||
|
|||||||
Reference in New Issue
Block a user