jack 2a9fb4d53f
Make SwiftLint blocking in CI now that the violation backlog is zero (#1646)
* Make SwiftLint blocking in CI now that the violation backlog is zero

The lint job has been advisory since #1361, with the backlog tracked in
#1088. That backlog is now down to 28 violations, so this clears it and
flips the job to enforcing instead of adding baseline machinery:

- Fix all remaining violations: closure parameters/capture lists joined
  onto their opening-brace line (BLEService, NoiseSessionManager,
  ChatMediaTransferCoordinator, BLENoisePacketHandlerTests), one
  trailing comma, one opening-brace spacing (both via swiftlint --fix).
- Move the two periphery:ignore comments above the doc blocks they were
  orphaning; verified with a periphery --strict scan that the commands
  still register (no new findings vs baseline).
- Disable the todo rule: the single TODO cites tracked issue #1434 and
  is a deliberate marker, not lint debt.
- Exclude .device-lab from local runs (untracked; CI never sees it).
- CI: drop continue-on-error and the "(advisory)" label; run with
  --strict so any new warning fails the job.

Full SwiftPM suite green (2,004 tests / 216 suites) after the
formatting changes; swiftlint lint --strict exits 0 with the same
0.65.0 the CI container pins.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Fix is_disjoint violation introduced by #1542

The strict lint gate caught this on its first run: the mention-suggestion
key handler used modifierFlags.intersection([...]).isEmpty, which is
exactly the pattern isDisjoint(with:) expresses directly.

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>
2026-08-08 12:12:30 +02:00

159 lines
6.3 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// AnnounceV2Packet.swift
// BitFoundation
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import Foundation
// periphery:ignore - intentionally unreferenced by production code; nothing
// emits or consumes this type yet, and BLEService parses it only to ignore it.
// Delete this annotation when the mesh starts using it.
/// Identity-free presence announcement for rotating peer IDs.
///
/// The v1 `AnnouncementPacket` broadcasts, in cleartext, every 430 seconds: the
/// nickname, the 32-byte Noise static public key, the 32-byte Ed25519 signing
/// key, and up to ten neighbour IDs. That is a permanent device fingerprint plus
/// the local social graph, free to anyone in radio range. This carries none of
/// it only an epoch, a fixed-size block of pairwise recognition tags, and
/// capability bits.
///
/// Deliberately absent, with reasons:
/// - **Public keys**: they are the linkage. Peers learn them inside the Noise XX
/// handshake, where they are already encrypted on the wire.
/// - **Nickname**: a self-chosen, frequently reused human label. It moves into
/// the session (`AuthenticatedPeerStatePacket`).
/// - **Neighbour list**: it seeds source routing, whose documented fallback is
/// flooding. Publishing a crowd's adjacency graph is not a reasonable price
/// for routing efficiency.
///
/// **Unsigned, on purpose and not without cost.** There is no key to verify a
/// signature against without disclosing one, so this asserts only "somebody is
/// here, and here are some tags". An attacker can therefore emit noise bounded
/// by existing announce and connection rate limits but cannot impersonate a
/// specific peer, because forging a recognition tag needs one of the two private
/// keys, and cannot send anything without completing a handshake. The intended
/// posture is to treat a v2 announce as *unverified presence* and not surface it
/// until a tag matches or a handshake completes. See open question O4 in
/// `docs/PEER-ID-ROTATION.md`.
///
/// Not emitted or consumed by the shipping mesh yet.
public struct AnnounceV2Packet: Equatable, Sendable {
/// Rotation epoch this announce was built for. Carried explicitly so a
/// receiver matches against a stated epoch instead of guessing.
public let epoch: UInt32
/// Exactly `PeerIDRotation.tagSlots * PeerIDRotation.idLength` bytes.
public let tagBlock: Data
public let capabilities: PeerCapabilities?
/// Coarse rendezvous cell, when bridging. Same semantics as v1.
public let bridgeGeohash: String?
public init(
epoch: UInt32,
tagBlock: Data,
capabilities: PeerCapabilities? = nil,
bridgeGeohash: String? = nil
) {
self.epoch = epoch
self.tagBlock = tagBlock
self.capabilities = capabilities
self.bridgeGeohash = bridgeGeohash
}
private enum TLVType: UInt8 {
case epoch = 0x01
case tagBlock = 0x02
case capabilities = 0x03
case bridgeGeohash = 0x04
}
/// Expected tag-block width. A fixed size is load-bearing: it hides how many
/// mutual favourites a device has.
public static var tagBlockLength: Int {
PeerIDRotation.tagSlots * PeerIDRotation.idLength
}
public func encode() -> Data? {
guard tagBlock.count == Self.tagBlockLength else { return nil }
var data = Data()
data.append(TLVType.epoch.rawValue)
data.append(UInt8(4))
withUnsafeBytes(of: epoch.bigEndian) { data.append(contentsOf: $0) }
data.append(TLVType.tagBlock.rawValue)
data.append(UInt8(tagBlock.count))
data.append(tagBlock)
if let capabilities {
let bytes = capabilities.encoded()
guard bytes.count <= 255 else { return nil }
data.append(TLVType.capabilities.rawValue)
data.append(UInt8(bytes.count))
data.append(bytes)
}
if let bridgeGeohash, !bridgeGeohash.isEmpty {
let bytes = Data(bridgeGeohash.utf8)
guard bytes.count <= 12 else { return nil }
data.append(TLVType.bridgeGeohash.rawValue)
data.append(UInt8(bytes.count))
data.append(bytes)
}
return data
}
public static func decode(from data: Data) -> AnnounceV2Packet? {
var epoch: UInt32?
var tagBlock: Data?
var capabilities: PeerCapabilities?
var bridgeGeohash: String?
var offset = data.startIndex
while offset < data.endIndex {
guard data.distance(from: offset, to: data.endIndex) >= 2 else { return nil }
let rawType = data[offset]
let length = Int(data[data.index(after: offset)])
let valueStart = data.index(offset, offsetBy: 2)
guard data.distance(from: valueStart, to: data.endIndex) >= length else { return nil }
let value = data.subdata(in: valueStart..<data.index(valueStart, offsetBy: length))
switch TLVType(rawValue: rawType) {
case .epoch:
guard length == 4 else { return nil }
epoch = value.reduce(UInt32(0)) { ($0 << 8) | UInt32($1) }
case .tagBlock:
guard length == tagBlockLength else { return nil }
tagBlock = value
case .capabilities:
let decoded = PeerCapabilities(encoded: value)
// Canonicality check, matching AuthenticatedPeerStatePacket: a
// non-minimal encoding would let the same capability set travel
// as different bytes.
guard decoded.encoded() == value else { return nil }
capabilities = decoded
case .bridgeGeohash:
guard length <= 12, let text = String(data: value, encoding: .utf8) else { return nil }
bridgeGeohash = text
case nil:
// Unknown TLV: skip, for forward compatibility.
break
}
offset = data.index(valueStart, offsetBy: length)
}
guard let epoch, let tagBlock else { return nil }
return AnnounceV2Packet(
epoch: epoch,
tagBlock: tagBlock,
capabilities: capabilities,
bridgeGeohash: bridgeGeohash
)
}
}