Merge #ce483d5b: Fix nostrconnect parser corrupting param values that c…

Fix nostrconnect parser corrupting param values that contain =

nostr:nevent1qqsvujpatdel7f9l7cn0zzux8pwcpyfajuffs0ma3ajyfqcwh4s6x0gpz3mhxue69uhhyetvv9ujumn8d96zuer9wcy06jya

PR-Author: greenart7c3
nostr:npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5

PR description:

Param values such as base64 secrets with padding (XQUdha4rjh_gjAgHN8X5yg==) were split on every = and rejoined with joinToString's default ", " separator, so the connect response returned "XQUdha4rjh_gjAgHN8X5yg, , " instead of the original secret and NDK-based clients failed their secret check.

Params are now split on the first = only, so values containing = round-trip unchanged. Also joins the query-string pieces after ? without a separator, so unencoded ? characters no longer corrupt the query.

Adds unit tests for the splitParam helper, including the base64-padding regression case.

Fixes https://github.com/greenart7c3/Amber/issues/526
This commit is contained in:
greenart7c3
2026-09-25 07:04:32 -03:00
2 changed files with 67 additions and 6 deletions
@@ -18,6 +18,11 @@ import java.util.UUID
import kotlinx.coroutines.launch
object NostrConnectUtils {
/**
* Splits a query parameter into its name and value on the first `=` only,
* so values that contain `=` (e.g. base64 padding like "XQUdha4rjh_gjAgHN8X5yg==") are preserved.
*/
fun splitParam(param: String): Pair<String, String> = param.substringBefore("=") to param.substringAfter("=", "")
private fun metaDataFromJson(json: String): BunkerMetadata = BunkerMetadata.mapper.readValue(json, BunkerMetadata::class.java)
fun getIntentFromNostrConnect(
@@ -36,16 +41,12 @@ object NostrConnectUtils {
var url = ""
var image = ""
val pubKey = split.first()
val parsedData = IntentUtils.decodeData(split.drop(1).joinToString { it })
val parsedData = IntentUtils.decodeData(split.drop(1).joinToString("") { it })
val splitParsedData = parsedData.split("&")
val permissions = mutableListOf<Permission>()
var nostrConnectSecret = ""
splitParsedData.forEach {
val internalSplit = it.split("=")
val paramName = internalSplit.first()
val json = internalSplit.mapIndexedNotNull { index, s ->
if (index == 0) null else s
}.joinToString { data -> data }
val (paramName, json) = splitParam(it)
if (paramName == "relay") {
val relayUrl = RelayUrlNormalizer.normalizeOrNull(json)
if (relayUrl != null) {
@@ -0,0 +1,60 @@
package com.greenart7c3.nostrsigner.service
import org.junit.Assert.assertEquals
import org.junit.Test
class NostrConnectUtilsTest {
// --- values that contain `=` (issue #526) ---
@Test
fun `base64 secret with padding is preserved`() {
val (name, value) = NostrConnectUtils.splitParam("secret=XQUdha4rjh_gjAgHN8X5yg==")
assertEquals("secret", name)
assertEquals("XQUdha4rjh_gjAgHN8X5yg==", value)
}
@Test
fun `value with multiple equals signs is preserved`() {
val (name, value) = NostrConnectUtils.splitParam("secret=YQ==Zm9v")
assertEquals("secret", name)
assertEquals("YQ==Zm9v", value)
}
@Test
fun `url encoded relay value is not corrupted`() {
val (name, value) = NostrConnectUtils.splitParam("relay=wss%3A%2F%2Frelay.primal.net")
assertEquals("relay", name)
assertEquals("wss%3A%2F%2Frelay.primal.net", value)
}
@Test
fun `perms value is preserved`() {
val (name, value) = NostrConnectUtils.splitParam("perms=nip04_encrypt,nip44_decrypt,sign_event:1")
assertEquals("perms", name)
assertEquals("nip04_encrypt,nip44_decrypt,sign_event:1", value)
}
// --- degenerate inputs keep the previous behavior ---
@Test
fun `parameter without equals has empty value`() {
val (name, value) = NostrConnectUtils.splitParam("flag")
assertEquals("flag", name)
assertEquals("", value)
}
@Test
fun `parameter with empty value returns empty string`() {
val (name, value) = NostrConnectUtils.splitParam("secret=")
assertEquals("secret", name)
assertEquals("", value)
}
@Test
fun `empty parameter returns empty name and value`() {
val (name, value) = NostrConnectUtils.splitParam("")
assertEquals("", name)
assertEquals("", value)
}
}