mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-09-19 04:59:59 +00:00
Merge 77fca90481b0d06db0c37bd6f768a53e9c8bfb07 into c127eb83ab94c069c32d37530d2faecd381cd2a8
This commit is contained in:
commit
081bee9c04
@ -22,14 +22,30 @@ 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')
|
||||
// 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" }
|
||||
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" }
|
||||
|
||||
|
||||
@ -0,0 +1,66 @@
|
||||
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
|
||||
|
||||
/**
|
||||
* BIP-173 allows a bech32 string to be all lowercase or all uppercase, and
|
||||
* forbids mixing the two. The uppercase form is what QR encoders emit — it fits
|
||||
* the alphanumeric mode, which is denser — so an npub scanned from a QR code
|
||||
* can arrive uppercased.
|
||||
*/
|
||||
class Bech32CaseTest {
|
||||
|
||||
private val pubkey = ByteArray(32) { (it + 1).toByte() }
|
||||
|
||||
@Test
|
||||
fun `uppercase decodes to the same payload as lowercase`() {
|
||||
val npub = Bech32.encode("npub", pubkey)
|
||||
val (lowerHrp, lowerData) = Bech32.decode(npub)
|
||||
val (upperHrp, upperData) = Bech32.decode(npub.uppercase())
|
||||
|
||||
assertEquals("npub", lowerHrp)
|
||||
assertEquals("npub", upperHrp)
|
||||
assertArrayEquals(pubkey, lowerData)
|
||||
assertArrayEquals(pubkey, upperData)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mixed case is rejected`() {
|
||||
val npub = Bech32.encode("npub", pubkey)
|
||||
val mixed = npub.substring(0, 6).uppercase() + npub.substring(6)
|
||||
|
||||
assertThrows(IllegalArgumentException::class.java) { Bech32.decode(mixed) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a corrupted uppercase string still fails the checksum`() {
|
||||
val npub = Bech32.encode("npub", pubkey).uppercase()
|
||||
val flipped = npub.dropLast(1) + if (npub.last() == 'Q') 'P' else 'Q'
|
||||
|
||||
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)
|
||||
val (hrp, data) = Bech32.decode(npub)
|
||||
assertEquals("npub", hrp)
|
||||
assertEquals(npub, Bech32.encode(hrp, data))
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user