Add portable NIP-46 crypto + protocol parsing to :shared/commonMain

BunkerSigner wraps Quartz's NostrSignerInternal for sign/nip44/nip04
encrypt-decrypt; BunkerMethod/BunkerRequestPayload mirror Amber's
existing bunker-request method dispatch and per-method params layout
(app/src/main/java/.../service/BunkerRequestUtils.kt) as pure,
platform-agnostic functions with unit tests running on the JVM target.
This commit is contained in:
Claude
2026-07-01 17:49:29 +00:00
parent 438a782cb1
commit e6c81fabad
4 changed files with 147 additions and 0 deletions
@@ -0,0 +1,38 @@
package com.greenart7c3.nostrsigner.shared
/** The NIP-46 bunker RPC methods a signer can be asked to perform. */
enum class BunkerMethod {
CONNECT,
SIGN_EVENT,
NIP04_ENCRYPT,
NIP04_DECRYPT,
NIP44_ENCRYPT,
NIP44_DECRYPT,
NIP44_V3_ENCRYPT,
NIP44_V3_DECRYPT,
GET_PUBLIC_KEY,
DECRYPT_ZAP_EVENT,
PING,
SWITCH_RELAYS,
SIGN_PSBT,
LOGOUT,
INVALID,
}
fun bunkerMethodOf(method: String): BunkerMethod = when (method) {
"connect" -> BunkerMethod.CONNECT
"sign_event" -> BunkerMethod.SIGN_EVENT
"get_public_key" -> BunkerMethod.GET_PUBLIC_KEY
"nip04_encrypt" -> BunkerMethod.NIP04_ENCRYPT
"nip04_decrypt" -> BunkerMethod.NIP04_DECRYPT
"nip44_encrypt" -> BunkerMethod.NIP44_ENCRYPT
"nip44_decrypt" -> BunkerMethod.NIP44_DECRYPT
"nip44v3_encrypt" -> BunkerMethod.NIP44_V3_ENCRYPT
"nip44v3_decrypt" -> BunkerMethod.NIP44_V3_DECRYPT
"decrypt_zap_event" -> BunkerMethod.DECRYPT_ZAP_EVENT
"ping" -> BunkerMethod.PING
"switch_relays" -> BunkerMethod.SWITCH_RELAYS
"sign_psbt" -> BunkerMethod.SIGN_PSBT
"logout" -> BunkerMethod.LOGOUT
else -> BunkerMethod.INVALID
}
@@ -0,0 +1,40 @@
package com.greenart7c3.nostrsigner.shared
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest
/**
* Pure helpers for reading the fields a [BunkerRequest]'s NIP-46 `params` array carries,
* mirroring the NIP-46 spec's per-method param layout. No signing/encryption happens here —
* see [BunkerSigner] for that.
*/
object BunkerRequestPayload {
/** The event JSON (for `sign_event`), ciphertext/plaintext payload, or PSBT hex a request carries, if any. */
fun payload(bunkerRequest: BunkerRequest): String = when (bunkerMethodOf(bunkerRequest.method)) {
BunkerMethod.SIGN_EVENT -> bunkerRequest.params.firstOrNull() ?: ""
BunkerMethod.NIP04_ENCRYPT, BunkerMethod.NIP04_DECRYPT,
BunkerMethod.NIP44_ENCRYPT, BunkerMethod.NIP44_DECRYPT,
BunkerMethod.DECRYPT_ZAP_EVENT,
-> bunkerRequest.params.getOrElse(1) { "" }
// NIP-44 v3 NIP-46 layout: [pubkey, kind, scope, payload]. For
// `nip44v3_encrypt` the payload is base64-encoded plaintext; for
// `nip44v3_decrypt` it is the v3 ciphertext.
BunkerMethod.NIP44_V3_ENCRYPT, BunkerMethod.NIP44_V3_DECRYPT -> bunkerRequest.params.getOrElse(3) { "" }
BunkerMethod.SIGN_PSBT -> bunkerRequest.params.firstOrNull() ?: ""
else -> ""
}
/** The counterparty pubkey a NIP-04/NIP-44 encrypt/decrypt request targets, if any. */
fun counterpartyPubKey(bunkerRequest: BunkerRequest): String? = when (bunkerMethodOf(bunkerRequest.method)) {
BunkerMethod.NIP04_ENCRYPT, BunkerMethod.NIP04_DECRYPT,
BunkerMethod.NIP44_ENCRYPT, BunkerMethod.NIP44_DECRYPT,
BunkerMethod.NIP44_V3_ENCRYPT, BunkerMethod.NIP44_V3_DECRYPT,
-> bunkerRequest.params.firstOrNull()
else -> null
}
/** Extract `kind` from a NIP-44 v3 bunker request; null if missing/invalid. */
fun nip44v3Kind(bunkerRequest: BunkerRequest): Int? = bunkerRequest.params.getOrNull(1)?.toIntOrNull()
/** Extract `scope` from a NIP-44 v3 bunker request; defaults to empty per spec. */
fun nip44v3Scope(bunkerRequest: BunkerRequest): String = bunkerRequest.params.getOrElse(2) { "" }
}
@@ -0,0 +1,39 @@
package com.greenart7c3.nostrsigner.shared
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
class BunkerRequestPayloadTest {
@Test
fun signEventPayload() {
val request = BunkerRequest("id-1", "sign_event", arrayOf("{\"kind\":1}"))
assertEquals(BunkerMethod.SIGN_EVENT, bunkerMethodOf(request.method))
assertEquals("{\"kind\":1}", BunkerRequestPayload.payload(request))
}
@Test
fun nip44EncryptPayload() {
val request = BunkerRequest("id-2", "nip44_encrypt", arrayOf("deadbeef", "plaintext"))
assertEquals(BunkerMethod.NIP44_ENCRYPT, bunkerMethodOf(request.method))
assertEquals("plaintext", BunkerRequestPayload.payload(request))
assertEquals("deadbeef", BunkerRequestPayload.counterpartyPubKey(request))
}
@Test
fun nip44v3Payload() {
val request = BunkerRequest("id-3", "nip44v3_encrypt", arrayOf("deadbeef", "7", "scope", "cGF5bG9hZA=="))
assertEquals(BunkerMethod.NIP44_V3_ENCRYPT, bunkerMethodOf(request.method))
assertEquals("cGF5bG9hZA==", BunkerRequestPayload.payload(request))
assertEquals(7, BunkerRequestPayload.nip44v3Kind(request))
assertEquals("scope", BunkerRequestPayload.nip44v3Scope(request))
}
@Test
fun unknownMethodIsInvalid() {
val request = BunkerRequest("id-4", "not_a_real_method", arrayOf())
assertEquals(BunkerMethod.INVALID, bunkerMethodOf(request.method))
assertNull(BunkerRequestPayload.counterpartyPubKey(request))
}
}
@@ -0,0 +1,30 @@
package com.greenart7c3.nostrsigner.shared
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlinx.coroutines.test.runTest
class BunkerSignerTest {
@Test
fun nip44RoundTrip() = runTest {
val alice = BunkerSigner(KeyPair())
val bob = BunkerSigner(KeyPair())
val cipherText = alice.nip44Encrypt("hello bunker", bob.pubKey)
val plainText = bob.nip44Decrypt(cipherText, alice.pubKey)
assertEquals("hello bunker", plainText)
}
@Test
fun nip04RoundTrip() = runTest {
val alice = BunkerSigner(KeyPair())
val bob = BunkerSigner(KeyPair())
val cipherText = alice.nip04Encrypt("hello bunker nip04", bob.pubKey)
val plainText = bob.nip04Decrypt(cipherText, alice.pubKey)
assertEquals("hello bunker nip04", plainText)
}
}