Authenticate only completed Noise candidates

This commit is contained in:
jack
2026-07-25 23:49:41 +02:00
parent 96f8336094
commit 6edf4c3972
6 changed files with 213 additions and 23 deletions
+30 -3
View File
@@ -11,6 +11,11 @@ import CryptoKit
import Foundation
import BitFoundation
struct NoiseHandshakeProcessingResult {
let response: Data?
let didEstablishAuthenticatedSession: Bool
}
final class NoiseSessionManager {
private var sessions: [PeerID: NoiseSession] = [:]
/// A responder rehandshake must not evict a working transport session
@@ -112,6 +117,21 @@ final class NoiseSessionManager {
}
func handleIncomingHandshake(from peerID: PeerID, message: Data) throws -> Data? {
try handleIncomingHandshakeWithResult(
from: peerID,
message: message
).response
}
/// Processes one exact handshake candidate and reports whether that
/// candidate completed authenticated establishment. The peer's retained
/// session may already be established while a replacement is only on
/// message one, so callers must not infer candidate completion from the
/// peer-level session table.
func handleIncomingHandshakeWithResult(
from peerID: PeerID,
message: Data
) throws -> NoiseHandshakeProcessingResult {
// Process everything within the synchronized block to prevent race conditions
return try managerQueue.sync(flags: .barrier) {
let session: NoiseSession
@@ -164,8 +184,11 @@ final class NoiseSessionManager {
do {
let response = try session.processHandshakeMessage(message)
// Check if session is established after processing
if session.isEstablished() {
// Check the exact session that processed this message. A
// preserved peer-level session can remain established while a
// replacement candidate is still unauthenticated.
let didEstablishAuthenticatedSession = session.isEstablished()
if didEstablishAuthenticatedSession {
guard let remoteKey = session.getRemoteStaticPublicKey(),
authenticatedRemoteKey(remoteKey, matches: peerID) else {
throw NoiseSessionError.peerIdentityMismatch
@@ -185,7 +208,11 @@ final class NoiseSessionManager {
}
}
return response
return NoiseHandshakeProcessingResult(
response: response,
didEstablishAuthenticatedSession:
didEstablishAuthenticatedSession
)
} catch {
// A failed candidate is discarded without touching the
// established session. Ordinary failed handshakes retain the
@@ -2,6 +2,11 @@ import BitFoundation
import BitLogger
import Foundation
struct BLENoiseHandshakeHandlingResult {
let processed: Bool
let didEstablishAuthenticatedSession: Bool
}
/// Narrow environment for `BLENoisePacketHandler`.
///
/// All queue hops (collections barrier writes, main-actor UI notification)
@@ -16,8 +21,11 @@ struct BLENoisePacketHandlerEnvironment {
let messageTTL: UInt8
/// Current time source.
let now: () -> Date
/// Processes an inbound handshake message, returning an optional response payload (crypto).
let processHandshakeMessage: (_ peerID: PeerID, _ message: Data) throws -> Data?
/// Processes an inbound handshake message, returning its optional response
/// and whether that exact candidate authenticated (crypto).
let processHandshakeMessage:
(_ peerID: PeerID, _ message: Data) throws
-> NoiseHandshakeProcessingResult
/// Whether any Noise session (established or pending) exists for the peer (crypto).
let hasNoiseSession: (PeerID) -> Bool
/// Initiates a fresh Noise handshake with the peer (crypto + send).
@@ -54,12 +62,23 @@ final class BLENoisePacketHandler {
/// from a rejected candidate while an older session remains established.
@discardableResult
func handleHandshake(_ packet: BitchatPacket, from peerID: PeerID) -> Bool {
handleHandshakeWithResult(packet, from: peerID).processed
}
func handleHandshakeWithResult(
_ packet: BitchatPacket,
from peerID: PeerID
) -> BLENoiseHandshakeHandlingResult {
let env = environment
// Use NoiseEncryptionService for handshake processing
if PeerID(hexData: packet.recipientID) == env.localPeerID() {
// Handshake is for us
do {
if let response = try env.processHandshakeMessage(peerID, packet.payload) {
let result = try env.processHandshakeMessage(
peerID,
packet.payload
)
if let response = result.response {
// Send response
let responsePacket = BitchatPacket(
type: MessageType.noiseHandshake.rawValue,
@@ -76,7 +95,11 @@ final class BLENoisePacketHandler {
// Session establishment will trigger onPeerAuthenticated callback
// which will send any pending messages at the right time
return true
return BLENoiseHandshakeHandlingResult(
processed: true,
didEstablishAuthenticatedSession:
result.didEstablishAuthenticatedSession
)
} catch NoiseSessionError.peerIdentityMismatch {
// The candidate was already discarded by the session manager.
// Do not let a spoofed claimed ID trigger a fresh outbound
@@ -85,17 +108,26 @@ final class BLENoisePacketHandler {
"Rejected Noise handshake whose static key does not match \(peerID.id.prefix(8))",
category: .security
)
return false
return BLENoiseHandshakeHandlingResult(
processed: false,
didEstablishAuthenticatedSession: false
)
} catch {
SecureLogger.error("Failed to process handshake: \(error)")
// Try initiating a new handshake
if !env.hasNoiseSession(peerID) {
env.initiateHandshake(peerID)
}
return false
return BLENoiseHandshakeHandlingResult(
processed: false,
didEstablishAuthenticatedSession: false
)
}
}
return false
return BLENoiseHandshakeHandlingResult(
processed: false,
didEstablishAuthenticatedSession: false
)
}
func handleEncrypted(_ packet: BitchatPacket, from peerID: PeerID) {
+15 -10
View File
@@ -5770,15 +5770,11 @@ extension BLEService {
}
private func handleNoiseHandshake(_ packet: BitchatPacket, from peerID: PeerID) {
let wasEstablished = noiseService.hasEstablishedSession(with: peerID)
let processed = noisePacketHandler.handleHandshake(packet, from: peerID)
let isEstablished = noiseService.hasEstablishedSession(with: peerID)
// XX message 1 is exactly the unauthenticated 32-byte ephemeral key.
// While replacing an existing session, do not authenticate its ingress
// link until a later message completes and validates the candidate.
let completedAuthenticatedHandshake = !wasEstablished
|| packet.payload.count != NoiseSecurityConstants.xxInitialMessageSize
if processed, isEstablished, completedAuthenticatedHandshake {
let result = noisePacketHandler.handleHandshakeWithResult(
packet,
from: peerID
)
if result.didEstablishAuthenticatedSession {
markNoiseAuthenticatedIngressLink(for: packet, peerID: peerID)
}
}
@@ -5801,7 +5797,16 @@ extension BLEService {
messageTTL: messageTTL,
now: { Date() },
processHandshakeMessage: { [weak self] peerID, message in
try self?.noiseService.processHandshakeMessage(from: peerID, message: message)
guard let self else {
return NoiseHandshakeProcessingResult(
response: nil,
didEstablishAuthenticatedSession: false
)
}
return try self.noiseService.processHandshakeMessageWithResult(
from: peerID,
message: message
)
},
hasNoiseSession: { [weak self] peerID in
self?.noiseService.hasSession(with: peerID) ?? false
+17 -2
View File
@@ -664,6 +664,18 @@ final class NoiseEncryptionService {
/// Process an incoming handshake message
func processHandshakeMessage(from peerID: PeerID, message: Data) throws -> Data? {
try processHandshakeMessageWithResult(
from: peerID,
message: message
).response
}
/// Process an incoming handshake message and report whether the exact
/// session that consumed it completed authenticated establishment.
func processHandshakeMessageWithResult(
from peerID: PeerID,
message: Data
) throws -> NoiseHandshakeProcessingResult {
// Validate peer ID
guard peerID.isValid else {
@@ -685,11 +697,14 @@ final class NoiseEncryptionService {
// For handshakes, we process the raw data directly without NoiseMessage wrapper
// The Noise protocol handles its own message format
let responsePayload = try sessionManager.handleIncomingHandshake(from: peerID, message: message)
let result = try sessionManager.handleIncomingHandshakeWithResult(
from: peerID,
message: message
)
// Return raw response without wrapper
return responsePayload
return result
}
/// Check if we have an established session with a peer