mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 22:45:19 +00:00
Transport callers no longer reach the raw NoiseEncryptionService: getNoiseService() is deleted in favor of narrow purpose-named Transport methods (session public key, identity fingerprint, static/signing keys, sign/verify, callback installation). VerificationService now reaches crypto through the transport, so it can no longer pin a stale service across a panic reset. myPeerID/myNickname become private(set); the existing setNickname mutator is the sole nickname path. ConversationStore is now the sole owner of private-chat selection: PrivateChatManager.selectedPeer is a published read-only mirror, and startChat/endChat mutate through the store intent. The bridge method and its five call sites are deleted, removing a latent bug where a stale manager selection pushed back into the store could resurrect a just-removed conversation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
132 lines
5.2 KiB
Swift
132 lines
5.2 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_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()
|
|
}
|
|
}
|