diff --git a/shared/src/commonMain/kotlin/com/greenart7c3/nostrsigner/shared/BunkerMethod.kt b/shared/src/commonMain/kotlin/com/greenart7c3/nostrsigner/shared/BunkerMethod.kt new file mode 100644 index 00000000..bd855d39 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/greenart7c3/nostrsigner/shared/BunkerMethod.kt @@ -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 +} diff --git a/shared/src/commonMain/kotlin/com/greenart7c3/nostrsigner/shared/BunkerRequestPayload.kt b/shared/src/commonMain/kotlin/com/greenart7c3/nostrsigner/shared/BunkerRequestPayload.kt new file mode 100644 index 00000000..0c2892d3 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/greenart7c3/nostrsigner/shared/BunkerRequestPayload.kt @@ -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) { "" } +} diff --git a/shared/src/commonTest/kotlin/com/greenart7c3/nostrsigner/shared/BunkerRequestPayloadTest.kt b/shared/src/commonTest/kotlin/com/greenart7c3/nostrsigner/shared/BunkerRequestPayloadTest.kt new file mode 100644 index 00000000..cc7f0037 --- /dev/null +++ b/shared/src/commonTest/kotlin/com/greenart7c3/nostrsigner/shared/BunkerRequestPayloadTest.kt @@ -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)) + } +} diff --git a/shared/src/commonTest/kotlin/com/greenart7c3/nostrsigner/shared/BunkerSignerTest.kt b/shared/src/commonTest/kotlin/com/greenart7c3/nostrsigner/shared/BunkerSignerTest.kt new file mode 100644 index 00000000..d00c0717 --- /dev/null +++ b/shared/src/commonTest/kotlin/com/greenart7c3/nostrsigner/shared/BunkerSignerTest.kt @@ -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) + } +}