mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-12 01:07:46 +00:00
fix(cashu): consistent prefix casing + reuse one CBOR codec
Audit follow-ups on the cashu token codec move: - CashuTokenB64Parser.parse() dispatches on the prefix case-insensitively (matching commons RichTextParser's case-insensitive cashuA/cashuB detection), but parseCashuA/parseCashuB stripped it with a case-sensitive removePrefix — so a mixed-case prefix passed dispatch and then fed its own prefix bytes into the Base64 decoder, failing to parse. Strip the fixed 6-char prefix with drop() so dispatch and stripping agree. - hoist a single shared CashuV4Cbor instance instead of allocating a new Cbor on every encode (V4Encoder) and every cashuB parse. Adds an acceptsMixedCasePrefix regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UMNKix4qEfiAPP9s2a4gB
This commit is contained in:
+8
-4
@@ -23,7 +23,6 @@ package com.vitorpamplona.quartz.nip60Cashu.token
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.cbor.Cbor
|
||||
import kotlinx.serialization.decodeFromByteArray
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
@@ -43,6 +42,9 @@ import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
* (one [CashuToken] per mint/keyset group).
|
||||
*/
|
||||
object CashuTokenB64Parser {
|
||||
/** Both "cashuA" and "cashuB" prefixes are 6 chars. */
|
||||
private const val PREFIX_LENGTH = 6
|
||||
|
||||
private val json =
|
||||
Json {
|
||||
ignoreUnknownKeys = true
|
||||
@@ -59,7 +61,9 @@ object CashuTokenB64Parser {
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
fun parseCashuA(token: String): List<CashuToken>? =
|
||||
try {
|
||||
val payload = token.removePrefix("cashuA")
|
||||
// drop() rather than removePrefix() so the case-insensitive
|
||||
// dispatch above stays consistent with prefix stripping.
|
||||
val payload = token.drop(PREFIX_LENGTH)
|
||||
val decoded =
|
||||
Base64.Default
|
||||
.withPadding(Base64.PaddingOption.PRESENT_OPTIONAL)
|
||||
@@ -88,12 +92,12 @@ object CashuTokenB64Parser {
|
||||
@OptIn(ExperimentalEncodingApi::class, ExperimentalSerializationApi::class)
|
||||
fun parseCashuB(token: String): List<CashuToken>? =
|
||||
try {
|
||||
val payload = token.removePrefix("cashuB")
|
||||
val payload = token.drop(PREFIX_LENGTH)
|
||||
val bytes =
|
||||
Base64.UrlSafe
|
||||
.withPadding(Base64.PaddingOption.PRESENT_OPTIONAL)
|
||||
.decode(payload)
|
||||
val parsed = Cbor { ignoreUnknownKeys = true }.decodeFromByteArray<V4Token>(bytes)
|
||||
val parsed = CashuV4Cbor.decodeFromByteArray<V4Token>(bytes)
|
||||
parsed.t?.map { group ->
|
||||
val keysetId = group.i.toHexKey()
|
||||
val proofs =
|
||||
|
||||
@@ -22,7 +22,6 @@ package com.vitorpamplona.quartz.nip60Cashu.token
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.cbor.Cbor
|
||||
import kotlinx.serialization.encodeToByteArray
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
@@ -72,11 +71,7 @@ object V4Encoder {
|
||||
t = groups.toTypedArray(),
|
||||
)
|
||||
|
||||
val cbor =
|
||||
Cbor {
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
val bytes = cbor.encodeToByteArray(token)
|
||||
val bytes = CashuV4Cbor.encodeToByteArray(token)
|
||||
val base64 = Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT).encode(bytes)
|
||||
return "cashuB$base64"
|
||||
}
|
||||
|
||||
@@ -23,6 +23,15 @@ package com.vitorpamplona.quartz.nip60Cashu.token
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.cbor.ByteString
|
||||
import kotlinx.serialization.cbor.Cbor
|
||||
|
||||
/**
|
||||
* Shared CBOR codec for the NUT-00 v4 (`cashuB`) wire format. Stateless and
|
||||
* thread-safe, so a single instance is reused by [V4Encoder] and
|
||||
* [CashuTokenB64Parser] instead of allocating one per call.
|
||||
*/
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
internal val CashuV4Cbor: Cbor = Cbor { ignoreUnknownKeys = true }
|
||||
|
||||
@Serializable
|
||||
class V4Token(
|
||||
|
||||
+11
@@ -125,4 +125,15 @@ class CashuTokenB64ParserTest {
|
||||
assertTrue(CashuTokenB64Parser.parse(cashuTokenA)!!.isNotEmpty())
|
||||
assertTrue(CashuTokenB64Parser.parse(cashuTokenB1)!!.isNotEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun acceptsMixedCasePrefix() {
|
||||
// parse() dispatches case-insensitively, so prefix stripping must too —
|
||||
// an upper-cased prefix should still decode the same payload.
|
||||
val upper = "CASHUB" + cashuTokenB1.drop(6)
|
||||
assertEquals(
|
||||
CashuTokenB64Parser.parse(cashuTokenB1)!!.flatMap { it.proofs }.map { it.secret },
|
||||
CashuTokenB64Parser.parse(upper)!!.flatMap { it.proofs }.map { it.secret },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user