From b542a1b8740faeabffde04a04f5ba4dd1a5e6ecc Mon Sep 17 00:00:00 2001 From: Taksh Date: Mon, 17 Aug 2026 08:31:37 +0530 Subject: [PATCH] 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. --- .../java/com/bitchat/android/nostr/Bech32.kt | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/nostr/Bech32.kt b/app/src/main/java/com/bitchat/android/nostr/Bech32.kt index 3e55ab67..7ad59f57 100644 --- a/app/src/main/java/com/bitchat/android/nostr/Bech32.kt +++ b/app/src/main/java/com/bitchat/android/nostr/Bech32.kt @@ -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 { - 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" }