mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-27 02:05:19 +00:00
Friend-courier store-and-forward: mutual favorites carry sealed messages to offline peers
When a private message has no reachable transport, the router now seals it to the recipient's Noise static key (new one-way Noise X pattern) and hands the envelope to up to three connected mutual favorites. Couriers store the opaque ciphertext under strict quotas (20 total, 5 per depositor, 16 KiB, 24 h) and hand it over when the recipient's announce matches a rotating HMAC recipient tag; the recipient opens it and the message flows through the normal private-message pipeline, so dedup and delivery acks just work. - CourierEnvelope TLV + courierEnvelope (0x04) message type in BitFoundation - Noise X one-way pattern reusing the existing handshake machinery, domain-separated by a courier prologue; sender identity authenticated via the ss DH (no forward secrecy - documented tradeoff) - CourierStore with eviction, file persistence, and panic-wipe integration - Rotating recipient tags (HMAC over epoch day) so carried envelopes don't correlate for observers who don't already know the recipient's key - New "carried" delivery status with figure.walk glyph; header indicator while carrying mail for others - Three-node end-to-end test ferrying packets through real BLEService instances, plus codec/crypto/store/router suites (986 tests green) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
//
|
||||
// NoiseCourierTests.swift
|
||||
// bitchat
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import bitchat
|
||||
|
||||
/// One-way Noise X envelopes: encryption to a known static key without an
|
||||
/// interactive handshake, used by the courier store-and-forward path.
|
||||
struct NoiseCourierTests {
|
||||
|
||||
@Test func sealAndOpenRoundTrip() throws {
|
||||
let alice = NoiseEncryptionService(keychain: MockKeychain())
|
||||
let bob = NoiseEncryptionService(keychain: MockKeychain())
|
||||
|
||||
let payload = Data("meet at the north gate".utf8)
|
||||
let sealed = try alice.sealCourierPayload(payload, recipientStaticKey: bob.getStaticPublicKeyData())
|
||||
|
||||
let opened = try bob.openCourierPayload(sealed)
|
||||
#expect(opened.payload == payload)
|
||||
// The X pattern authenticates the sender: Bob learns Alice's real static key.
|
||||
#expect(opened.senderStaticKey == alice.getStaticPublicKeyData())
|
||||
}
|
||||
|
||||
@Test func wrongRecipientCannotOpen() throws {
|
||||
let alice = NoiseEncryptionService(keychain: MockKeychain())
|
||||
let bob = NoiseEncryptionService(keychain: MockKeychain())
|
||||
let carol = NoiseEncryptionService(keychain: MockKeychain())
|
||||
|
||||
let sealed = try alice.sealCourierPayload(Data("secret".utf8), recipientStaticKey: bob.getStaticPublicKeyData())
|
||||
|
||||
#expect(throws: (any Error).self) {
|
||||
_ = try carol.openCourierPayload(sealed)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func tamperedEnvelopeFailsToOpen() throws {
|
||||
let alice = NoiseEncryptionService(keychain: MockKeychain())
|
||||
let bob = NoiseEncryptionService(keychain: MockKeychain())
|
||||
|
||||
var sealed = try alice.sealCourierPayload(Data("secret".utf8), recipientStaticKey: bob.getStaticPublicKeyData())
|
||||
sealed[sealed.count - 1] ^= 0x01
|
||||
|
||||
#expect(throws: (any Error).self) {
|
||||
_ = try bob.openCourierPayload(sealed)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func senderIdentityCannotBeForged() throws {
|
||||
// The encrypted static key inside the envelope is bound by the ss DH;
|
||||
// splicing one envelope's ephemeral prefix onto another must fail.
|
||||
let alice = NoiseEncryptionService(keychain: MockKeychain())
|
||||
let mallory = NoiseEncryptionService(keychain: MockKeychain())
|
||||
let bob = NoiseEncryptionService(keychain: MockKeychain())
|
||||
|
||||
let bobKey = bob.getStaticPublicKeyData()
|
||||
let fromAlice = try alice.sealCourierPayload(Data("hi".utf8), recipientStaticKey: bobKey)
|
||||
let fromMallory = try mallory.sealCourierPayload(Data("hi".utf8), recipientStaticKey: bobKey)
|
||||
|
||||
// e (32 bytes) from Mallory's envelope + rest from Alice's.
|
||||
let spliced = fromMallory.prefix(32) + fromAlice.dropFirst(32)
|
||||
#expect(throws: (any Error).self) {
|
||||
_ = try bob.openCourierPayload(Data(spliced))
|
||||
}
|
||||
}
|
||||
|
||||
@Test func sealRejectsInvalidRecipientKey() {
|
||||
let alice = NoiseEncryptionService(keychain: MockKeychain())
|
||||
#expect(throws: (any Error).self) {
|
||||
_ = try alice.sealCourierPayload(Data("x".utf8), recipientStaticKey: Data(repeating: 0, count: 32))
|
||||
}
|
||||
#expect(throws: (any Error).self) {
|
||||
_ = try alice.sealCourierPayload(Data("x".utf8), recipientStaticKey: Data(repeating: 1, count: 8))
|
||||
}
|
||||
}
|
||||
|
||||
@Test func emptyAndLargePayloadsRoundTrip() throws {
|
||||
let alice = NoiseEncryptionService(keychain: MockKeychain())
|
||||
let bob = NoiseEncryptionService(keychain: MockKeychain())
|
||||
let bobKey = bob.getStaticPublicKeyData()
|
||||
|
||||
let empty = try alice.sealCourierPayload(Data(), recipientStaticKey: bobKey)
|
||||
#expect(try bob.openCourierPayload(empty).payload.isEmpty)
|
||||
|
||||
let large = Data((0..<8192).map { UInt8($0 % 251) })
|
||||
let sealed = try alice.sealCourierPayload(large, recipientStaticKey: bobKey)
|
||||
#expect(try bob.openCourierPayload(sealed).payload == large)
|
||||
}
|
||||
|
||||
@Test func envelopesAreNotLinkableAcrossSends() throws {
|
||||
// Fresh ephemeral per seal: same payload to the same recipient must
|
||||
// produce entirely different ciphertexts.
|
||||
let alice = NoiseEncryptionService(keychain: MockKeychain())
|
||||
let bob = NoiseEncryptionService(keychain: MockKeychain())
|
||||
let payload = Data("same message".utf8)
|
||||
|
||||
let a = try alice.sealCourierPayload(payload, recipientStaticKey: bob.getStaticPublicKeyData())
|
||||
let b = try alice.sealCourierPayload(payload, recipientStaticKey: bob.getStaticPublicKeyData())
|
||||
#expect(a != b)
|
||||
#expect(a.prefix(32) != b.prefix(32))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user