From b533d9560d205cf291ac616f14bfa21a60551085 Mon Sep 17 00:00:00 2001 From: jack Date: Mon, 12 Jan 2026 11:32:17 -1000 Subject: [PATCH] Fix: Capture handshake hash before split() clears symmetric state Addresses Codex review feedback on PR #948: the getTransportCiphers() method now returns the handshake hash as part of its return tuple, capturing it BEFORE split() calls clearSensitiveData(). Previously, calling getHandshakeHash() after getTransportCiphers() would return an all-zero value since split() clears the symmetric state. This broke channel binding functionality. Co-Authored-By: Claude Opus 4.5 --- bitchat/Noise/NoiseProtocol.swift | 12 ++++++++---- bitchat/Noise/NoiseSession.swift | 30 +++++++++++++++--------------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/bitchat/Noise/NoiseProtocol.swift b/bitchat/Noise/NoiseProtocol.swift index aa5f35be..b49baa0a 100644 --- a/bitchat/Noise/NoiseProtocol.swift +++ b/bitchat/Noise/NoiseProtocol.swift @@ -833,16 +833,20 @@ final class NoiseHandshakeState { return currentPattern >= messagePatterns.count } - func getTransportCiphers(useExtractedNonce: Bool) throws -> (send: NoiseCipherState, receive: NoiseCipherState) { + func getTransportCiphers(useExtractedNonce: Bool) throws -> (send: NoiseCipherState, receive: NoiseCipherState, handshakeHash: Data) { guard isHandshakeComplete() else { throw NoiseError.handshakeNotComplete } - + + // BCH-01-010: Capture handshake hash BEFORE split() clears symmetric state + let finalHandshakeHash = symmetricState.getHandshakeHash() + let (c1, c2) = symmetricState.split(useExtractedNonce: useExtractedNonce) - + // Initiator uses c1 for sending, c2 for receiving // Responder uses c2 for sending, c1 for receiving - return role == .initiator ? (c1, c2) : (c2, c1) + let ciphers = role == .initiator ? (c1, c2) : (c2, c1) + return (send: ciphers.0, receive: ciphers.1, handshakeHash: finalHandshakeHash) } func getRemoteStaticPublicKey() -> Curve25519.KeyAgreement.PublicKey? { diff --git a/bitchat/Noise/NoiseSession.swift b/bitchat/Noise/NoiseSession.swift index 8c84f85a..0b96a124 100644 --- a/bitchat/Noise/NoiseSession.swift +++ b/bitchat/Noise/NoiseSession.swift @@ -102,23 +102,23 @@ class NoiseSession { // Check if handshake is complete if handshake.isHandshakeComplete() { - // Get transport ciphers - let (send, receive) = try handshake.getTransportCiphers(useExtractedNonce: true) + // Get transport ciphers and handshake hash (hash captured before split clears state) + let (send, receive, hash) = try handshake.getTransportCiphers(useExtractedNonce: true) sendCipher = send receiveCipher = receive - + // Store remote static key remoteStaticPublicKey = handshake.getRemoteStaticPublicKey() - + // Store handshake hash for channel binding - handshakeHash = handshake.getHandshakeHash() - + handshakeHash = hash + state = .established handshakeState = nil // Clear handshake state - + SecureLogger.debug("NoiseSession[\(peerID)]: Handshake complete (no response needed), transitioning to established") SecureLogger.info(.handshakeCompleted(peerID: peerID.id)) - + return nil } else { // Generate response @@ -128,20 +128,20 @@ class NoiseSession { // Check if handshake is complete after writing if handshake.isHandshakeComplete() { - // Get transport ciphers - let (send, receive) = try handshake.getTransportCiphers(useExtractedNonce: true) + // Get transport ciphers and handshake hash (hash captured before split clears state) + let (send, receive, hash) = try handshake.getTransportCiphers(useExtractedNonce: true) sendCipher = send receiveCipher = receive - + // Store remote static key remoteStaticPublicKey = handshake.getRemoteStaticPublicKey() - + // Store handshake hash for channel binding - handshakeHash = handshake.getHandshakeHash() - + handshakeHash = hash + state = .established handshakeState = nil // Clear handshake state - + SecureLogger.debug("NoiseSession[\(peerID)]: Handshake complete after writing response, transitioning to established") SecureLogger.info(.handshakeCompleted(peerID: peerID.id)) }