mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 20:45:19 +00:00
Authenticate private media capabilities in Noise
This commit is contained in:
@@ -6,6 +6,7 @@ import Testing
|
||||
struct BLEAnnounceHandlerTests {
|
||||
private final class Recorder {
|
||||
var existingNoisePublicKey: Data?
|
||||
var authenticatedSigningPublicKey: Data?
|
||||
var signatureValid = true
|
||||
var linkState: (hasPeripheral: Bool, hasCentral: Bool) = (false, false)
|
||||
var linkBoundToOtherPeer = false
|
||||
@@ -37,6 +38,7 @@ struct BLEAnnounceHandlerTests {
|
||||
messageTTL: TransportConfig.messageTTLDefault,
|
||||
now: { now },
|
||||
existingNoisePublicKey: { _ in recorder.existingNoisePublicKey },
|
||||
authenticatedSigningPublicKey: { _ in recorder.authenticatedSigningPublicKey },
|
||||
verifySignature: { packet, signingPublicKey in
|
||||
recorder.verifySignatureCalls.append((packet, signingPublicKey))
|
||||
return recorder.signatureValid
|
||||
|
||||
@@ -169,6 +169,23 @@ struct BLEAnnounceHandlingPolicyTests {
|
||||
#expect(decision.isVerified)
|
||||
}
|
||||
|
||||
@Test
|
||||
func trustPolicyRejectsSigningKeyReplacementAfterNoiseBinding() {
|
||||
let noiseKey = Data(repeating: 0xCC, count: 32)
|
||||
let boundSigningKey = Data(repeating: 0x11, count: 32)
|
||||
|
||||
let decision = BLEAnnounceTrustPolicy.evaluate(
|
||||
hasSignature: true,
|
||||
signatureValid: true,
|
||||
existingNoisePublicKey: noiseKey,
|
||||
announcedNoisePublicKey: noiseKey,
|
||||
authenticatedSigningPublicKey: boundSigningKey,
|
||||
announcedSigningPublicKey: Data(repeating: 0x22, count: 32)
|
||||
)
|
||||
|
||||
#expect(decision == .reject(.authenticatedSigningKeyMismatch))
|
||||
}
|
||||
|
||||
@Test
|
||||
func responsePolicyConnectsOnlyForDirectNewOrReconnectedPeers() {
|
||||
let directNew = BLEAnnounceResponsePolicy.plan(
|
||||
|
||||
@@ -18,6 +18,7 @@ struct BLENoisePacketHandlerTests {
|
||||
var lastSeenUpdates: [PeerID] = []
|
||||
var decryptCalls: [(payload: Data, peerID: PeerID)] = []
|
||||
var clearedSessions: [PeerID] = []
|
||||
var authenticatedPeerStates: [(peerID: PeerID, payload: Data)] = []
|
||||
var deliveries: [(peerID: PeerID, type: NoisePayloadType, payload: Data, timestamp: Date)] = []
|
||||
/// Ordered side-effect log to assert recovery sequencing.
|
||||
var events: [String] = []
|
||||
@@ -62,6 +63,9 @@ struct BLENoisePacketHandlerTests {
|
||||
recorder.clearedSessions.append(peerID)
|
||||
recorder.events.append("clearSession")
|
||||
},
|
||||
handleAuthenticatedPeerState: { peerID, payload in
|
||||
recorder.authenticatedPeerStates.append((peerID, payload))
|
||||
},
|
||||
deliverNoisePayload: { peerID, type, payload, timestamp in
|
||||
recorder.deliveries.append((peerID, type, payload, timestamp))
|
||||
}
|
||||
@@ -221,6 +225,24 @@ struct BLENoisePacketHandlerTests {
|
||||
#expect(recorder.initiatedHandshakes.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func authenticatedPeerStateIsConsumedByTransportNotDeliveredToUI() {
|
||||
let recorder = Recorder()
|
||||
recorder.decryptResult = .success(Data([
|
||||
NoisePayloadType.authenticatedPeerState.rawValue,
|
||||
0x01, 0x02, 0x03
|
||||
]))
|
||||
let handler = makeHandler(recorder: recorder)
|
||||
let packet = makeEncryptedPacket(recipientID: Data(hexString: localPeerID.id))
|
||||
|
||||
handler.handleEncrypted(packet, from: remotePeerID)
|
||||
|
||||
#expect(recorder.authenticatedPeerStates.count == 1)
|
||||
#expect(recorder.authenticatedPeerStates.first?.peerID == remotePeerID)
|
||||
#expect(recorder.authenticatedPeerStates.first?.payload == Data([0x01, 0x02, 0x03]))
|
||||
#expect(recorder.deliveries.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func emptyDecryptedPayloadIsIgnored() {
|
||||
let recorder = Recorder()
|
||||
|
||||
@@ -79,4 +79,16 @@ struct BLENoisePayloadFactoryTests {
|
||||
#expect(decoded.data == Data([0xCA, 0xFE]))
|
||||
#expect(decoded.encode().first == 0x20)
|
||||
}
|
||||
|
||||
@Test
|
||||
func authenticatedPeerStateUsesPermanent0x21Type() throws {
|
||||
let state = AuthenticatedPeerStatePacket(
|
||||
capabilities: .privateMedia,
|
||||
signingPublicKey: Data(repeating: 0x77, count: 32)
|
||||
)
|
||||
let encoded = try #require(BLENoisePayloadFactory.authenticatedPeerState(state))
|
||||
|
||||
#expect(encoded.first == 0x21)
|
||||
#expect(AuthenticatedPeerStatePacket.decode(from: Data(encoded.dropFirst())) == state)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,6 +237,40 @@ struct NoiseEncryptionServiceTests {
|
||||
#expect(try receiver.decrypt(ciphertext, from: alicePeerID) == Data("new session".utf8))
|
||||
}
|
||||
|
||||
@Test("Automatic rekey exposes and completes its exact handshake bytes")
|
||||
func automaticRekeyHandshakeIsNotStranded() throws {
|
||||
let alice = NoiseEncryptionService(keychain: MockKeychain())
|
||||
let bob = NoiseEncryptionService(keychain: MockKeychain())
|
||||
let alicePeerID = PeerID(publicKey: alice.getStaticPublicKeyData())
|
||||
let bobPeerID = PeerID(publicKey: bob.getStaticPublicKeyData())
|
||||
try establishSessions(alice: alice, bob: bob)
|
||||
|
||||
var emittedPeerID: PeerID?
|
||||
var emittedMessage: Data?
|
||||
alice.onRekeyHandshakeReady = { peerID, message in
|
||||
emittedPeerID = peerID
|
||||
emittedMessage = message
|
||||
}
|
||||
try alice._test_initiateAutomaticRekey(for: bobPeerID)
|
||||
|
||||
#expect(emittedPeerID == bobPeerID)
|
||||
let message1 = try #require(emittedMessage)
|
||||
#expect(!message1.isEmpty)
|
||||
#expect(alice.hasSession(with: bobPeerID))
|
||||
#expect(!alice.hasEstablishedSession(with: bobPeerID))
|
||||
|
||||
let message2 = try #require(
|
||||
try bob.processHandshakeMessage(from: alicePeerID, message: message1)
|
||||
)
|
||||
let message3 = try #require(
|
||||
try alice.processHandshakeMessage(from: bobPeerID, message: message2)
|
||||
)
|
||||
_ = try bob.processHandshakeMessage(from: alicePeerID, message: message3)
|
||||
|
||||
#expect(alice.hasEstablishedSession(with: bobPeerID))
|
||||
#expect(bob.hasEstablishedSession(with: alicePeerID))
|
||||
}
|
||||
|
||||
@Test("Large private-file payloads use the bounded Noise extension")
|
||||
func largePrivateFileNoiseRoundTrip() throws {
|
||||
let alice = NoiseEncryptionService(keychain: MockKeychain())
|
||||
|
||||
@@ -427,6 +427,31 @@ final class SecureIdentityStateManagerTests: XCTestCase {
|
||||
XCTAssertTrue(cleared)
|
||||
}
|
||||
|
||||
func test_noiseAuthenticatedSigningKeyBindingPersistsAndPanicClearRemovesIt() async {
|
||||
let keychain = MockKeychain()
|
||||
let fingerprint = Data(repeating: 0x31, count: 32).sha256Fingerprint()
|
||||
let firstKey = Data(repeating: 0x41, count: 32)
|
||||
let rotatedKey = Data(repeating: 0x42, count: 32)
|
||||
let manager = SecureIdentityStateManager(keychain)
|
||||
|
||||
manager.bindAuthenticatedSigningPublicKey(firstKey, fingerprint: fingerprint)
|
||||
XCTAssertEqual(manager.authenticatedSigningPublicKey(forFingerprint: fingerprint), firstKey)
|
||||
// A later authenticated Noise session may legitimately rotate the
|
||||
// announcement signing key.
|
||||
manager.bindAuthenticatedSigningPublicKey(rotatedKey, fingerprint: fingerprint)
|
||||
XCTAssertEqual(manager.authenticatedSigningPublicKey(forFingerprint: fingerprint), rotatedKey)
|
||||
|
||||
manager.forceSave()
|
||||
let reloaded = SecureIdentityStateManager(keychain)
|
||||
XCTAssertEqual(reloaded.authenticatedSigningPublicKey(forFingerprint: fingerprint), rotatedKey)
|
||||
|
||||
reloaded.clearAllIdentityData()
|
||||
let cleared = await waitUntil {
|
||||
reloaded.authenticatedSigningPublicKey(forFingerprint: fingerprint) == nil
|
||||
}
|
||||
XCTAssertTrue(cleared)
|
||||
}
|
||||
|
||||
func test_forceSave_withFailingCacheWriteDoesNotPersistCache() async {
|
||||
let keychain = FailingCacheSaveKeychain()
|
||||
let manager = SecureIdentityStateManager(keychain)
|
||||
|
||||
@@ -268,6 +268,8 @@ private final class TestIdentityManager: SecureIdentityStateManagerProtocol {
|
||||
|
||||
func markPrivateMediaCapable(fingerprint: String) {}
|
||||
func hasObservedPrivateMediaCapability(fingerprint: String) -> Bool { false }
|
||||
func bindAuthenticatedSigningPublicKey(_ signingPublicKey: Data, fingerprint: String) {}
|
||||
func authenticatedSigningPublicKey(forFingerprint fingerprint: String) -> Data? { nil }
|
||||
|
||||
func removeEphemeralSession(peerID: PeerID) {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user