mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 19:45:22 +00:00
* Bound public rate-limit buckets against attacker-keyed growth. Keep the NIP-13 PoW sender bypass, skip content-bucket minting on sender reject, and evict idle/oldest entries at a hard cap. Co-authored-by: Cursor <cursoragent@cursor.com> * Stop Cashu-looking text from skipping long-message guards. Oversized public content always collapses and takes the plain formatting path so remote tokens cannot force layout/regex DoS. Co-authored-by: Cursor <cursoragent@cursor.com> * Cap teleported geohash participant markers. Bound the set with FIFO eviction, clear it on channel switch, and prune markers that leave the visible participant list. Co-authored-by: Cursor <cursoragent@cursor.com> * Bound untrusted Nostr relay frames and event tags. Reject oversized inbound messages before JSON parse, cap tag arrays/values at decode, and stop logging raw tag contents. Co-authored-by: Cursor <cursoragent@cursor.com> * Fail soft when Noise handshake state is unexpectedly missing. Replace the initiator startHandshake force unwrap with a guard that throws invalidState instead of crashing. Co-authored-by: Cursor <cursoragent@cursor.com> * Cap geohash nickname cache from remote Nostr events. FIFO-evict at capacity, clear on channel switch, and prune nicknames that leave the visible participant list. Co-authored-by: Cursor <cursoragent@cursor.com> * Avoid overlapping exclusive access in the rate limiter. Make bucket helpers static so inout dictionary updates do not conflict with a mutating call on self. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix rate-limiter tests for mutating allow under #expect. Call allow outside the macro so Swift Testing does not capture an immutable copy of the struct. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop unused WebSocket data helper; reset rate limiter on panic wipe. dataWithinInboundLimit replaced the unbounded path, and panic clear should not leave public intake buckets behind. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
234 lines
8.2 KiB
Swift
234 lines
8.2 KiB
Swift
//
|
|
// NoiseSession.swift
|
|
// bitchat
|
|
//
|
|
// This is free and unencumbered software released into the public domain.
|
|
// For more information, see <https://unlicense.org>
|
|
//
|
|
|
|
import BitLogger
|
|
import Foundation
|
|
import CryptoKit
|
|
import BitFoundation
|
|
|
|
class NoiseSession {
|
|
let peerID: PeerID
|
|
let role: NoiseRole
|
|
private let keychain: KeychainManagerProtocol
|
|
private var state: NoiseSessionState = .uninitialized
|
|
private var handshakeState: NoiseHandshakeState?
|
|
private var sendCipher: NoiseCipherState?
|
|
private var receiveCipher: NoiseCipherState?
|
|
|
|
// Keys
|
|
private let localStaticKey: Curve25519.KeyAgreement.PrivateKey
|
|
private var remoteStaticPublicKey: Curve25519.KeyAgreement.PublicKey?
|
|
|
|
// Handshake messages for retransmission
|
|
private var sentHandshakeMessages: [Data] = []
|
|
private var handshakeHash: Data?
|
|
|
|
// Thread safety
|
|
private let sessionQueue = DispatchQueue(label: "chat.bitchat.noise.session", attributes: .concurrent)
|
|
|
|
init(
|
|
peerID: PeerID,
|
|
role: NoiseRole,
|
|
keychain: KeychainManagerProtocol,
|
|
localStaticKey: Curve25519.KeyAgreement.PrivateKey,
|
|
remoteStaticKey: Curve25519.KeyAgreement.PublicKey? = nil
|
|
) {
|
|
self.peerID = peerID
|
|
self.role = role
|
|
self.keychain = keychain
|
|
self.localStaticKey = localStaticKey
|
|
self.remoteStaticPublicKey = remoteStaticKey
|
|
}
|
|
|
|
// MARK: - Handshake
|
|
|
|
func startHandshake() throws -> Data {
|
|
return try sessionQueue.sync(flags: .barrier) {
|
|
guard case .uninitialized = state else {
|
|
throw NoiseSessionError.invalidState
|
|
}
|
|
|
|
// For XX pattern, we don't need remote static key upfront
|
|
handshakeState = NoiseHandshakeState(
|
|
role: role,
|
|
pattern: .XX,
|
|
keychain: keychain,
|
|
localStaticKey: localStaticKey,
|
|
remoteStaticKey: nil
|
|
)
|
|
|
|
state = .handshaking
|
|
|
|
// Only initiator writes the first message
|
|
if role == .initiator {
|
|
guard let handshake = handshakeState else {
|
|
throw NoiseSessionError.invalidState
|
|
}
|
|
let message = try handshake.writeMessage()
|
|
sentHandshakeMessages.append(message)
|
|
return message
|
|
} else {
|
|
// Responder doesn't send first message in XX pattern
|
|
return Data()
|
|
}
|
|
}
|
|
}
|
|
|
|
func processHandshakeMessage(_ message: Data) throws -> Data? {
|
|
return try sessionQueue.sync(flags: .barrier) {
|
|
SecureLogger.debug("NoiseSession[\(peerID)]: Processing handshake message, current state: \(state), role: \(role)")
|
|
|
|
// Initialize handshake state if needed (for responders)
|
|
if state == .uninitialized && role == .responder {
|
|
handshakeState = NoiseHandshakeState(
|
|
role: role,
|
|
pattern: .XX,
|
|
keychain: keychain,
|
|
localStaticKey: localStaticKey,
|
|
remoteStaticKey: nil
|
|
)
|
|
state = .handshaking
|
|
SecureLogger.debug("NoiseSession[\(peerID)]: Initialized handshake state for responder")
|
|
}
|
|
|
|
guard case .handshaking = state, let handshake = handshakeState else {
|
|
throw NoiseSessionError.invalidState
|
|
}
|
|
|
|
// Process incoming message
|
|
_ = try handshake.readMessage(message)
|
|
SecureLogger.debug("NoiseSession[\(peerID)]: Read handshake message, checking if complete")
|
|
|
|
// Check if handshake is complete
|
|
if handshake.isHandshakeComplete() {
|
|
// 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 = 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
|
|
let response = try handshake.writeMessage()
|
|
sentHandshakeMessages.append(response)
|
|
SecureLogger.debug("NoiseSession[\(peerID)]: Generated handshake response of size \(response.count)")
|
|
|
|
// Check if handshake is complete after writing
|
|
if handshake.isHandshakeComplete() {
|
|
// 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 = 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))
|
|
}
|
|
|
|
return response
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Transport
|
|
|
|
func encrypt(_ plaintext: Data) throws -> Data {
|
|
return try sessionQueue.sync(flags: .barrier) {
|
|
guard case .established = state, let cipher = sendCipher else {
|
|
throw NoiseSessionError.notEstablished
|
|
}
|
|
|
|
return try cipher.encrypt(plaintext: plaintext)
|
|
}
|
|
}
|
|
|
|
func decrypt(_ ciphertext: Data) throws -> Data {
|
|
return try sessionQueue.sync(flags: .barrier) {
|
|
guard case .established = state, let cipher = receiveCipher else {
|
|
throw NoiseSessionError.notEstablished
|
|
}
|
|
|
|
return try cipher.decrypt(ciphertext: ciphertext)
|
|
}
|
|
}
|
|
|
|
// MARK: - State Management
|
|
|
|
func getState() -> NoiseSessionState {
|
|
return sessionQueue.sync {
|
|
return state
|
|
}
|
|
}
|
|
|
|
func isEstablished() -> Bool {
|
|
return sessionQueue.sync {
|
|
if case .established = state {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
func getRemoteStaticPublicKey() -> Curve25519.KeyAgreement.PublicKey? {
|
|
return sessionQueue.sync {
|
|
return remoteStaticPublicKey
|
|
}
|
|
}
|
|
|
|
func reset() {
|
|
sessionQueue.sync(flags: .barrier) {
|
|
let wasEstablished = state == .established
|
|
state = .uninitialized
|
|
handshakeState = nil
|
|
|
|
// Clear sensitive cipher states
|
|
sendCipher?.clearSensitiveData()
|
|
receiveCipher?.clearSensitiveData()
|
|
sendCipher = nil
|
|
receiveCipher = nil
|
|
|
|
// Clear sent handshake messages
|
|
for i in 0..<sentHandshakeMessages.count {
|
|
var message = sentHandshakeMessages[i]
|
|
keychain.secureClear(&message)
|
|
}
|
|
sentHandshakeMessages.removeAll()
|
|
|
|
// Clear handshake hash
|
|
if var hash = handshakeHash {
|
|
keychain.secureClear(&hash)
|
|
}
|
|
handshakeHash = nil
|
|
|
|
if wasEstablished {
|
|
SecureLogger.info(.sessionExpired(peerID: peerID.id))
|
|
}
|
|
}
|
|
}
|
|
}
|