mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 21:05:20 +00:00
Extract Noise into a dedicated module
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,96 +0,0 @@
|
||||
//
|
||||
// NoiseRateLimiter.swift
|
||||
// bitchat
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import BitLogger
|
||||
import BitFoundation
|
||||
import Foundation
|
||||
|
||||
final class NoiseRateLimiter {
|
||||
private var handshakeTimestamps: [PeerID: [Date]] = [:]
|
||||
private var messageTimestamps: [PeerID: [Date]] = [:]
|
||||
|
||||
// Global rate limiting
|
||||
private var globalHandshakeTimestamps: [Date] = []
|
||||
private var globalMessageTimestamps: [Date] = []
|
||||
|
||||
private let queue = DispatchQueue(label: "chat.bitchat.noise.ratelimit", attributes: .concurrent)
|
||||
|
||||
func allowHandshake(from peerID: PeerID) -> Bool {
|
||||
return queue.sync(flags: .barrier) {
|
||||
let now = Date()
|
||||
let oneMinuteAgo = now.addingTimeInterval(-60)
|
||||
|
||||
// Check global rate limit first
|
||||
globalHandshakeTimestamps = globalHandshakeTimestamps.filter { $0 > oneMinuteAgo }
|
||||
if globalHandshakeTimestamps.count >= NoiseSecurityConstants.maxGlobalHandshakesPerMinute {
|
||||
SecureLogger.warning("Global handshake rate limit exceeded: \(globalHandshakeTimestamps.count)/\(NoiseSecurityConstants.maxGlobalHandshakesPerMinute) per minute", category: .security)
|
||||
return false
|
||||
}
|
||||
|
||||
// Check per-peer rate limit
|
||||
var timestamps = handshakeTimestamps[peerID] ?? []
|
||||
timestamps = timestamps.filter { $0 > oneMinuteAgo }
|
||||
|
||||
if timestamps.count >= NoiseSecurityConstants.maxHandshakesPerMinute {
|
||||
SecureLogger.warning("Per-peer handshake rate limit exceeded for \(peerID): \(timestamps.count)/\(NoiseSecurityConstants.maxHandshakesPerMinute) per minute", category: .security)
|
||||
return false
|
||||
}
|
||||
|
||||
// Record new handshake
|
||||
timestamps.append(now)
|
||||
handshakeTimestamps[peerID] = timestamps
|
||||
globalHandshakeTimestamps.append(now)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func allowMessage(from peerID: PeerID) -> Bool {
|
||||
return queue.sync(flags: .barrier) {
|
||||
let now = Date()
|
||||
let oneSecondAgo = now.addingTimeInterval(-1)
|
||||
|
||||
// Check global rate limit first
|
||||
globalMessageTimestamps = globalMessageTimestamps.filter { $0 > oneSecondAgo }
|
||||
if globalMessageTimestamps.count >= NoiseSecurityConstants.maxGlobalMessagesPerSecond {
|
||||
SecureLogger.warning("Global message rate limit exceeded: \(globalMessageTimestamps.count)/\(NoiseSecurityConstants.maxGlobalMessagesPerSecond) per second", category: .security)
|
||||
return false
|
||||
}
|
||||
|
||||
// Check per-peer rate limit
|
||||
var timestamps = messageTimestamps[peerID] ?? []
|
||||
timestamps = timestamps.filter { $0 > oneSecondAgo }
|
||||
|
||||
if timestamps.count >= NoiseSecurityConstants.maxMessagesPerSecond {
|
||||
SecureLogger.warning("Per-peer message rate limit exceeded for \(peerID): \(timestamps.count)/\(NoiseSecurityConstants.maxMessagesPerSecond) per second", category: .security)
|
||||
return false
|
||||
}
|
||||
|
||||
// Record new message
|
||||
timestamps.append(now)
|
||||
messageTimestamps[peerID] = timestamps
|
||||
globalMessageTimestamps.append(now)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func reset(for peerID: PeerID) {
|
||||
queue.async(flags: .barrier) {
|
||||
self.handshakeTimestamps.removeValue(forKey: peerID)
|
||||
self.messageTimestamps.removeValue(forKey: peerID)
|
||||
}
|
||||
}
|
||||
|
||||
func resetAll() {
|
||||
queue.async(flags: .barrier) {
|
||||
self.handshakeTimestamps.removeAll()
|
||||
self.messageTimestamps.removeAll()
|
||||
self.globalHandshakeTimestamps.removeAll()
|
||||
self.globalMessageTimestamps.removeAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
//
|
||||
// NoiseSecurityConstants.swift
|
||||
// bitchat
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum NoiseSecurityConstants {
|
||||
// Maximum message size to prevent memory exhaustion
|
||||
static let maxMessageSize = 65535 // 64KB as per Noise spec
|
||||
|
||||
// Maximum handshake message size
|
||||
static let maxHandshakeMessageSize = 2048 // 2KB to accommodate XX pattern
|
||||
|
||||
// Session timeout - sessions older than this should be renegotiated
|
||||
static let sessionTimeout: TimeInterval = 86400 // 24 hours
|
||||
|
||||
// Maximum number of messages before rekey (2^64 - 1 is the nonce limit)
|
||||
static let maxMessagesPerSession: UInt64 = 1_000_000_000 // 1 billion messages
|
||||
|
||||
// Handshake timeout - abandon incomplete handshakes
|
||||
static let handshakeTimeout: TimeInterval = 60 // 1 minute
|
||||
|
||||
// Maximum concurrent sessions per peer
|
||||
static let maxSessionsPerPeer = 3
|
||||
|
||||
// Rate limiting
|
||||
static let maxHandshakesPerMinute = 10
|
||||
static let maxMessagesPerSecond = 100
|
||||
|
||||
// Global rate limiting (across all peers)
|
||||
static let maxGlobalHandshakesPerMinute = 30
|
||||
static let maxGlobalMessagesPerSecond = 500
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// NoiseSecurityError.swift
|
||||
// bitchat
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum NoiseSecurityError: Error {
|
||||
case sessionExpired
|
||||
case sessionExhausted
|
||||
case messageTooLarge
|
||||
case invalidPeerID
|
||||
case rateLimitExceeded
|
||||
case handshakeTimeout
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
//
|
||||
// NoiseSecurityValidator.swift
|
||||
// bitchat
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct NoiseSecurityValidator {
|
||||
|
||||
/// Validate message size
|
||||
static func validateMessageSize(_ data: Data) -> Bool {
|
||||
return data.count <= NoiseSecurityConstants.maxMessageSize
|
||||
}
|
||||
|
||||
/// Validate handshake message size
|
||||
static func validateHandshakeMessageSize(_ data: Data) -> Bool {
|
||||
return data.count <= NoiseSecurityConstants.maxHandshakeMessageSize
|
||||
}
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
//
|
||||
// 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 {
|
||||
let message = try handshakeState!.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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
//
|
||||
// NoiseSessionError.swift
|
||||
// bitchat
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
enum NoiseSessionError: Error, Equatable {
|
||||
case invalidState
|
||||
case notEstablished
|
||||
case sessionNotFound
|
||||
case alreadyEstablished
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
//
|
||||
// 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 localStaticKey: Curve25519.KeyAgreement.PrivateKey
|
||||
private let keychain: KeychainManagerProtocol
|
||||
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.localStaticKey = localStaticKey
|
||||
self.keychain = keychain
|
||||
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.localStaticKey = localStaticKey
|
||||
self.keychain = keychain
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// NoiseSessionState.swift
|
||||
// bitchat
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
enum NoiseSessionState: Equatable {
|
||||
case uninitialized
|
||||
case handshaking
|
||||
case established
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
//
|
||||
// SecureNoiseSession.swift
|
||||
// bitchat
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
final class SecureNoiseSession: NoiseSession {
|
||||
private(set) var messageCount: UInt64 = 0
|
||||
private var sessionStartTime = Date()
|
||||
private(set) var lastActivityTime = Date()
|
||||
|
||||
override func encrypt(_ plaintext: Data) throws -> Data {
|
||||
// Check session age
|
||||
if Date().timeIntervalSince(sessionStartTime) > NoiseSecurityConstants.sessionTimeout {
|
||||
throw NoiseSecurityError.sessionExpired
|
||||
}
|
||||
|
||||
// Check message count
|
||||
if messageCount >= NoiseSecurityConstants.maxMessagesPerSession {
|
||||
throw NoiseSecurityError.sessionExhausted
|
||||
}
|
||||
|
||||
// Validate message size
|
||||
guard NoiseSecurityValidator.validateMessageSize(plaintext) else {
|
||||
throw NoiseSecurityError.messageTooLarge
|
||||
}
|
||||
|
||||
let encrypted = try super.encrypt(plaintext)
|
||||
messageCount += 1
|
||||
lastActivityTime = Date()
|
||||
|
||||
return encrypted
|
||||
}
|
||||
|
||||
override func decrypt(_ ciphertext: Data) throws -> Data {
|
||||
// Check session age
|
||||
if Date().timeIntervalSince(sessionStartTime) > NoiseSecurityConstants.sessionTimeout {
|
||||
throw NoiseSecurityError.sessionExpired
|
||||
}
|
||||
|
||||
// Validate message size
|
||||
guard NoiseSecurityValidator.validateMessageSize(ciphertext) else {
|
||||
throw NoiseSecurityError.messageTooLarge
|
||||
}
|
||||
|
||||
let decrypted = try super.decrypt(ciphertext)
|
||||
lastActivityTime = Date()
|
||||
|
||||
return decrypted
|
||||
}
|
||||
|
||||
func needsRenegotiation() -> Bool {
|
||||
// Check if we've used more than 90% of message limit
|
||||
let messageThreshold = UInt64(Double(NoiseSecurityConstants.maxMessagesPerSession) * 0.9)
|
||||
if messageCount >= messageThreshold {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if last activity was more than 30 minutes ago
|
||||
if Date().timeIntervalSince(lastActivityTime) > NoiseSecurityConstants.sessionTimeout {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// MARK: - Testing Support
|
||||
#if DEBUG
|
||||
func setLastActivityTimeForTesting(_ date: Date) {
|
||||
lastActivityTime = date
|
||||
}
|
||||
|
||||
func setMessageCountForTesting(_ count: UInt64) {
|
||||
messageCount = count
|
||||
}
|
||||
|
||||
func setSessionStartTimeForTesting(_ date: Date) {
|
||||
sessionStartTime = date
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import BitLogger
|
||||
import protocol Noise.SecureMemoryCleaner
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
@@ -51,7 +52,7 @@ enum KeychainSaveResult {
|
||||
}
|
||||
}
|
||||
|
||||
protocol KeychainManagerProtocol {
|
||||
protocol KeychainManagerProtocol: SecureMemoryCleaner {
|
||||
func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool
|
||||
func getIdentityKey(forKey key: String) -> Data?
|
||||
func deleteIdentityKey(forKey key: String) -> Bool
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
|
||||
import BitLogger
|
||||
import BitFoundation
|
||||
import Noise
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
|
||||
Reference in New Issue
Block a user