Skip to content

fix: reject a signed byte pair in Data(hexString:) - #1687

Open
Chessing234 wants to merge 2 commits into
permissionlesstech:mainfrom
Chessing234:fix/hex-string-rejects-sign
Open

fix: reject a signed byte pair in Data(hexString:)#1687
Chessing234 wants to merge 2 commits into
permissionlesstech:mainfrom
Chessing234:fix/hex-string-rejects-sign

Conversation

@Chessing234

Copy link
Copy Markdown
Contributor

Data(hexString:) chunks the string into two-character pairs and hands each to UInt8(_:radix:). That initializer accepts a leading sign, so "+f" parses as 0x0f. Its own doc comment promises nil for a string that "contains invalid hex characters", and + is one.

Every identity-bearing hex string in the app goes through this parser — Noise keys, Nostr pubkeys, group member fingerprints, signatures, routing IDs — so each one had a second spelling that decodes to the same bytes.

PeerID is where that shows up most sharply. A 64-character id built from "+b" pairs:

isValid       = true
isHex         = false      <- disagrees with the line below
isNoiseKeyHex = true
noiseKey      = 0b0b0b…0b  <- byte-identical to the genuine peer
toShort()     = f0e38b830ebd8a50  <- same short routing ID

isShort validates the 16-hex form with isHexDigit and rejects + correctly; isNoiseKeyHex validates the 64-hex form by asking whether Data(hexString:) returns non-nil, so the two halves of the same check disagreed. PeerID equality and hashing are on the string, so the spoofed id is a distinct key in any dictionary while resolving to the same cryptographic identity.

Fix is to require two hex digits before parsing the pair. Everything the parser is documented to accept still parses: plain, uppercase, 0x/0X-prefixed, whitespace-padded, and every byte value round-trips.

I did not widen this beyond the parser. PeerID.isValid needs no change once Data(hexString:) keeps its contract, and I would rather not touch the validity rules in the same diff.

Evidence

No Xcode on this machine, so I could not run bitchatTests or build the app. What I did instead: built BitFoundation with swift build and linked its object files into a standalone harness, so the assertions below ran against the real compiled module rather than a stub.

Before the fix, the harness reproduced the table above — +b×32 and 0b×32 are distinct PeerID values that both pass isValid and produce the identical 32-byte key and identical toShort(). After it, the spoofed id returns nil from noiseKey, isValid is false, isHex and isNoiseKeyHex agree, and the genuine id is untouched.

I then ran all 22 assertions from the two test files through that harness; they pass. The tests themselves are swift-testing, and the Testing module needs the Xcode toolchain, so I have not executed DataHexTests or PeerIDTests as tests — CI is the first thing that will. The assertions inside them are the ones I ran; the @Test/#expect scaffolding is copied from the sibling files.

swift build --package-path localPackages/BitFoundation is clean.

Related but not a duplicate: #911 hardened the same initializer for odd length, 0x prefix, whitespace and empty input. Those all landed and are still there; the sign case was not part of it.

This was AI-assisted.

UInt8(_:radix:) accepts a leading sign, so the two-character chunks this
parser feeds it let "+f" through as 0x0f. The doc comment already promises nil
for a string containing invalid hex characters, and it did not deliver that.

The cost is a second spelling for every identity-bearing hex string the app
parses -- Noise keys, Nostr pubkeys, fingerprints, signatures, routing IDs.
A 64-character peer ID of "+b" pairs passed PeerID.isValid, reported isHex
false and isNoiseKeyHex true at the same time, and resolved to the same
32-byte key and the same short routing ID as the genuine peer it shadowed.

Require two hex digits before parsing the pair.
DataHexTests covers what the parser accepts (plain, uppercase, 0x-prefixed,
whitespace-padded, every byte value round-tripped) alongside what it must
refuse, including the signed pairs that used to alias a genuine key.

PeerIDTests gains the 64-character negatives it was missing -- the short form
already had them -- and asserts isHex and isNoiseKeyHex agree.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant