Merge pull request #691 from greenart7c3/test_external_signer

Fixes/improvements to external signer
This commit is contained in:
Vitor Pamplona
2023-11-22 08:44:46 -05:00
committed by GitHub
7 changed files with 116 additions and 51 deletions
@@ -26,9 +26,11 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.hexToByteArray import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.encoders.toNpub
import com.vitorpamplona.quartz.events.ContactListEvent import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.LnZapEvent import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.signers.ExternalSignerLauncher
import com.vitorpamplona.quartz.signers.NostrSignerExternal import com.vitorpamplona.quartz.signers.NostrSignerExternal
import com.vitorpamplona.quartz.signers.NostrSignerInternal import com.vitorpamplona.quartz.signers.NostrSignerInternal
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -89,6 +91,7 @@ private object PrefKeys {
const val AUTOMATICALLY_HIDE_NAV_BARS = "automatically_hide_nav_bars" const val AUTOMATICALLY_HIDE_NAV_BARS = "automatically_hide_nav_bars"
const val LOGIN_WITH_EXTERNAL_SIGNER = "login_with_external_signer" const val LOGIN_WITH_EXTERNAL_SIGNER = "login_with_external_signer"
const val AUTOMATICALLY_SHOW_PROFILE_PICTURE = "automatically_show_profile_picture" const val AUTOMATICALLY_SHOW_PROFILE_PICTURE = "automatically_show_profile_picture"
const val SIGNER_PACKAGE_NAME = "signer_package_name"
const val ALL_ACCOUNT_INFO = "all_saved_accounts_info" const val ALL_ACCOUNT_INFO = "all_saved_accounts_info"
const val SHARED_SETTINGS = "shared_settings" const val SHARED_SETTINGS = "shared_settings"
@@ -249,6 +252,7 @@ object LocalPreferences {
putBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, account.signer is NostrSignerExternal) putBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, account.signer is NostrSignerExternal)
if (account.signer is NostrSignerExternal) { if (account.signer is NostrSignerExternal) {
remove(PrefKeys.NOSTR_PRIVKEY) remove(PrefKeys.NOSTR_PRIVKEY)
putString(PrefKeys.SIGNER_PACKAGE_NAME, account.signer.launcher.signerPackageName)
} else { } else {
account.keyPair.privKey?.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHexKey()) } account.keyPair.privKey?.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHexKey()) }
} }
@@ -475,7 +479,8 @@ object LocalPreferences {
val keyPair = KeyPair(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()) val keyPair = KeyPair(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray())
val signer = if (loginWithExternalSigner) { val signer = if (loginWithExternalSigner) {
NostrSignerExternal(pubKey) val packageName = getString(PrefKeys.SIGNER_PACKAGE_NAME, null) ?: "com.greenart7c3.nostrsigner"
NostrSignerExternal(pubKey, ExternalSignerLauncher(pubKey.hexToByteArray().toNpub(), packageName))
} else { } else {
NostrSignerInternal(keyPair) NostrSignerInternal(keyPair)
} }
@@ -49,6 +49,8 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.net.URLEncoder import java.net.URLEncoder
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.util.Timer
import kotlin.concurrent.schedule
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private val isOnMobileDataState = mutableStateOf(false) private val isOnMobileDataState = mutableStateOf(false)
@@ -108,6 +110,7 @@ class MainActivity : AppCompatActivity() {
DefaultMutedSetting.value = true DefaultMutedSetting.value = true
// Keep connection alive if it's calling the signer app // Keep connection alive if it's calling the signer app
Log.d("shouldPauseService", "shouldPauseService onResume: $shouldPauseService")
if (shouldPauseService) { if (shouldPauseService) {
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
serviceManager.justStart() serviceManager.justStart()
@@ -121,7 +124,9 @@ class MainActivity : AppCompatActivity() {
(getSystemService(ConnectivityManager::class.java) as ConnectivityManager).registerDefaultNetworkCallback(networkCallback) (getSystemService(ConnectivityManager::class.java) as ConnectivityManager).registerDefaultNetworkCallback(networkCallback)
// resets state until next External Signer Call // resets state until next External Signer Call
shouldPauseService = true Timer().schedule(350) {
shouldPauseService = true
}
} }
override fun onPause() { override fun onPause() {
@@ -134,6 +139,7 @@ class MainActivity : AppCompatActivity() {
} }
// } // }
Log.d("shouldPauseService", "shouldPauseService onPause: $shouldPauseService")
if (shouldPauseService) { if (shouldPauseService) {
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
serviceManager.pauseForGood() serviceManager.pauseForGood()
@@ -171,8 +177,11 @@ class MainActivity : AppCompatActivity() {
override fun onAvailable(network: Network) { override fun onAvailable(network: Network) {
super.onAvailable(network) super.onAvailable(network)
GlobalScope.launch(Dispatchers.IO) { Log.d("shouldPauseService", "shouldPauseService onAvailable: $shouldPauseService")
serviceManager.forceRestart() if (shouldPauseService) {
GlobalScope.launch(Dispatchers.IO) {
serviceManager.forceRestart()
}
} }
} }
@@ -202,7 +211,8 @@ class MainActivity : AppCompatActivity() {
changedNetwork = true changedNetwork = true
} }
if (changedNetwork) { Log.d("shouldPauseService", "shouldPauseService onCapabilitiesChanged: $shouldPauseService")
if (changedNetwork && shouldPauseService) {
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
serviceManager.forceRestart() serviceManager.forceRestart()
} }
@@ -34,6 +34,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
@@ -170,6 +171,7 @@ fun InvoiceRequest(
) )
} else { } else {
account.createZapRequestFor(toUserPubKeyHex, message, account.defaultZapType) { zapRequest -> account.createZapRequestFor(toUserPubKeyHex, message, account.defaultZapType) { zapRequest ->
LocalCache.justConsume(zapRequest, null)
LightningAddressResolver().lnAddressInvoice( LightningAddressResolver().lnAddressInvoice(
lud16, lud16,
amount * 1000, amount * 1000,
@@ -14,6 +14,8 @@ import com.vitorpamplona.quartz.encoders.Nip19
import com.vitorpamplona.quartz.encoders.bechToBytes import com.vitorpamplona.quartz.encoders.bechToBytes
import com.vitorpamplona.quartz.encoders.hexToByteArray import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.encoders.toNpub
import com.vitorpamplona.quartz.signers.ExternalSignerLauncher
import com.vitorpamplona.quartz.signers.NostrSignerExternal import com.vitorpamplona.quartz.signers.NostrSignerExternal
import com.vitorpamplona.quartz.signers.NostrSignerInternal import com.vitorpamplona.quartz.signers.NostrSignerInternal
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
@@ -56,7 +58,8 @@ class AccountStateViewModel() : ViewModel() {
key: String, key: String,
useProxy: Boolean, useProxy: Boolean,
proxyPort: Int, proxyPort: Int,
loginWithExternalSigner: Boolean = false loginWithExternalSigner: Boolean = false,
packageName: String = ""
) = withContext(Dispatchers.IO) { ) = withContext(Dispatchers.IO) {
val parsed = Nip19.uriToRoute(key) val parsed = Nip19.uriToRoute(key)
val pubKeyParsed = parsed?.hex?.hexToByteArray() val pubKeyParsed = parsed?.hex?.hexToByteArray()
@@ -69,7 +72,8 @@ class AccountStateViewModel() : ViewModel() {
val account = val account =
if (loginWithExternalSigner) { if (loginWithExternalSigner) {
val keyPair = KeyPair(pubKey = pubKeyParsed) val keyPair = KeyPair(pubKey = pubKeyParsed)
Account(keyPair, proxy = proxy, proxyPort = proxyPort, signer = NostrSignerExternal(keyPair.pubKey.toHexKey())) val localPackageName = packageName.ifBlank { "com.greenart7c3.nostrsigner" }
Account(keyPair, proxy = proxy, proxyPort = proxyPort, signer = NostrSignerExternal(keyPair.pubKey.toHexKey(), ExternalSignerLauncher(keyPair.pubKey.toNpub(), localPackageName)))
} else if (key.startsWith("nsec")) { } else if (key.startsWith("nsec")) {
val keyPair = KeyPair(privKey = key.bechToBytes()) val keyPair = KeyPair(privKey = key.bechToBytes())
Account(keyPair, proxy = proxy, proxyPort = proxyPort, signer = NostrSignerInternal(keyPair)) Account(keyPair, proxy = proxy, proxyPort = proxyPort, signer = NostrSignerInternal(keyPair))
@@ -130,11 +134,12 @@ class AccountStateViewModel() : ViewModel() {
useProxy: Boolean, useProxy: Boolean,
proxyPort: Int, proxyPort: Int,
loginWithExternalSigner: Boolean = false, loginWithExternalSigner: Boolean = false,
packageName: String = "",
onError: () -> Unit onError: () -> Unit
) { ) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
loginAndStartUI(key, useProxy, proxyPort, loginWithExternalSigner) loginAndStartUI(key, useProxy, proxyPort, loginWithExternalSigner, packageName)
} catch (e: Exception) { } catch (e: Exception) {
Log.e("Login", "Could not sign in", e) Log.e("Login", "Could not sign in", e)
onError() onError()
@@ -108,7 +108,7 @@ fun LoginPage(
var loginWithExternalSigner by remember { mutableStateOf(false) } var loginWithExternalSigner by remember { mutableStateOf(false) }
if (loginWithExternalSigner) { if (loginWithExternalSigner) {
val externalSignerLauncher = remember { ExternalSignerLauncher("") } val externalSignerLauncher = remember { ExternalSignerLauncher("", signerPackageName = "") }
val id = remember { UUID.randomUUID().toString() } val id = remember { UUID.randomUUID().toString() }
val launcher = rememberLauncherForActivityResult( val launcher = rememberLauncherForActivityResult(
@@ -162,8 +162,11 @@ fun LoginPage(
SignerType.GET_PUBLIC_KEY, SignerType.GET_PUBLIC_KEY,
"", "",
id id
) { pubkey -> ) { result ->
println("AAAA- COME BACK") println("AAAA- COME BACK")
val split = result.split("-")
val pubkey = split.first()
val packageName = if (split.size > 1) split[1] else ""
key.value = TextFieldValue(pubkey) key.value = TextFieldValue(pubkey)
if (!acceptedTerms.value) { if (!acceptedTerms.value) {
termsAcceptanceIsRequired = termsAcceptanceIsRequired =
@@ -175,7 +178,7 @@ fun LoginPage(
} }
if (acceptedTerms.value && key.value.text.isNotBlank()) { if (acceptedTerms.value && key.value.text.isNotBlank()) {
accountViewModel.login(key.value.text, useProxy.value, proxyPort.value.toInt(), true) { accountViewModel.login(key.value.text, useProxy.value, proxyPort.value.toInt(), true, packageName) {
errorMessage = context.getString(R.string.invalid_key) errorMessage = context.getString(R.string.invalid_key)
} }
} }
@@ -432,33 +435,8 @@ fun LoginPage(
return@Button return@Button
} }
val result = ExternalSignerLauncher("").getDataFromResolver( loginWithExternalSigner = true
SignerType.GET_PUBLIC_KEY, return@Button
arrayOf("login"),
contentResolver = Amethyst.instance.contentResolver
)
if (result == null) {
loginWithExternalSigner = true
return@Button
} else {
key.value = TextFieldValue(result)
if (key.value.text.isBlank()) {
errorMessage = context.getString(R.string.key_is_required)
return@Button
}
if (acceptedTerms.value && key.value.text.isNotBlank()) {
accountViewModel.login(
key.value.text,
useProxy.value,
proxyPort.value.toInt(),
true
) {
errorMessage = context.getString(R.string.invalid_key)
}
}
}
}, },
shape = RoundedCornerShape(Size35dp), shape = RoundedCornerShape(Size35dp),
modifier = Modifier modifier = Modifier
@@ -19,9 +19,18 @@ enum class SignerType {
DECRYPT_ZAP_EVENT DECRYPT_ZAP_EVENT
} }
class Permission(
val type: String,
val kind: Int? = null
) {
fun toJson(): String {
return "{\"type\":\"${type}\",\"kind\":${kind}}"
}
}
class ExternalSignerLauncher( class ExternalSignerLauncher(
private val npub: String, private val npub: String,
private val signerPackageName: String = "com.greenart7c3.nostrsigner" val signerPackageName: String = "com.greenart7c3.nostrsigner"
) { ) {
private val contentCache = LruCache<String, (String) -> Unit>(20) private val contentCache = LruCache<String, (String) -> Unit>(20)
@@ -49,9 +58,11 @@ class ExternalSignerLauncher(
fun newResult(data: Intent) { fun newResult(data: Intent) {
val signature = data.getStringExtra("signature") ?: "" val signature = data.getStringExtra("signature") ?: ""
val packageName = data.getStringExtra("package") ?: ""
val id = data.getStringExtra("id") ?: "" val id = data.getStringExtra("id") ?: ""
if (id.isNotBlank()) { if (id.isNotBlank()) {
contentCache.get(id)?.invoke(signature) val result = if (packageName.isNotBlank()) "$signature-$packageName" else signature
contentCache.get(id)?.invoke(result)
} }
} }
@@ -70,6 +81,40 @@ class ExternalSignerLauncher(
} }
} }
private fun defaultPermissions(): String {
val permissions = listOf(
Permission(
"sign_event",
22242
),
Permission(
"nip04_encrypt"
),
Permission(
"nip04_decrypt"
),
Permission(
"nip44_encrypt"
),
Permission(
"nip44_decrypt"
),
Permission(
"decrypt_zap_event"
),
)
val jsonArray = StringBuilder("[")
permissions.forEachIndexed { index, permission ->
jsonArray.append(permission.toJson())
if (index < permissions.size - 1) {
jsonArray.append(",")
}
}
jsonArray.append("]")
return jsonArray.toString()
}
private fun openSignerApp( private fun openSignerApp(
data: String, data: String,
type: SignerType, type: SignerType,
@@ -93,8 +138,12 @@ class ExternalSignerLauncher(
intent.putExtra("id", id) intent.putExtra("id", id)
if (type !== SignerType.GET_PUBLIC_KEY) { if (type !== SignerType.GET_PUBLIC_KEY) {
intent.putExtra("current_user", npub) intent.putExtra("current_user", npub)
} else {
intent.putExtra("permissions", defaultPermissions())
}
if (signerPackageName.isNotBlank()) {
intent.`package` = signerPackageName
} }
intent.`package` = signerPackageName
contentCache.put(id, onReady) contentCache.put(id, onReady)
@@ -34,17 +34,33 @@ class NostrSignerExternal(
) )
launcher.openSigner(event) { signature -> launcher.openSigner(event) { signature ->
(EventFactory.create( if (signature.startsWith("{")) {
event.id, val localEvent = Event.fromJson(signature)
event.pubKey, (EventFactory.create(
event.createdAt, localEvent.id,
event.kind, localEvent.pubKey,
event.tags, localEvent.createdAt,
event.content, localEvent.kind,
signature localEvent.tags,
) as? T?)?.let { localEvent.content,
onReady(it) localEvent.sig
) as? T?)?.let {
onReady(it)
}
} else {
(EventFactory.create(
event.id,
event.pubKey,
event.createdAt,
event.kind,
event.tags,
event.content,
signature
) as? T?)?.let {
onReady(it)
}
} }
} }
} }