mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-08-08 06:56:10 +00:00
* Cohere per-link Noise auth and rebind containment into BLELinkAuthState The authenticated-link owners, the reconnect revalidation policy, and the two rebind-containment cooldowns were four loose bleQueue-owned maps whose invariants lived in call-site discipline: every teardown path had to remember to retire the proof AND close the revalidation epoch (the pair appeared seven times), and both cooldowns hand-rolled the same prune-check-record dance. BLELinkAuthState owns them as whole transitions — retireLink, retireLinks(ownedBy:), permitRebind, permitRedundantRetirement — with the ownership question (bleQueue today, engine after the option-B flip) answered in one place. No behavior change; the one call-site reordering (redundant retirement computes the survivor before the cooldown check instead of after) is outcome-equivalent since the cooldown only ever recorded when a survivor existed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Split identity-link bindings out of the physical link store BLELinkStateStore owned two different kinds of truth: what physical links exist (CB handles, connect lifecycles, characteristics, stream assemblers) and who each link belongs to (peer bindings in both roles plus the preferred-peripheral reverse map for directed sends and fanout collapse). The bindings now live on BLELinkBindings — same bleQueue ownership, whole-transition methods, direct tests for the rotation reverse-map cleanup and the preferred-link survivor repair that were previously only exercised end to end. Composed operations that need both truths (remove-with-repair, direct link state, the subscribed- central snapshot, bind-only-live-links) live on the transport as explicitly bleQueue-confined helpers. This is the structural half of the option-B boundary flip (docs/BLE-ARCHITECTURE-V3.md): ownership of the bindings can now move to the engine without touching what-links-exist. An audit of every physical clear/remove found three sites (emergency clear, both unauthorized branches) that needed explicit binding-clear pairing under the split — each now clears both. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix iOS-gated constructors and preserve containment cooldowns on reset CI caught what the macOS SwiftPM build cannot see: two #if os(iOS) sites still passed the peerID field that slice B1 removed from BLEPeripheralLinkState (willRestoreState in BLEService and armPendingBackgroundConnects in BLERadioController). Both fixed and verified with a local iOS simulator xcodebuild. Codex also caught a real regression: BLELinkAuthState.removeAll() cleared the rebind/retirement cooldown maps, which the original panic and emergency reset paths deliberately left alive. A stable CoreBluetooth UUID must not earn a fresh rebind allowance just because the session state around it was wiped. removeAll() now clears only the proofs and revalidation epochs, and BLELinkAuthStateTests pins the survival invariant along with the other auth-state transitions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
76 lines
3.2 KiB
Swift
76 lines
3.2 KiB
Swift
import BitFoundation
|
|
import Foundation
|
|
import Testing
|
|
@testable import bitchat
|
|
|
|
struct BLELinkAuthStateTests {
|
|
private let peerID = PeerID(str: "1122334455667788")
|
|
private let link = BLEIngressLinkID.peripheral("periph-a")
|
|
|
|
@Test
|
|
func authenticationBindsToTheExactLinkAndOwner() {
|
|
var auth = BLELinkAuthState()
|
|
auth.markAuthenticated(link, owner: peerID)
|
|
|
|
#expect(auth.isAuthenticated(link, for: peerID))
|
|
#expect(!auth.isAuthenticated(link, for: PeerID(str: "8899aabbccddeeff")))
|
|
#expect(!auth.isAuthenticated(.peripheral("periph-b"), for: peerID))
|
|
|
|
auth.retireLink(link)
|
|
#expect(!auth.isAuthenticated(link, for: peerID))
|
|
}
|
|
|
|
@Test
|
|
func retireLinksOwnedByPeerReturnsAndRetiresThemAll() {
|
|
var auth = BLELinkAuthState()
|
|
auth.markAuthenticated(.peripheral("periph-a"), owner: peerID)
|
|
auth.markAuthenticated(.central("central-a"), owner: peerID)
|
|
auth.markAuthenticated(.central("central-b"), owner: PeerID(str: "8899aabbccddeeff"))
|
|
|
|
let departed = Set(auth.retireLinks(ownedBy: peerID))
|
|
|
|
#expect(departed == [.peripheral("periph-a"), .central("central-a")])
|
|
#expect(auth.links(ownedBy: peerID).isEmpty)
|
|
#expect(auth.isAuthenticated(.central("central-b"), for: PeerID(str: "8899aabbccddeeff")))
|
|
}
|
|
|
|
@Test
|
|
func rebindCooldownPermitsOncePerWindowAndAgesOut() {
|
|
var auth = BLELinkAuthState()
|
|
let start = Date(timeIntervalSince1970: 1_000)
|
|
|
|
let first = auth.permitRebind(linkUUID: "periph-a", now: start, cooldown: 30)
|
|
#expect(first)
|
|
let withinWindow = auth.permitRebind(linkUUID: "periph-a", now: start.addingTimeInterval(10), cooldown: 30)
|
|
#expect(!withinWindow)
|
|
// A different link has its own allowance.
|
|
let otherLink = auth.permitRebind(linkUUID: "periph-b", now: start.addingTimeInterval(10), cooldown: 30)
|
|
#expect(otherLink)
|
|
// The window ages out.
|
|
let afterWindow = auth.permitRebind(linkUUID: "periph-a", now: start.addingTimeInterval(31), cooldown: 30)
|
|
#expect(afterWindow)
|
|
}
|
|
|
|
@Test
|
|
func containmentCooldownsSurviveASessionReset() {
|
|
var auth = BLELinkAuthState()
|
|
let start = Date(timeIntervalSince1970: 2_000)
|
|
auth.markAuthenticated(link, owner: peerID)
|
|
let rebindBefore = auth.permitRebind(linkUUID: "periph-a", now: start, cooldown: 30)
|
|
let retirementBefore = auth.permitRedundantRetirement(peerID: peerID, now: start, cooldown: 30)
|
|
#expect(rebindBefore)
|
|
#expect(retirementBefore)
|
|
|
|
// Panic/emergency resets wipe proofs and epochs — but a stable
|
|
// CoreBluetooth UUID must not earn a fresh rebind or retirement
|
|
// allowance just because the session state around it was wiped.
|
|
auth.removeAll()
|
|
|
|
#expect(!auth.isAuthenticated(link, for: peerID))
|
|
let rebindAfterReset = auth.permitRebind(linkUUID: "periph-a", now: start.addingTimeInterval(5), cooldown: 30)
|
|
let retirementAfterReset = auth.permitRedundantRetirement(peerID: peerID, now: start.addingTimeInterval(5), cooldown: 30)
|
|
#expect(!rebindAfterReset)
|
|
#expect(!retirementAfterReset)
|
|
}
|
|
}
|