mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-08-22 07:16:03 +00:00
Two re-assessment findings: - verifyScannedQR checked only staleness (now - ts > maxAge), so a future-dated timestamp bought a QR a longer validity window than a fresh one. The freshness check is now symmetric. - Inbound Nostr DMs had no client-side created_at validation; the age bound relied entirely on relays honoring the subscription's `since` filter. Both gift-wrap decrypt paths now drop rumors outside [now - lookback - skew, now + skew]. The inner rumor timestamp is the sender's true send time (only the outer gift wrap is randomized per NIP-17), so the window mirrors exactly what an honest relay already guarantees — a dishonest relay can no longer inject stale or future-dated DMs. Existing mitigations (persistent gift-wrap dedup, relay-side since) are unchanged; this closes the malicious-relay gap. Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
145 lines
5.6 KiB
Swift
145 lines
5.6 KiB
Swift
import XCTest
|
|
@testable import bitchat
|
|
|
|
final class VerificationServiceTests: XCTestCase {
|
|
func test_buildMyQRString_roundTripsSuccessfully() throws {
|
|
let (service, noise) = makeService()
|
|
let nickname = "alice-\(UUID().uuidString)"
|
|
let npub = "npub1testvalue"
|
|
|
|
let qrString = try XCTUnwrap(service.buildMyQRString(nickname: nickname, npub: npub))
|
|
let parsed = try XCTUnwrap(service.verifyScannedQR(qrString))
|
|
|
|
XCTAssertEqual(parsed.nickname, nickname)
|
|
XCTAssertEqual(parsed.npub, npub)
|
|
XCTAssertEqual(parsed.noiseKeyHex, noise.getStaticPublicKeyData().hexEncodedString())
|
|
XCTAssertEqual(parsed.signKeyHex, noise.getSigningPublicKeyData().hexEncodedString())
|
|
}
|
|
|
|
func test_buildMyQRString_returnsCachedValueForSameInputs() throws {
|
|
let (service, _) = makeService()
|
|
let nickname = "cache-\(UUID().uuidString)"
|
|
|
|
let first = try XCTUnwrap(service.buildMyQRString(nickname: nickname, npub: nil))
|
|
let second = try XCTUnwrap(service.buildMyQRString(nickname: nickname, npub: nil))
|
|
|
|
XCTAssertEqual(first, second)
|
|
}
|
|
|
|
func test_verifyScannedQR_rejectsExpiredPayload() throws {
|
|
let (service, noise) = makeService()
|
|
let oldTimestamp = Int64(Date().addingTimeInterval(-3600).timeIntervalSince1970)
|
|
let qrString = try makeSignedQR(
|
|
noise: noise,
|
|
nickname: "expired-\(UUID().uuidString)",
|
|
npub: nil,
|
|
ts: oldTimestamp
|
|
)
|
|
|
|
XCTAssertNil(service.verifyScannedQR(qrString, maxAge: 60))
|
|
}
|
|
|
|
func test_verifyScannedQR_rejectsFutureDatedPayload() throws {
|
|
let (service, noise) = makeService()
|
|
let futureTimestamp = Int64(Date().addingTimeInterval(3600).timeIntervalSince1970)
|
|
let qrString = try makeSignedQR(
|
|
noise: noise,
|
|
nickname: "future-\(UUID().uuidString)",
|
|
npub: nil,
|
|
ts: futureTimestamp
|
|
)
|
|
|
|
XCTAssertNil(service.verifyScannedQR(qrString, maxAge: 60))
|
|
}
|
|
|
|
func test_verifyScannedQR_rejectsTamperedSignature() throws {
|
|
let (service, noise) = makeService()
|
|
let badSignature = Data(repeating: 0xAA, count: 64)
|
|
let qrString = try makeSignedQR(
|
|
noise: noise,
|
|
nickname: "tampered-\(UUID().uuidString)",
|
|
npub: nil,
|
|
ts: Int64(Date().timeIntervalSince1970),
|
|
signatureOverride: badSignature
|
|
)
|
|
|
|
XCTAssertNil(service.verifyScannedQR(qrString))
|
|
}
|
|
|
|
func test_buildVerifyChallenge_roundTripsThroughNoisePayload() throws {
|
|
let (service, _) = makeService()
|
|
let noiseKeyHex = String(repeating: "ab", count: 32)
|
|
let nonce = Data([0x01, 0x02, 0x03, 0x04])
|
|
|
|
let encoded = service.buildVerifyChallenge(noiseKeyHex: noiseKeyHex, nonceA: nonce)
|
|
let payload = try XCTUnwrap(NoisePayload.decode(encoded))
|
|
let parsed = try XCTUnwrap(service.parseVerifyChallenge(payload.data))
|
|
|
|
XCTAssertEqual(payload.type, .verifyChallenge)
|
|
XCTAssertEqual(parsed.noiseKeyHex, noiseKeyHex)
|
|
XCTAssertEqual(parsed.nonceA, nonce)
|
|
}
|
|
|
|
func test_buildVerifyResponse_roundTripsAndVerifiesSignature() throws {
|
|
let (service, noise) = makeService()
|
|
let noiseKeyHex = String(repeating: "cd", count: 32)
|
|
let nonce = Data([0x10, 0x20, 0x30, 0x40, 0x50])
|
|
|
|
let encoded = try XCTUnwrap(service.buildVerifyResponse(noiseKeyHex: noiseKeyHex, nonceA: nonce))
|
|
let payload = try XCTUnwrap(NoisePayload.decode(encoded))
|
|
let parsed = try XCTUnwrap(service.parseVerifyResponse(payload.data))
|
|
|
|
XCTAssertEqual(payload.type, .verifyResponse)
|
|
XCTAssertEqual(parsed.noiseKeyHex, noiseKeyHex)
|
|
XCTAssertEqual(parsed.nonceA, nonce)
|
|
XCTAssertTrue(
|
|
service.verifyResponseSignature(
|
|
noiseKeyHex: parsed.noiseKeyHex,
|
|
nonceA: parsed.nonceA,
|
|
signature: parsed.signature,
|
|
signerPublicKeyHex: noise.getSigningPublicKeyData().hexEncodedString()
|
|
)
|
|
)
|
|
XCTAssertFalse(
|
|
service.verifyResponseSignature(
|
|
noiseKeyHex: parsed.noiseKeyHex,
|
|
nonceA: Data([0xFF]),
|
|
signature: parsed.signature,
|
|
signerPublicKeyHex: noise.getSigningPublicKeyData().hexEncodedString()
|
|
)
|
|
)
|
|
}
|
|
|
|
private func makeService() -> (VerificationService, NoiseEncryptionService) {
|
|
// The service consumes Noise identity operations through the
|
|
// Transport's narrow noise* wrappers; the mock transport's backing
|
|
// encryption service is returned for direct assertions.
|
|
let transport = MockTransport()
|
|
let service = VerificationService()
|
|
service.configure(with: transport)
|
|
return (service, transport.mockNoiseService)
|
|
}
|
|
|
|
private func makeSignedQR(
|
|
noise: NoiseEncryptionService,
|
|
nickname: String,
|
|
npub: String?,
|
|
ts: Int64,
|
|
signatureOverride: Data? = nil
|
|
) throws -> String {
|
|
var payload = VerificationService.VerificationQR(
|
|
v: 1,
|
|
noiseKeyHex: noise.getStaticPublicKeyData().hexEncodedString(),
|
|
signKeyHex: noise.getSigningPublicKeyData().hexEncodedString(),
|
|
npub: npub,
|
|
nickname: nickname,
|
|
ts: ts,
|
|
nonceB64: Data((0..<16).map(UInt8.init)).base64EncodedString(),
|
|
sigHex: ""
|
|
)
|
|
let signature = try XCTUnwrap(signatureOverride ?? noise.signData(payload.canonicalBytes()))
|
|
payload.sigHex = signature.hexEncodedString()
|
|
return payload.toURLString()
|
|
}
|
|
}
|