Fix asymmetric voice/media delivery in private chats

When selectedPrivateChatPeer was migrated to a 64-hex stable Noise key
after session establishment, file transfers would silently fail because:

1. Sender used raw 64-hex key, truncated to first 8 bytes by BinaryProtocol
2. Receiver's myPeerID is SHA256-fingerprint-derived (different value)
3. Recipient check failed, causing silent packet drop

The fix normalizes peerID to short form (SHA256-derived 16-hex) in
sendFilePrivate before constructing recipientData, ensuring the wire
format matches what receivers expect.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jack
2026-01-12 16:17:50 -10:00
co-authored by Claude Opus 4.5
parent 7cfdcfe174
commit b47fb736f4
2 changed files with 70 additions and 1 deletions
+66
View File
@@ -426,4 +426,70 @@ struct PeerIDTests {
#expect(!PeerID(str: "nostr:\(hex65)").isValid)
#expect(!PeerID(str: "nostr_\(hex65)").isValid)
}
// MARK: - File Transfer PeerID Normalization
// These tests verify the fix for asymmetric voice/media delivery (BCH-01-XXX)
// The bug occurred when selectedPrivateChatPeer was migrated to 64-hex stable key
// but the receiver expected SHA256-derived 16-hex format
@Test func fileTransfer_toShortNormalizesNoiseKeyToFingerprint() {
// Given: A 64-hex Noise public key (what selectedPrivateChatPeer becomes after session)
let noiseKey = Data(repeating: 0xAB, count: 32)
let stableKeyPeerID = PeerID(hexData: noiseKey) // 64-hex
// When: Convert to short form (what sendFilePrivate should do)
let shortID = stableKeyPeerID.toShort()
// Then: Should be 16-hex SHA256 fingerprint (matching myPeerID format)
let expected = noiseKey.sha256Fingerprint().prefix(16)
#expect(shortID.id == String(expected))
#expect(shortID.id.count == 16)
}
@Test func fileTransfer_shortIDMatchesMyPeerIDFormat() {
// Given: A receiver's myPeerID is SHA256-derived (from refreshPeerIdentity)
let noiseKey = Data(repeating: 0xCD, count: 32)
let myPeerID = PeerID(publicKey: noiseKey) // SHA256-derived 16-hex
// When: Sender uses toShort() on 64-hex stable key
let senderStableKey = PeerID(hexData: noiseKey) // 64-hex
let recipientData = Data(hexString: senderStableKey.toShort().id)!
let receivedRecipientID = PeerID(hexData: recipientData)
// Then: Should match receiver's myPeerID (file transfer accepted)
#expect(receivedRecipientID == myPeerID)
}
@Test func fileTransfer_truncatedRawKeyDoesNotMatchMyPeerID() {
// This test demonstrates the bug we fixed
// When 64-hex was truncated to first 8 bytes instead of using SHA256 fingerprint
// Given: Receiver's myPeerID is SHA256-derived
let noiseKey = Data(repeating: 0xEF, count: 32)
let myPeerID = PeerID(publicKey: noiseKey) // SHA256-derived 16-hex
// When: Truncate raw key (the OLD buggy behavior)
let truncatedRaw = noiseKey.prefix(8) // First 8 bytes of raw key
let wrongRecipientID = PeerID(hexData: truncatedRaw)
// Then: Should NOT match (demonstrates why fix was needed)
#expect(wrongRecipientID != myPeerID)
}
@Test func fileTransfer_shortIDProducesCorrect8ByteRoutingData() {
// Verify the wire format is correct (8 bytes for BinaryProtocol)
let noiseKey = Data(repeating: 0x12, count: 32)
let stableKeyPeerID = PeerID(hexData: noiseKey)
let shortID = stableKeyPeerID.toShort()
// routingData should be 8 bytes (16 hex chars -> 8 bytes)
let routingData = shortID.routingData
#expect(routingData != nil)
#expect(routingData?.count == 8)
// And it should match SHA256 fingerprint first 8 bytes
let expectedFingerprint = noiseKey.sha256Fingerprint()
let expectedFirst8 = Data(hexString: String(expectedFingerprint.prefix(16)))
#expect(routingData == expectedFirst8)
}
}