mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 20:45:19 +00:00
Bind capability state to Noise generations
This commit is contained in:
@@ -18,6 +18,10 @@ struct NoiseHandshakeProcessingResult {
|
||||
|
||||
final class NoiseSessionManager {
|
||||
private var sessions: [PeerID: NoiseSession] = [:]
|
||||
/// Opaque identity for each exact entry in `sessions`. The generation is
|
||||
/// created and removed under the same barrier as the session itself, so a
|
||||
/// caller can never authenticate data with one session and lease another.
|
||||
private var sessionGenerations: [PeerID: UUID] = [:]
|
||||
/// A responder rehandshake must not evict a working transport session
|
||||
/// before the candidate proves that its authenticated static key belongs
|
||||
/// to the claimed wire ID. Candidates therefore live outside `sessions`
|
||||
@@ -27,7 +31,7 @@ final class NoiseSessionManager {
|
||||
private let managerQueue = DispatchQueue(label: "chat.bitchat.noise.manager", attributes: .concurrent)
|
||||
|
||||
// Callbacks
|
||||
var onSessionEstablished: ((PeerID, Curve25519.KeyAgreement.PublicKey) -> Void)?
|
||||
var onSessionEstablished: ((PeerID, Curve25519.KeyAgreement.PublicKey, UUID) -> Void)?
|
||||
var onSessionFailed: ((PeerID, Error) -> Void)?
|
||||
|
||||
init(localStaticKey: Curve25519.KeyAgreement.PrivateKey, keychain: KeychainManagerProtocol) {
|
||||
@@ -64,6 +68,7 @@ final class NoiseSessionManager {
|
||||
if let session = sessions.removeValue(forKey: peerID) {
|
||||
session.reset() // Clear sensitive data before removing
|
||||
}
|
||||
sessionGenerations.removeValue(forKey: peerID)
|
||||
if let candidate = responderCandidates.removeValue(forKey: peerID) {
|
||||
candidate.reset()
|
||||
}
|
||||
@@ -79,6 +84,7 @@ final class NoiseSessionManager {
|
||||
candidate.reset()
|
||||
}
|
||||
sessions.removeAll()
|
||||
sessionGenerations.removeAll()
|
||||
responderCandidates.removeAll()
|
||||
}
|
||||
}
|
||||
@@ -96,12 +102,14 @@ final class NoiseSessionManager {
|
||||
// Remove any existing non-established session
|
||||
if let existingSession = sessions[peerID], !existingSession.isEstablished() {
|
||||
_ = sessions.removeValue(forKey: peerID)
|
||||
sessionGenerations.removeValue(forKey: peerID)
|
||||
existingSession.reset()
|
||||
}
|
||||
|
||||
// Create new initiator session
|
||||
let session = sessionFactory(peerID, .initiator)
|
||||
sessions[peerID] = session
|
||||
sessionGenerations[peerID] = UUID()
|
||||
|
||||
do {
|
||||
let handshakeData = try session.startHandshake()
|
||||
@@ -109,6 +117,7 @@ final class NoiseSessionManager {
|
||||
} catch {
|
||||
// Clean up failed session
|
||||
_ = sessions.removeValue(forKey: peerID)
|
||||
sessionGenerations.removeValue(forKey: peerID)
|
||||
session.reset()
|
||||
SecureLogger.error(.handshakeFailed(peerID: peerID.id, error: error.localizedDescription))
|
||||
throw error
|
||||
@@ -132,8 +141,17 @@ final class NoiseSessionManager {
|
||||
from peerID: PeerID,
|
||||
message: Data
|
||||
) throws -> NoiseHandshakeProcessingResult {
|
||||
// Process everything within the synchronized block to prevent race conditions
|
||||
return try managerQueue.sync(flags: .barrier) {
|
||||
// Process everything within the synchronized block to prevent race conditions.
|
||||
// Return establishment metadata and publish the callback only after the
|
||||
// manager barrier is released, avoiding both a deadlock and a window in
|
||||
// which `processHandshakeMessage` returns before authentication state.
|
||||
let result: (
|
||||
response: Data?,
|
||||
establishedSession: (
|
||||
remoteKey: Curve25519.KeyAgreement.PublicKey,
|
||||
generation: UUID
|
||||
)?
|
||||
) = try managerQueue.sync(flags: .barrier) {
|
||||
let session: NoiseSession
|
||||
let isReplacementCandidate: Bool
|
||||
|
||||
@@ -164,9 +182,11 @@ final class NoiseSessionManager {
|
||||
// No established transport state exists to preserve. A
|
||||
// fresh initiation replaces the incomplete handshake.
|
||||
_ = sessions.removeValue(forKey: peerID)
|
||||
sessionGenerations.removeValue(forKey: peerID)
|
||||
existing.reset()
|
||||
let replacement = sessionFactory(peerID, .responder)
|
||||
sessions[peerID] = replacement
|
||||
sessionGenerations[peerID] = UUID()
|
||||
session = replacement
|
||||
isReplacementCandidate = false
|
||||
} else {
|
||||
@@ -176,6 +196,7 @@ final class NoiseSessionManager {
|
||||
} else {
|
||||
let newSession = sessionFactory(peerID, .responder)
|
||||
sessions[peerID] = newSession
|
||||
sessionGenerations[peerID] = UUID()
|
||||
session = newSession
|
||||
isReplacementCandidate = false
|
||||
}
|
||||
@@ -187,8 +208,11 @@ final class NoiseSessionManager {
|
||||
// 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 {
|
||||
var establishedSession: (
|
||||
remoteKey: Curve25519.KeyAgreement.PublicKey,
|
||||
generation: UUID
|
||||
)?
|
||||
if session.isEstablished() {
|
||||
guard let remoteKey = session.getRemoteStaticPublicKey(),
|
||||
authenticatedRemoteKey(remoteKey, matches: peerID) else {
|
||||
throw NoiseSessionError.peerIdentityMismatch
|
||||
@@ -197,22 +221,18 @@ final class NoiseSessionManager {
|
||||
if isReplacementCandidate {
|
||||
_ = responderCandidates.removeValue(forKey: peerID)
|
||||
let previous = sessions.updateValue(session, forKey: peerID)
|
||||
sessionGenerations[peerID] = UUID()
|
||||
if let previous, previous !== session {
|
||||
previous.reset()
|
||||
}
|
||||
}
|
||||
|
||||
// Schedule callback outside the synchronized block to prevent deadlock
|
||||
DispatchQueue.global().async { [weak self] in
|
||||
self?.onSessionEstablished?(peerID, remoteKey)
|
||||
guard let generation = sessionGenerations[peerID] else {
|
||||
throw NoiseEncryptionError.sessionNotEstablished
|
||||
}
|
||||
establishedSession = (remoteKey, generation)
|
||||
}
|
||||
|
||||
return NoiseHandshakeProcessingResult(
|
||||
response: response,
|
||||
didEstablishAuthenticatedSession:
|
||||
didEstablishAuthenticatedSession
|
||||
)
|
||||
return (response, establishedSession)
|
||||
} catch {
|
||||
// A failed candidate is discarded without touching the
|
||||
// established session. Ordinary failed handshakes retain the
|
||||
@@ -225,6 +245,7 @@ final class NoiseSessionManager {
|
||||
} else if let storedSession = sessions[peerID],
|
||||
storedSession === session {
|
||||
_ = sessions.removeValue(forKey: peerID)
|
||||
sessionGenerations.removeValue(forKey: peerID)
|
||||
}
|
||||
session.reset()
|
||||
|
||||
@@ -237,6 +258,14 @@ final class NoiseSessionManager {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
if let established = result.establishedSession {
|
||||
onSessionEstablished?(peerID, established.remoteKey, established.generation)
|
||||
}
|
||||
return NoiseHandshakeProcessingResult(
|
||||
response: result.response,
|
||||
didEstablishAuthenticatedSession: result.establishedSession != nil
|
||||
)
|
||||
}
|
||||
|
||||
/// Mesh handshakes normally use a 16-hex wire ID. Full Noise-key IDs are
|
||||
@@ -260,19 +289,74 @@ final class NoiseSessionManager {
|
||||
// MARK: - Encryption/Decryption
|
||||
|
||||
func encrypt(_ plaintext: Data, for peerID: PeerID) throws -> Data {
|
||||
guard let session = getSession(for: peerID) else {
|
||||
throw NoiseSessionError.sessionNotFound
|
||||
try managerQueue.sync {
|
||||
guard let session = sessions[peerID] else {
|
||||
throw NoiseSessionError.sessionNotFound
|
||||
}
|
||||
return try session.encrypt(plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
/// Encrypts only if `expected` still names the current established entry.
|
||||
/// A rekey between capability proof and media encryption therefore fails
|
||||
/// closed instead of sending on an unproven replacement session.
|
||||
func encrypt(
|
||||
_ plaintext: Data,
|
||||
for peerID: PeerID,
|
||||
expectedSessionGeneration expected: UUID
|
||||
) throws -> Data {
|
||||
try managerQueue.sync {
|
||||
guard let session = sessions[peerID],
|
||||
session.isEstablished(),
|
||||
sessionGenerations[peerID] == expected else {
|
||||
throw NoiseEncryptionError.sessionNotEstablished
|
||||
}
|
||||
return try session.encrypt(plaintext)
|
||||
}
|
||||
|
||||
return try session.encrypt(plaintext)
|
||||
}
|
||||
|
||||
func decrypt(_ ciphertext: Data, from peerID: PeerID) throws -> Data {
|
||||
guard let session = getSession(for: peerID) else {
|
||||
throw NoiseSessionError.sessionNotFound
|
||||
try decryptWithSessionGeneration(ciphertext, from: peerID).plaintext
|
||||
}
|
||||
|
||||
func sessionGeneration(for peerID: PeerID) -> UUID? {
|
||||
managerQueue.sync {
|
||||
guard sessions[peerID]?.isEstablished() == true else { return nil }
|
||||
return sessionGenerations[peerID]
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrypts while holding the manager's read lease. Session promotion and
|
||||
/// removal require its barrier, so the returned generation always names
|
||||
/// the exact session object that authenticated these bytes.
|
||||
func decryptWithSessionGeneration(
|
||||
_ ciphertext: Data,
|
||||
from peerID: PeerID
|
||||
) throws -> (plaintext: Data, sessionGeneration: UUID) {
|
||||
try managerQueue.sync {
|
||||
guard let session = sessions[peerID] else {
|
||||
throw NoiseSessionError.sessionNotFound
|
||||
}
|
||||
guard session.isEstablished(),
|
||||
let generation = sessionGenerations[peerID] else {
|
||||
throw NoiseEncryptionError.sessionNotEstablished
|
||||
}
|
||||
return (try session.decrypt(ciphertext), generation)
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs a state commit under a read lease for the exact established
|
||||
/// session. Rekey, replacement, and removal all need the same barrier.
|
||||
func withCurrentSessionGeneration<Result>(
|
||||
for peerID: PeerID,
|
||||
expected: UUID,
|
||||
_ body: () -> Result
|
||||
) -> Result? {
|
||||
managerQueue.sync {
|
||||
guard sessions[peerID]?.isEstablished() == true,
|
||||
sessionGenerations[peerID] == expected else { return nil }
|
||||
return body()
|
||||
}
|
||||
|
||||
return try session.decrypt(ciphertext)
|
||||
}
|
||||
|
||||
// MARK: - Key Management
|
||||
|
||||
Reference in New Issue
Block a user