mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 22:45:19 +00:00
Periphery 3.7.4 audit of both schemes (macOS + iOS, intersected so platform-specific code is never touched), with test targets indexed and the share extension built. 277 dead declarations removed or demoted: dead forwarding wrappers (ChatViewModel+Nostr/+PrivateChat), removed- feature remnants (autocomplete command suggestions, back-swipe tuning, MediaSendError, GeohashParticipantTracker), unused Tor dormancy bindings, assign-only properties, unused parameters (renamed to _), and redundant public accessibility. 13 orphaned localization keys deleted across all 29 locales (old pre-#1392 location-notes UI, app_info warnings). Two real tests were flagged as unused because they never ran: Swift Testing methods missing @Test (NostrProtocolTests. testAckRoundTripNIP44V2_Delivered, NotificationStreamAssemblerTests. testAssemblesCompressedLargeFrame). Re-armed both; they pass. Deliberately kept, now recorded in .periphery.baseline.json: iOS-only code invisible to the CI macOS scan, C FFI signatures, keep-alive NWPathMonitor reference, InboundEventKey.eventID (dedup semantics), wifiBulk capability bit (reserved for Wi-Fi bulk work, used by BitFoundation package tests), and the String secureClear cluster (exercised by package tests). New: .periphery.yml config and an advisory Dead Code CI job (mirrors the SwiftLint precedent from #1361) that fails on findings not in the committed baseline. Verified: full macOS app suite, BitFoundation (119) and BitLogger (13) package tests green; periphery scan --strict exits clean. Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
218 lines
7.9 KiB
Swift
218 lines
7.9 KiB
Swift
//
|
|
// NoiseSessionManager.swift
|
|
// bitchat
|
|
//
|
|
// This is free and unencumbered software released into the public domain.
|
|
// For more information, see <https://unlicense.org>
|
|
//
|
|
|
|
import BitLogger
|
|
import CryptoKit
|
|
import Foundation
|
|
import BitFoundation
|
|
|
|
final class NoiseSessionManager {
|
|
private var sessions: [PeerID: NoiseSession] = [:]
|
|
private let sessionFactory: (PeerID, NoiseRole) -> NoiseSession
|
|
private let managerQueue = DispatchQueue(label: "chat.bitchat.noise.manager", attributes: .concurrent)
|
|
|
|
// Callbacks
|
|
var onSessionEstablished: ((PeerID, Curve25519.KeyAgreement.PublicKey) -> Void)?
|
|
var onSessionFailed: ((PeerID, Error) -> Void)?
|
|
|
|
init(localStaticKey: Curve25519.KeyAgreement.PrivateKey, keychain: KeychainManagerProtocol) {
|
|
self.sessionFactory = { peerID, role in
|
|
SecureNoiseSession(
|
|
peerID: peerID,
|
|
role: role,
|
|
keychain: keychain,
|
|
localStaticKey: localStaticKey
|
|
)
|
|
}
|
|
}
|
|
|
|
#if DEBUG
|
|
init(
|
|
localStaticKey _: Curve25519.KeyAgreement.PrivateKey,
|
|
keychain _: KeychainManagerProtocol,
|
|
sessionFactory: @escaping (PeerID, NoiseRole) -> NoiseSession
|
|
) {
|
|
self.sessionFactory = sessionFactory
|
|
}
|
|
#endif
|
|
|
|
// MARK: - Session Management
|
|
|
|
func getSession(for peerID: PeerID) -> NoiseSession? {
|
|
return managerQueue.sync {
|
|
return sessions[peerID]
|
|
}
|
|
}
|
|
|
|
func removeSession(for peerID: PeerID) {
|
|
managerQueue.sync(flags: .barrier) {
|
|
if let session = sessions.removeValue(forKey: peerID) {
|
|
session.reset() // Clear sensitive data before removing
|
|
}
|
|
}
|
|
}
|
|
|
|
func removeAllSessions() {
|
|
managerQueue.sync(flags: .barrier) {
|
|
for (_, session) in sessions {
|
|
session.reset()
|
|
}
|
|
sessions.removeAll()
|
|
}
|
|
}
|
|
|
|
// MARK: - Handshake Helpers
|
|
|
|
func initiateHandshake(with peerID: PeerID) throws -> Data {
|
|
return try managerQueue.sync(flags: .barrier) {
|
|
// Check if we already have an established session
|
|
if let existingSession = sessions[peerID], existingSession.isEstablished() {
|
|
// Session already established, don't recreate
|
|
throw NoiseSessionError.alreadyEstablished
|
|
}
|
|
|
|
// Remove any existing non-established session
|
|
if let existingSession = sessions[peerID], !existingSession.isEstablished() {
|
|
_ = sessions.removeValue(forKey: peerID)
|
|
}
|
|
|
|
// Create new initiator session
|
|
let session = sessionFactory(peerID, .initiator)
|
|
sessions[peerID] = session
|
|
|
|
do {
|
|
let handshakeData = try session.startHandshake()
|
|
return handshakeData
|
|
} catch {
|
|
// Clean up failed session
|
|
_ = sessions.removeValue(forKey: peerID)
|
|
SecureLogger.error(.handshakeFailed(peerID: peerID.id, error: error.localizedDescription))
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
func handleIncomingHandshake(from peerID: PeerID, message: Data) throws -> Data? {
|
|
// Process everything within the synchronized block to prevent race conditions
|
|
return try managerQueue.sync(flags: .barrier) {
|
|
var shouldCreateNew = false
|
|
var existingSession: NoiseSession? = nil
|
|
|
|
if let existing = sessions[peerID] {
|
|
// If we have an established session, the peer must have cleared their session
|
|
// for a good reason (e.g., decryption failure, restart, etc.)
|
|
// We should accept the new handshake to re-establish encryption
|
|
if existing.isEstablished() {
|
|
SecureLogger.info("Accepting handshake from \(peerID) despite existing session - peer likely cleared their session", category: .session)
|
|
_ = sessions.removeValue(forKey: peerID)
|
|
shouldCreateNew = true
|
|
} else {
|
|
// If we're in the middle of a handshake and receive a new initiation,
|
|
// reset and start fresh (the other side may have restarted)
|
|
if existing.getState() == .handshaking && message.count == 32 {
|
|
_ = sessions.removeValue(forKey: peerID)
|
|
shouldCreateNew = true
|
|
} else {
|
|
existingSession = existing
|
|
}
|
|
}
|
|
} else {
|
|
shouldCreateNew = true
|
|
}
|
|
|
|
// Get or create session
|
|
let session: NoiseSession
|
|
if shouldCreateNew {
|
|
let newSession = sessionFactory(peerID, .responder)
|
|
sessions[peerID] = newSession
|
|
session = newSession
|
|
} else {
|
|
session = existingSession!
|
|
}
|
|
|
|
// Process the handshake message within the synchronized block
|
|
do {
|
|
let response = try session.processHandshakeMessage(message)
|
|
|
|
// Check if session is established after processing
|
|
if session.isEstablished() {
|
|
if let remoteKey = session.getRemoteStaticPublicKey() {
|
|
// Schedule callback outside the synchronized block to prevent deadlock
|
|
DispatchQueue.global().async { [weak self] in
|
|
self?.onSessionEstablished?(peerID, remoteKey)
|
|
}
|
|
}
|
|
}
|
|
|
|
return response
|
|
} catch {
|
|
// Reset the session on handshake failure so next attempt can start fresh
|
|
_ = sessions.removeValue(forKey: peerID)
|
|
|
|
// Schedule callback outside the synchronized block to prevent deadlock
|
|
DispatchQueue.global().async { [weak self] in
|
|
self?.onSessionFailed?(peerID, error)
|
|
}
|
|
|
|
SecureLogger.error(.handshakeFailed(peerID: peerID.id, error: error.localizedDescription))
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Encryption/Decryption
|
|
|
|
func encrypt(_ plaintext: Data, for peerID: PeerID) throws -> Data {
|
|
guard let session = getSession(for: peerID) else {
|
|
throw NoiseSessionError.sessionNotFound
|
|
}
|
|
|
|
return try session.encrypt(plaintext)
|
|
}
|
|
|
|
func decrypt(_ ciphertext: Data, from peerID: PeerID) throws -> Data {
|
|
guard let session = getSession(for: peerID) else {
|
|
throw NoiseSessionError.sessionNotFound
|
|
}
|
|
|
|
return try session.decrypt(ciphertext)
|
|
}
|
|
|
|
// MARK: - Key Management
|
|
|
|
func getRemoteStaticKey(for peerID: PeerID) -> Curve25519.KeyAgreement.PublicKey? {
|
|
return getSession(for: peerID)?.getRemoteStaticPublicKey()
|
|
}
|
|
|
|
// MARK: - Session Rekeying
|
|
|
|
func getSessionsNeedingRekey() -> [(peerID: PeerID, needsRekey: Bool)] {
|
|
return managerQueue.sync {
|
|
var needingRekey: [(peerID: PeerID, needsRekey: Bool)] = []
|
|
|
|
for (peerID, session) in sessions {
|
|
if let secureSession = session as? SecureNoiseSession,
|
|
secureSession.isEstablished(),
|
|
secureSession.needsRenegotiation() {
|
|
needingRekey.append((peerID: peerID, needsRekey: true))
|
|
}
|
|
}
|
|
|
|
return needingRekey
|
|
}
|
|
}
|
|
|
|
func initiateRekey(for peerID: PeerID) throws {
|
|
// Remove old session
|
|
removeSession(for: peerID)
|
|
|
|
// Initiate new handshake
|
|
_ = try initiateHandshake(with: peerID)
|
|
}
|
|
}
|