From e4ec2ef3fe18eceab87f1e0b0de2acc139ef6fe0 Mon Sep 17 00:00:00 2001 From: Islam <2553451+qalandarov@users.noreply.github.com> Date: Thu, 2 Oct 2025 12:29:48 +0100 Subject: [PATCH] PeerID 5/n: Ephemeral and Secure Identities (#742) --- bitchat/Identity/IdentityModels.swift | 2 +- .../Identity/SecureIdentityStateManager.swift | 22 +++++++++---------- bitchat/Services/BLEService.swift | 2 +- bitchat/ViewModels/ChatViewModel.swift | 6 ++--- bitchat/Views/ContentView.swift | 2 +- bitchatTests/Mocks/MockIdentityManager.swift | 8 +++---- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/bitchat/Identity/IdentityModels.swift b/bitchat/Identity/IdentityModels.swift index 5d901d84..11bf2f7e 100644 --- a/bitchat/Identity/IdentityModels.swift +++ b/bitchat/Identity/IdentityModels.swift @@ -87,7 +87,7 @@ import Foundation /// Represents the ephemeral layer of identity - short-lived peer IDs that provide network privacy. /// These IDs rotate periodically to prevent tracking while maintaining cryptographic relationships. struct EphemeralIdentity { - let peerID: String // 8 random bytes + let peerID: PeerID // 8 random bytes let sessionStart: Date var handshakeState: HandshakeState } diff --git a/bitchat/Identity/SecureIdentityStateManager.swift b/bitchat/Identity/SecureIdentityStateManager.swift index 339605b7..f2172524 100644 --- a/bitchat/Identity/SecureIdentityStateManager.swift +++ b/bitchat/Identity/SecureIdentityStateManager.swift @@ -103,7 +103,7 @@ protocol SecureIdentityStateManagerProtocol { // MARK: Cryptographic Identities func upsertCryptographicIdentity(fingerprint: String, noisePublicKey: Data, signingPublicKey: Data?, claimedNickname: String?) - func getCryptoIdentitiesByPeerIDPrefix(_ peerID: String) -> [CryptographicIdentity] + func getCryptoIdentitiesByPeerIDPrefix(_ peerID: PeerID) -> [CryptographicIdentity] func updateSocialIdentity(_ identity: SocialIdentity) // MARK: Favorites Management @@ -121,12 +121,12 @@ protocol SecureIdentityStateManagerProtocol { func getBlockedNostrPubkeys() -> Set // MARK: Ephemeral Session Management - func registerEphemeralSession(peerID: String, handshakeState: HandshakeState) - func updateHandshakeState(peerID: String, state: HandshakeState) + func registerEphemeralSession(peerID: PeerID, handshakeState: HandshakeState) + func updateHandshakeState(peerID: PeerID, state: HandshakeState) // MARK: Cleanup func clearAllIdentityData() - func removeEphemeralSession(peerID: String) + func removeEphemeralSession(peerID: PeerID) // MARK: Verification func setVerified(fingerprint: String, verified: Bool) @@ -143,7 +143,7 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol { private let encryptionKeyName = "identityCacheEncryptionKey" // In-memory state - private var ephemeralSessions: [String: EphemeralIdentity] = [:] + private var ephemeralSessions: [PeerID: EphemeralIdentity] = [:] private var cryptographicIdentities: [String: CryptographicIdentity] = [:] private var cache: IdentityCache = IdentityCache() @@ -321,11 +321,11 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol { } /// Find cryptographic identities whose fingerprint prefix matches a peerID (16-hex) short ID - func getCryptoIdentitiesByPeerIDPrefix(_ peerID: String) -> [CryptographicIdentity] { + func getCryptoIdentitiesByPeerIDPrefix(_ peerID: PeerID) -> [CryptographicIdentity] { queue.sync { // Defensive: ensure hex and correct length - guard peerID.count == 16, peerID.allSatisfy({ $0.isHexDigit }) else { return [] } - return cryptographicIdentities.values.filter { $0.fingerprint.hasPrefix(peerID) } + guard peerID.isShort else { return [] } + return cryptographicIdentities.values.filter { $0.fingerprint.hasPrefix(peerID.id) } } } @@ -455,7 +455,7 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol { // MARK: - Ephemeral Session Management - func registerEphemeralSession(peerID: String, handshakeState: HandshakeState = .none) { + func registerEphemeralSession(peerID: PeerID, handshakeState: HandshakeState = .none) { queue.async(flags: .barrier) { self.ephemeralSessions[peerID] = EphemeralIdentity( peerID: peerID, @@ -465,7 +465,7 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol { } } - func updateHandshakeState(peerID: String, state: HandshakeState) { + func updateHandshakeState(peerID: PeerID, state: HandshakeState) { queue.async(flags: .barrier) { self.ephemeralSessions[peerID]?.handshakeState = state @@ -493,7 +493,7 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol { } } - func removeEphemeralSession(peerID: String) { + func removeEphemeralSession(peerID: PeerID) { queue.async(flags: .barrier) { self.ephemeralSessions.removeValue(forKey: peerID) } diff --git a/bitchat/Services/BLEService.swift b/bitchat/Services/BLEService.swift index c2eceb36..e18fcbd1 100644 --- a/bitchat/Services/BLEService.swift +++ b/bitchat/Services/BLEService.swift @@ -1786,7 +1786,7 @@ final class BLEService: NSObject { // Fallback: verify signature using persisted signing key for this peerID's fingerprint prefix if let signature = packet.signature, let packetData = packet.toBinaryDataForSigning() { // Find candidate identities by peerID prefix (16 hex) - let candidates = identityManager.getCryptoIdentitiesByPeerIDPrefix(peerID) + let candidates = identityManager.getCryptoIdentitiesByPeerIDPrefix(PeerID(str: peerID)) for candidate in candidates { if let signingKey = candidate.signingPublicKey, noiseService.verifySignature(signature, for: packetData, publicKey: signingKey) { diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index a868995c..7ed914dc 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -4607,7 +4607,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { isConnected = true // Register ephemeral session with identity manager - identityManager.registerEphemeralSession(peerID: peerID, handshakeState: .none) + identityManager.registerEphemeralSession(peerID: PeerID(str: peerID), handshakeState: .none) // Intentionally do not resend favorites on reconnect. // We only send our npub when a favorite is toggled on, or if our npub changes. @@ -4632,7 +4632,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { SecureLogger.debug("👋 Peer disconnected: \(peerID)", category: .session) // Remove ephemeral session from identity manager - identityManager.removeEphemeralSession(peerID: peerID) + identityManager.removeEphemeralSession(peerID: PeerID(str: peerID)) // 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] @@ -4739,7 +4739,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Register ephemeral sessions for all connected peers for peerID in peers { - self.identityManager.registerEphemeralSession(peerID: peerID, handshakeState: .none) + self.identityManager.registerEphemeralSession(peerID: PeerID(str: peerID), handshakeState: .none) } // Schedule UI refresh to ensure offline favorites are shown diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift index b0d82ffe..4dc4e59b 100644 --- a/bitchat/Views/ContentView.swift +++ b/bitchat/Views/ContentView.swift @@ -1407,7 +1407,7 @@ struct ContentView: View { !fav.peerNickname.isEmpty { return fav.peerNickname } // Fallback: resolve from persisted social identity via fingerprint mapping if headerPeerID.count == 16 { - let candidates = viewModel.identityManager.getCryptoIdentitiesByPeerIDPrefix(headerPeerID) + let candidates = viewModel.identityManager.getCryptoIdentitiesByPeerIDPrefix(PeerID(str: headerPeerID)) if let id = candidates.first, let social = viewModel.identityManager.getSocialIdentity(for: id.fingerprint) { if let pet = social.localPetname, !pet.isEmpty { return pet } diff --git a/bitchatTests/Mocks/MockIdentityManager.swift b/bitchatTests/Mocks/MockIdentityManager.swift index 10eb9094..15c335be 100644 --- a/bitchatTests/Mocks/MockIdentityManager.swift +++ b/bitchatTests/Mocks/MockIdentityManager.swift @@ -28,7 +28,7 @@ final class MockIdentityManager: SecureIdentityStateManagerProtocol { func upsertCryptographicIdentity(fingerprint: String, noisePublicKey: Data, signingPublicKey: Data?, claimedNickname: String?) {} - func getCryptoIdentitiesByPeerIDPrefix(_ peerID: String) -> [CryptographicIdentity] { + func getCryptoIdentitiesByPeerIDPrefix(_ peerID: PeerID) -> [CryptographicIdentity] { [] } @@ -60,13 +60,13 @@ final class MockIdentityManager: SecureIdentityStateManagerProtocol { Set() } - func registerEphemeralSession(peerID: String, handshakeState: HandshakeState) {} + func registerEphemeralSession(peerID: PeerID, handshakeState: HandshakeState) {} - func updateHandshakeState(peerID: String, state: HandshakeState) {} + func updateHandshakeState(peerID: PeerID, state: HandshakeState) {} func clearAllIdentityData() {} - func removeEphemeralSession(peerID: String) {} + func removeEphemeralSession(peerID: PeerID) {} func setVerified(fingerprint: String, verified: Bool) {}