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 7ad59f57..21d0dbfb 100644 --- a/app/src/main/java/com/bitchat/android/nostr/Bech32.kt +++ b/app/src/main/java/com/bitchat/android/nostr/Bech32.kt @@ -28,6 +28,13 @@ object Bech32 { * alphanumeric mode. Mixed case is invalid and is rejected. */ fun decode(bech32String: String): Pair { + // BIP-173 restricts every character to printable US-ASCII, 33..126. + // This has to run before the case test and before lowercase(): a + // non-ASCII uppercase homoglyph such as U+212A KELVIN SIGN reads as + // uppercase, so an otherwise all-uppercase string carrying one shows no + // mixed case, and lowercase() then folds it into an ASCII 'k'. + require(bech32String.all { it.code in 33..126 }) { "Invalid character" } + val hasLower = bech32String.any { it.isLowerCase() } val hasUpper = bech32String.any { it.isUpperCase() } require(!(hasLower && hasUpper)) { "Mixed case" } diff --git a/app/src/test/java/com/bitchat/android/nostr/Bech32CaseTest.kt b/app/src/test/java/com/bitchat/android/nostr/Bech32CaseTest.kt index 1f66c593..ffad6987 100644 --- a/app/src/test/java/com/bitchat/android/nostr/Bech32CaseTest.kt +++ b/app/src/test/java/com/bitchat/android/nostr/Bech32CaseTest.kt @@ -3,6 +3,7 @@ package com.bitchat.android.nostr import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertThrows +import org.junit.Assume.assumeTrue import org.junit.Test /** @@ -43,6 +44,18 @@ class Bech32CaseTest { assertThrows(IllegalArgumentException::class.java) { Bech32.decode(flipped) } } + @Test + fun `a non-ascii homoglyph is rejected rather than folded to ascii`() { + val npub = Bech32.encode("npub", pubkey).uppercase() + // U+212A KELVIN SIGN is uppercase and lowercases to an ASCII 'k', so an + // all-uppercase string carrying one passes a naive mixed-case test and + // then folds into a perfectly valid npub. + val kelvin = npub.replace("K", "\u212A") + assumeTrue(kelvin != npub) + + assertThrows(IllegalArgumentException::class.java) { Bech32.decode(kelvin) } + } + @Test fun `round trip through encode and decode is stable`() { val npub = Bech32.encode("npub", pubkey)