diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/NostrConnectUtils.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/NostrConnectUtils.kt index f488af34..84708b4e 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/NostrConnectUtils.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/NostrConnectUtils.kt @@ -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 = 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() 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) { diff --git a/app/src/test/java/com/greenart7c3/nostrsigner/service/NostrConnectUtilsTest.kt b/app/src/test/java/com/greenart7c3/nostrsigner/service/NostrConnectUtilsTest.kt new file mode 100644 index 00000000..42748c92 --- /dev/null +++ b/app/src/test/java/com/greenart7c3/nostrsigner/service/NostrConnectUtilsTest.kt @@ -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) + } +}