fix(nostr): accept an uppercase npub, reject a mixed-case one

BIP-173 defines a bech32 string as all lowercase or all uppercase, and
declares mixed case invalid. The decoder only looked up lowercase
characters in the charset, so an all-uppercase npub - the form QR
encoders emit, because uppercase fits the denser alphanumeric mode -
failed with "Invalid character" before the checksum was ever checked.

Normalise case once, after rejecting a mixed-case string outright.
ContactIdentityResolver already worked around this by lowercasing at the
call site; NostrClient.sendPrivateMessage does not, so an npub that came
in uppercase could not be messaged.
This commit is contained in:
Taksh 2026-08-17 08:31:37 +05:30
parent b028270cc4
commit b542a1b874

View File

@ -22,14 +22,23 @@ object Bech32 {
/**
* Decode bech32 string
* Returns (hrp, data) pair
*
* Per BIP-173 a bech32 string is either all lowercase or all uppercase;
* the uppercase form is what QR encoders emit, since it fits the compact
* alphanumeric mode. Mixed case is invalid and is rejected.
*/
fun decode(bech32String: String): Pair<String, ByteArray> {
val separatorIndex = bech32String.lastIndexOf('1')
val hasLower = bech32String.any { it.isLowerCase() }
val hasUpper = bech32String.any { it.isUpperCase() }
require(!(hasLower && hasUpper)) { "Mixed case" }
val normalized = bech32String.lowercase()
val separatorIndex = normalized.lastIndexOf('1')
require(separatorIndex >= 0) { "No separator found" }
val hrp = bech32String.substring(0, separatorIndex)
val dataString = bech32String.substring(separatorIndex + 1)
val hrp = normalized.substring(0, separatorIndex)
val dataString = normalized.substring(separatorIndex + 1)
// Validate HRP contains only ASCII
require(hrp.all { it.code < 128 }) { "Invalid HRP characters" }