mirror of
https://github.com/block/buzz.git
synced 2026-10-29 05:21:31 +00:00
🤖 ## Summary When an iOS push notification comes from someone the app has no cached name for, the notification title showed the first characters of the sender's raw public key — for example `aa4fc866…`. That fragment is unreadable and doesn't match how the same person appears anywhere else in Buzz. This PR changes that title to the compact form of the sender's npub (npub is the human-readable encoding of a Nostr public key): first 8 and last 4 characters — for example `npub14f8…9nsy`, the same identity shape used across the desktop and mobile apps. - Unnamed senders: raw hex fragment → compact npub. - Named senders: unchanged — a sender the app has a display name for still titles the notification with that name. - Unverifiable sender identities (malformed keys, or lookalike strings that are not literal 64-hex-digit keys) now render a neutral "Someone" instead of partial raw key material. - Everything else about the notification is unchanged: body text, subtitle, thread matching and grouping, deep-link navigation, thread identifiers, and the internal hex public key the resolver matches on. The native iOS notification-service package (`BuzzPushKit`) gains a minimal in-house bech32 codec (bech32 is the checksummed string encoding npubs use) — checksum-validated, 32-byte keys only, and no new external dependency. The hex input branch accepts exactly a 64 ASCII hex digit key before any parsing, so strings that merely parse like hex (for example a run of `+a` pairs) cannot become a displayed identity; this is input validation for presentation. Event signature verification is untouched. ### Related issue Fixes: N/A. Searched existing issues/PRs for push-notification npub identity — closest related: none found. ### Testing At head `3e3f2813b8864b76257ccb50dea3a4b31fa4de0d` (base `44316ff72f5f7de014c66b01cbf534298a70c249`; 4 files, +321/−4): - CI `Mobile Swift` lane, at this exact head — all passed: `swift test` (73 tests, 0 failures), the SwiftPM debug and release builds of `mobile/ios/BuzzPushKit`, and the unsigned iOS release build. - Test coverage: npub encoding cross-checked against independent nostr-rs/NIP-19 vectors; rejection of bad checksums, mixed case, wrong lengths, invalid alphabet, padding, and non-32-byte payloads; resolver boundary matrix — hex/npub/invalid sender keys render compact npub or "Someone" while body, subtitle, sender key, and thread identifier pass through; named senders keep cached display names. ### Task provenance Buzz channel: `1f0e4a3d-7e01-4efe-bb16-843b357f85c9` Task: buzz://message?channel=1f0e4a3d-7e01-4efe-bb16-843b357f85c9&id=86b34eb4bd84a1472419e9af22636c011c0fe273e3c196f967d7a36996e149b6 --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
99 lines
4.5 KiB
Swift
99 lines
4.5 KiB
Swift
import XCTest
|
|
@testable import BuzzPushKit
|
|
|
|
/// Bech32 codec tests against independently published npub vectors — the
|
|
/// NIP-19 spec example key and the nostr crate 0.44 test suite — plus
|
|
/// degenerate key material for the padding and payload-length boundaries.
|
|
/// Generic BIP-173 conformance is not asserted here: nothing outside the
|
|
/// codec calls raw decode, so alphabet, checksum, case, and length
|
|
/// rejection are pinned at canonicalNpub/npubBytes, the npub seam Buzz
|
|
/// actually uses.
|
|
final class Bech32Tests: XCTestCase {
|
|
/// Hex public keys with their published npub equivalents.
|
|
static let npubVectors: [(hex: String, npub: String)] = [
|
|
// nostr-rs 0.44 key test: aa4fc866… ↔ npub14f8usejl…qqh9nsy.
|
|
(
|
|
"aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4",
|
|
"npub14f8usejl26twx0dhuxjh9cas7keav9vr0v8nvtwtrjqx3vycc76qqh9nsy"
|
|
),
|
|
// The NIP-19 spec's example profile key.
|
|
(
|
|
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
|
|
"npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6"
|
|
),
|
|
// Degenerate key material still encodes (and exercises 5-bit padding).
|
|
(String(repeating: "00", count: 32), "npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzqujme"),
|
|
]
|
|
|
|
func testNpubEncodingMatchesKnownVectors() throws {
|
|
for vector in Self.npubVectors {
|
|
let bytes = try XCTUnwrap(VerifiedNostrEvent.hexBytes(vector.hex))
|
|
XCTAssertEqual(bytes.count, 32)
|
|
XCTAssertEqual(Bech32.npub(from: bytes), vector.npub, "hex: \(vector.hex)")
|
|
XCTAssertEqual(Bech32.canonicalNpub(from: vector.hex), vector.npub, "hex: \(vector.hex)")
|
|
}
|
|
}
|
|
|
|
func testCanonicalNpubValidatesAndCanonicalizesNpubInputs() throws {
|
|
for vector in Self.npubVectors {
|
|
let npub = try XCTUnwrap(Bech32.canonicalNpub(from: vector.npub))
|
|
XCTAssertEqual(npub, vector.npub, "npub: \(vector.npub)")
|
|
XCTAssertEqual(
|
|
Bech32.npubBytes(from: npub).map(VerifiedNostrEvent.hex), vector.hex,
|
|
"npub: \(vector.npub)")
|
|
}
|
|
// Uppercase bech32 is valid per BIP-173; canonical output is lowercase.
|
|
XCTAssertEqual(
|
|
Bech32.canonicalNpub(
|
|
from: "NPUB14F8USEJL26TWX0DHUXJH9CAS7KEAV9VR0V8NVTWTRJQX3VYCC76QQH9NSY"),
|
|
"npub14f8usejl26twx0dhuxjh9cas7keav9vr0v8nvtwtrjqx3vycc76qqh9nsy")
|
|
// Letter case within a hex key stays valid input; digits are caseless,
|
|
// so mixed-case letters still canonicalize to the lowercase npub.
|
|
let mixedCaseHex = String(
|
|
Self.npubVectors[0].hex.enumerated().map {
|
|
$0.offset.isMultiple(of: 2) ? $0.element : Character($0.element.uppercased())
|
|
})
|
|
XCTAssertEqual(Bech32.canonicalNpub(from: mixedCaseHex), Self.npubVectors[0].npub)
|
|
}
|
|
|
|
func testCanonicalNpubRejectsInvalidAndLookalikeKeys() {
|
|
let rejected = [
|
|
// Junk and empty payloads.
|
|
"",
|
|
"author-pubkey",
|
|
String(repeating: "a", count: 63),
|
|
"0",
|
|
// Hex that is not a 32-byte key.
|
|
String(repeating: "ab", count: 31),
|
|
String(repeating: "ab", count: 33),
|
|
// Signed radix-16 chunks ("+a"→10, "-0"→0) parse via UInt8(_:radix:)
|
|
// but are not literal ASCII hex digits, so never 32-byte keys.
|
|
String(repeating: "+a", count: 32),
|
|
String(repeating: "+A", count: 32),
|
|
String(repeating: "-0", count: 32),
|
|
// Npub-shaped payload with a character outside the bech32 alphabet
|
|
// ("b" never appears in the charset).
|
|
"npub1bqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzqujme",
|
|
// Valid npub with the checksum's final character mutated.
|
|
"npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzqujma",
|
|
// Mixed case is never valid bech32.
|
|
"Npub14f8usejl26twx0dhuxjh9cas7keav9vr0v8nvtwtrjqx3vycc76qqh9nsy",
|
|
// Valid checksum but the wrong payload length for a key.
|
|
"npub1qqqqqqqqqqqqqqqqqqqqqqqqqqk7h3rf",
|
|
"npub1llllllllllllllllllllllllllllllllllllllllllllllllllll7w6tc2n",
|
|
// Valid bech32 under a different human-readable part.
|
|
"nsec1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqwkhnav",
|
|
]
|
|
for string in rejected {
|
|
XCTAssertNil(Bech32.canonicalNpub(from: string), "expected rejection: \(string)")
|
|
XCTAssertNil(Bech32.npubBytes(from: string), "expected rejection: \(string)")
|
|
}
|
|
}
|
|
|
|
func testNpubRejectsNon32ByteKeys() {
|
|
XCTAssertNil(Bech32.npub(from: [UInt8](repeating: 0, count: 16)))
|
|
XCTAssertNil(Bech32.npub(from: [UInt8](repeating: 0xff, count: 33)))
|
|
XCTAssertNil(Bech32.npub(from: []))
|
|
}
|
|
}
|