Skip to content

Commit

Permalink
support for nip 49
Browse files Browse the repository at this point in the history
  • Loading branch information
greenart7c3 committed Feb 21, 2024
1 parent d79c0ff commit 2e5f00d
Show file tree
Hide file tree
Showing 5 changed files with 498 additions and 98 deletions.
88 changes: 44 additions & 44 deletions app/src/main/java/com/greenart7c3/nostrsigner/service/Biometrics.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -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)
}
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
Expand Down
144 changes: 130 additions & 14 deletions app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
}
},
Expand Down Expand Up @@ -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)
}
}
}
)
Expand All @@ -176,16 +206,100 @@ 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 = {
if (key.value.text.isBlank()) {
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)
Expand All @@ -201,6 +315,8 @@ fun LoginPage(
}
}

Spacer(modifier = Modifier.height(10.dp))

TextButton(
modifier = Modifier
.padding(30.dp)
Expand Down
Loading

0 comments on commit 2e5f00d

Please sign in to comment.