From 9ecda048e71795d8150c4f28eb03ab85ab482394 Mon Sep 17 00:00:00 2001 From: Islam <2553451+qalandarov@users.noreply.github.com> Date: Sun, 5 Oct 2025 11:39:16 +0100 Subject: [PATCH] PeerID 9/n: `NoiseEncryptionService` (#747) --- bitchat/Services/BLEService.swift | 40 ++++----- bitchat/Services/NoiseEncryptionService.swift | 85 +++++++------------ bitchat/ViewModels/ChatViewModel.swift | 16 ++-- 3 files changed, 60 insertions(+), 81 deletions(-) diff --git a/bitchat/Services/BLEService.swift b/bitchat/Services/BLEService.swift index 1fa8fa69..e9694652 100644 --- a/bitchat/Services/BLEService.swift +++ b/bitchat/Services/BLEService.swift @@ -675,10 +675,10 @@ final class BLEService: NSObject { var payload = Data([NoisePayloadType.readReceipt.rawValue]) payload.append(contentsOf: receipt.originalMessageID.utf8) - if noiseService.hasEstablishedSession(with: peerID) { + if noiseService.hasEstablishedSession(with: PeerID(str: peerID)) { SecureLogger.debug("πŸ“€ Sending READ receipt for message \(receipt.originalMessageID) to \(peerID)", category: .session) do { - let encrypted = try noiseService.encrypt(payload, for: peerID) + let encrypted = try noiseService.encrypt(payload, for: PeerID(str: peerID)) let packet = BitchatPacket( type: MessageType.noiseEncrypted.rawValue, senderID: myPeerIDData, @@ -702,7 +702,7 @@ final class BLEService: NSObject { guard let self = self else { return } self.pendingNoisePayloadsAfterHandshake[peerID, default: []].append(payload) } - if !noiseService.hasSession(with: peerID) { initiateNoiseHandshake(with: peerID) } + if !noiseService.hasSession(with: PeerID(str: peerID)) { initiateNoiseHandshake(with: peerID) } SecureLogger.debug("πŸ•’ Queued READ receipt for \(peerID) until handshake completes", category: .session) } } @@ -723,13 +723,13 @@ final class BLEService: NSObject { } private func sendNoisePayload(_ typedPayload: Data, to peerID: String) { - guard noiseService.hasSession(with: peerID) else { + guard noiseService.hasSession(with: PeerID(str: peerID)) else { // Lazy-handshake path: queue? For now, initiate handshake and drop initiateNoiseHandshake(with: peerID) return } do { - let encrypted = try noiseService.encrypt(typedPayload, for: peerID) + let encrypted = try noiseService.encrypt(typedPayload, for: PeerID(str: peerID)) let packet = BitchatPacket( type: MessageType.noiseEncrypted.rawValue, senderID: myPeerIDData, @@ -759,9 +759,9 @@ final class BLEService: NSObject { func getNoiseSessionState(for peerID: String) -> LazyHandshakeState { - if noiseService.hasEstablishedSession(with: peerID) { + if noiseService.hasEstablishedSession(with: PeerID(str: peerID)) { return .established - } else if noiseService.hasSession(with: peerID) { + } else if noiseService.hasSession(with: PeerID(str: peerID)) { return .handshaking } else { return .none @@ -892,7 +892,7 @@ final class BLEService: NSObject { SecureLogger.debug("πŸ“¨ Sending PM to \(recipientID): \(content.prefix(30))...", category: .session) // Check if we have an established Noise session - if noiseService.hasEstablishedSession(with: recipientID) { + if noiseService.hasEstablishedSession(with: PeerID(str: recipientID)) { // Encrypt and send do { // Create TLV-encoded private message @@ -906,7 +906,7 @@ final class BLEService: NSObject { var messagePayload = Data([NoisePayloadType.privateMessage.rawValue]) messagePayload.append(tlvData) - let encrypted = try noiseService.encrypt(messagePayload, for: recipientID) + let encrypted = try noiseService.encrypt(messagePayload, for: PeerID(str: recipientID)) // Convert recipientID to Data (assuming it's a hex string) var recipientData = Data() @@ -972,10 +972,10 @@ final class BLEService: NSObject { private func initiateNoiseHandshake(with peerID: String) { // Use NoiseEncryptionService for handshake - guard !noiseService.hasSession(with: peerID) else { return } + guard !noiseService.hasSession(with: PeerID(str: peerID)) else { return } do { - let handshakeData = try noiseService.initiateHandshake(with: peerID) + let handshakeData = try noiseService.initiateHandshake(with: PeerID(str: peerID)) // Send handshake init let packet = BitchatPacket( @@ -1025,7 +1025,7 @@ final class BLEService: NSObject { var messagePayload = Data([NoisePayloadType.privateMessage.rawValue]) messagePayload.append(tlvData) - let encrypted = try noiseService.encrypt(messagePayload, for: peerID) + let encrypted = try noiseService.encrypt(messagePayload, for: PeerID(str: peerID)) let packet = BitchatPacket( type: MessageType.noiseEncrypted.rawValue, @@ -1841,7 +1841,7 @@ final class BLEService: NSObject { recipientID.hexEncodedString() == myPeerID { // Handshake is for us do { - if let response = try noiseService.processHandshakeMessage(from: peerID, message: packet.payload) { + if let response = try noiseService.processHandshakeMessage(from: PeerID(str: peerID), message: packet.payload) { // Send response let responsePacket = BitchatPacket( type: MessageType.noiseHandshake.rawValue, @@ -1861,7 +1861,7 @@ final class BLEService: NSObject { } catch { SecureLogger.error("Failed to process handshake: \(error)") // Try initiating a new handshake - if !noiseService.hasSession(with: peerID) { + if !noiseService.hasSession(with: PeerID(str: peerID)) { initiateNoiseHandshake(with: peerID) } } @@ -1886,7 +1886,7 @@ final class BLEService: NSObject { updatePeerLastSeen(peerID) do { - let decrypted = try noiseService.decrypt(packet.payload, from: peerID) + let decrypted = try noiseService.decrypt(packet.payload, from: PeerID(str: peerID)) guard decrypted.count > 0 else { return } // First byte indicates the payload type @@ -1926,7 +1926,7 @@ final class BLEService: NSObject { // We received an encrypted message before establishing a session with this peer. // Trigger a handshake so future messages can be decrypted. SecureLogger.debug("πŸ”‘ Encrypted message from \(peerID) without session; initiating handshake") - if !noiseService.hasSession(with: peerID) { + if !noiseService.hasSession(with: PeerID(str: peerID)) { initiateNoiseHandshake(with: peerID) } } catch { @@ -2031,9 +2031,9 @@ final class BLEService: NSObject { var payload = Data([NoisePayloadType.delivered.rawValue]) payload.append(contentsOf: messageID.utf8) - if noiseService.hasEstablishedSession(with: peerID) { + if noiseService.hasEstablishedSession(with: PeerID(str: peerID)) { do { - let encrypted = try noiseService.encrypt(payload, for: peerID) + let encrypted = try noiseService.encrypt(payload, for: PeerID(str: peerID)) let packet = BitchatPacket( type: MessageType.noiseEncrypted.rawValue, senderID: myPeerIDData, @@ -2053,7 +2053,7 @@ final class BLEService: NSObject { guard let self = self else { return } self.pendingNoisePayloadsAfterHandshake[peerID, default: []].append(payload) } - if !noiseService.hasSession(with: peerID) { initiateNoiseHandshake(with: peerID) } + if !noiseService.hasSession(with: PeerID(str: peerID)) { initiateNoiseHandshake(with: peerID) } SecureLogger.debug("πŸ•’ Queued DELIVERED ack for \(peerID) until handshake completes", category: .session) } } @@ -2068,7 +2068,7 @@ final class BLEService: NSObject { SecureLogger.debug("πŸ“€ Sending \(payloads.count) pending noise payloads to \(peerID) after handshake", category: .session) for payload in payloads { do { - let encrypted = try noiseService.encrypt(payload, for: peerID) + let encrypted = try noiseService.encrypt(payload, for: PeerID(str: peerID)) let packet = BitchatPacket( type: MessageType.noiseEncrypted.rawValue, senderID: myPeerIDData, diff --git a/bitchat/Services/NoiseEncryptionService.swift b/bitchat/Services/NoiseEncryptionService.swift index c406ff54..98630a19 100644 --- a/bitchat/Services/NoiseEncryptionService.swift +++ b/bitchat/Services/NoiseEncryptionService.swift @@ -162,8 +162,8 @@ final class NoiseEncryptionService { private let sessionManager: NoiseSessionManager // Peer fingerprints (SHA256 hash of static public key) - private var peerFingerprints: [String: String] = [:] // peerID -> fingerprint - private var fingerprintToPeerID: [String: String] = [:] // fingerprint -> peerID + private var peerFingerprints: [PeerID: String] = [:] + private var fingerprintToPeerID: [String: PeerID] = [:] // Thread safety private let serviceQueue = DispatchQueue(label: "chat.bitchat.noise.service", attributes: .concurrent) @@ -251,7 +251,7 @@ final class NoiseEncryptionService { // Set up session callbacks sessionManager.onSessionEstablished = { [weak self] peerID, remoteStaticKey in - self?.handleSessionEstablished(peerID: peerID, remoteStaticKey: remoteStaticKey) + self?.handleSessionEstablished(peerID: PeerID(str: peerID), remoteStaticKey: remoteStaticKey) } // Start session maintenance timer @@ -276,8 +276,8 @@ final class NoiseEncryptionService { } /// Get peer's public key data - func getPeerPublicKeyData(_ peerID: String) -> Data? { - return sessionManager.getRemoteStaticKey(for: peerID)?.rawRepresentation + func getPeerPublicKeyData(_ peerID: PeerID) -> Data? { + return sessionManager.getRemoteStaticKey(for: peerID.id)?.rawRepresentation } /// Clear persistent identity (for panic mode) @@ -404,52 +404,52 @@ final class NoiseEncryptionService { // MARK: - Handshake Management /// Initiate a Noise handshake with a peer - func initiateHandshake(with peerID: String) throws -> Data { + func initiateHandshake(with peerID: PeerID) throws -> Data { // Validate peer ID - guard PeerID(str: peerID).isValid else { - SecureLogger.warning(.authenticationFailed(peerID: peerID)) + guard peerID.isValid else { + SecureLogger.warning(.authenticationFailed(peerID: peerID.id)) throw NoiseSecurityError.invalidPeerID } // Check rate limit - guard rateLimiter.allowHandshake(from: PeerID(str: peerID)) else { + guard rateLimiter.allowHandshake(from: peerID) else { SecureLogger.warning(.authenticationFailed(peerID: "Rate limited: \(peerID)")) throw NoiseSecurityError.rateLimitExceeded } - SecureLogger.info(.handshakeStarted(peerID: peerID)) + SecureLogger.info(.handshakeStarted(peerID: peerID.id)) // Return raw handshake data without wrapper // The Noise protocol handles its own message format - let handshakeData = try sessionManager.initiateHandshake(with: peerID) + let handshakeData = try sessionManager.initiateHandshake(with: peerID.id) return handshakeData } /// Process an incoming handshake message - func processHandshakeMessage(from peerID: String, message: Data) throws -> Data? { + func processHandshakeMessage(from peerID: PeerID, message: Data) throws -> Data? { // Validate peer ID - guard PeerID(str: peerID).isValid else { - SecureLogger.warning(.authenticationFailed(peerID: peerID)) + guard peerID.isValid else { + SecureLogger.warning(.authenticationFailed(peerID: peerID.id)) throw NoiseSecurityError.invalidPeerID } // Validate message size guard NoiseSecurityValidator.validateHandshakeMessageSize(message) else { - SecureLogger.warning(.handshakeFailed(peerID: peerID, error: "Message too large")) + SecureLogger.warning(.handshakeFailed(peerID: peerID.id, error: "Message too large")) throw NoiseSecurityError.messageTooLarge } // Check rate limit - guard rateLimiter.allowHandshake(from: PeerID(str: peerID)) else { + guard rateLimiter.allowHandshake(from: peerID) else { SecureLogger.warning(.authenticationFailed(peerID: "Rate limited: \(peerID)")) throw NoiseSecurityError.rateLimitExceeded } // 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 responsePayload = try sessionManager.handleIncomingHandshake(from: peerID.id, message: message) // Return raw response without wrapper @@ -457,48 +457,48 @@ final class NoiseEncryptionService { } /// Check if we have an established session with a peer - func hasEstablishedSession(with peerID: String) -> Bool { - return sessionManager.getSession(for: peerID)?.isEstablished() ?? false + func hasEstablishedSession(with peerID: PeerID) -> Bool { + return sessionManager.getSession(for: peerID.id)?.isEstablished() ?? false } /// Check if we have a session (established or handshaking) with a peer - func hasSession(with peerID: String) -> Bool { - return sessionManager.getSession(for: peerID) != nil + func hasSession(with peerID: PeerID) -> Bool { + return sessionManager.getSession(for: peerID.id) != nil } // MARK: - Encryption/Decryption /// Encrypt data for a specific peer - func encrypt(_ data: Data, for peerID: String) throws -> Data { + func encrypt(_ data: Data, for peerID: PeerID) throws -> Data { // Validate message size guard NoiseSecurityValidator.validateMessageSize(data) else { throw NoiseSecurityError.messageTooLarge } // Check rate limit - guard rateLimiter.allowMessage(from: PeerID(str: peerID)) else { + guard rateLimiter.allowMessage(from: peerID) else { throw NoiseSecurityError.rateLimitExceeded } // Check if we have an established session guard hasEstablishedSession(with: peerID) else { // Signal that handshake is needed - onHandshakeRequired?(peerID) + onHandshakeRequired?(peerID.id) throw NoiseEncryptionError.handshakeRequired } - return try sessionManager.encrypt(data, for: peerID) + return try sessionManager.encrypt(data, for: peerID.id) } /// Decrypt data from a specific peer - func decrypt(_ data: Data, from peerID: String) throws -> Data { + func decrypt(_ data: Data, from peerID: PeerID) throws -> Data { // Validate message size guard NoiseSecurityValidator.validateMessageSize(data) else { throw NoiseSecurityError.messageTooLarge } // Check rate limit - guard rateLimiter.allowMessage(from: PeerID(str: peerID)) else { + guard rateLimiter.allowMessage(from: peerID) else { throw NoiseSecurityError.rateLimitExceeded } @@ -507,38 +507,17 @@ final class NoiseEncryptionService { throw NoiseEncryptionError.sessionNotEstablished } - return try sessionManager.decrypt(data, from: peerID) + return try sessionManager.decrypt(data, from: peerID.id) } // MARK: - Peer Management /// Get fingerprint for a peer - func getPeerFingerprint(_ peerID: String) -> String? { + func getPeerFingerprint(_ peerID: PeerID) -> String? { return serviceQueue.sync { return peerFingerprints[peerID] } } - - /// Get peer ID for a fingerprint - func getPeerID(for fingerprint: String) -> String? { - return serviceQueue.sync { - return fingerprintToPeerID[fingerprint] - } - } - - /// Remove a peer session - func removePeer(_ peerID: String) { - sessionManager.removeSession(for: peerID) - - serviceQueue.sync(flags: .barrier) { - if let fingerprint = peerFingerprints[peerID] { - fingerprintToPeerID.removeValue(forKey: fingerprint) - } - peerFingerprints.removeValue(forKey: peerID) - } - - SecureLogger.info(.sessionExpired(peerID: peerID)) - } func clearEphemeralStateForPanic() { sessionManager.removeAllSessions() @@ -551,7 +530,7 @@ final class NoiseEncryptionService { // MARK: - Private Helpers - private func handleSessionEstablished(peerID: String, remoteStaticKey: Curve25519.KeyAgreement.PublicKey) { + private func handleSessionEstablished(peerID: PeerID, remoteStaticKey: Curve25519.KeyAgreement.PublicKey) { // Calculate fingerprint let fingerprint = remoteStaticKey.rawRepresentation.sha256Fingerprint() @@ -562,12 +541,12 @@ final class NoiseEncryptionService { } // Log security event - SecureLogger.info(.handshakeCompleted(peerID: peerID)) + SecureLogger.info(.handshakeCompleted(peerID: peerID.id)) // Notify all handlers about authentication serviceQueue.async { [weak self] in self?.onPeerAuthenticatedHandlers.forEach { handler in - handler(peerID, fingerprint) + handler(peerID.id, fingerprint) } } } diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index 7ed914dc..dffdeb10 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -310,7 +310,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { if let mapped = shortIDToNoiseKey[shortPeerID] { return mapped } // Fallback: derive from active Noise session if available if shortPeerID.count == 16, - let key = meshService.getNoiseService().getPeerPublicKeyData(shortPeerID) { + let key = meshService.getNoiseService().getPeerPublicKeyData(PeerID(str: shortPeerID)) { let stable = key.hexEncodedString() shortIDToNoiseKey[shortPeerID] = stable return stable @@ -3737,7 +3737,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { func updateEncryptionStatusForPeer(_ peerID: String) { let noiseService = meshService.getNoiseService() - if noiseService.hasEstablishedSession(with: peerID) { + if noiseService.hasEstablishedSession(with: PeerID(str: peerID)) { // Check if fingerprint is verified using our persisted data if let fingerprint = getFingerprint(for: peerID), verifiedFingerprints.contains(fingerprint) { @@ -3745,7 +3745,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } else { peerEncryptionStatus[peerID] = .noiseSecured } - } else if noiseService.hasSession(with: peerID) { + } else if noiseService.hasSession(with: PeerID(str: peerID)) { // Session exists but not established - handshaking peerEncryptionStatus[peerID] = .noiseHandshaking } else { @@ -4195,7 +4195,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { private func updateEncryptionStatus(for peerID: String) { let noiseService = meshService.getNoiseService() - if noiseService.hasEstablishedSession(with: peerID) { + if noiseService.hasEstablishedSession(with: PeerID(str: peerID)) { if let fingerprint = getFingerprint(for: peerID) { if verifiedFingerprints.contains(fingerprint) { peerEncryptionStatus[peerID] = .noiseVerified @@ -4206,7 +4206,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Session established but no fingerprint yet peerEncryptionStatus[peerID] = .noiseSecured } - } else if noiseService.hasSession(with: peerID) { + } else if noiseService.hasSession(with: PeerID(str: peerID)) { peerEncryptionStatus[peerID] = .noiseHandshaking } else { peerEncryptionStatus[peerID] = Optional.none @@ -4357,7 +4357,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Cache shortID -> full Noise key mapping as soon as session authenticates if self.shortIDToNoiseKey[peerID] == nil, - let keyData = self.meshService.getNoiseService().getPeerPublicKeyData(peerID) { + let keyData = self.meshService.getNoiseService().getPeerPublicKeyData(PeerID(str: peerID)) { let stable = keyData.hexEncodedString() self.shortIDToNoiseKey[peerID] = stable SecureLogger.debug("πŸ—ΊοΈ Mapped short peerID to Noise key for header continuity: \(peerID) -> \(stable.prefix(8))…", category: .session) @@ -4586,7 +4586,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { pendingQRVerifications[peerID] = pending // If Noise session is established, send immediately; otherwise trigger handshake and send on auth let noise = meshService.getNoiseService() - if noise.hasEstablishedSession(with: peerID) { + if noise.hasEstablishedSession(with: PeerID(str: peerID)) { meshService.sendVerifyChallenge(to: peerID, noiseKeyHex: qr.noiseKeyHex, nonceA: nonce) pending.sent = true pendingQRVerifications[peerID] = pending @@ -4637,7 +4637,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // If the open PM is tied to this short peer ID, switch UI context to the full Noise key (offline favorite) var derivedStableKeyHex: String? = shortIDToNoiseKey[peerID] if derivedStableKeyHex == nil, - let key = meshService.getNoiseService().getPeerPublicKeyData(peerID) { + let key = meshService.getNoiseService().getPeerPublicKeyData(PeerID(str: peerID)) { derivedStableKeyHex = key.hexEncodedString() shortIDToNoiseKey[peerID] = derivedStableKeyHex }