mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 23:05:20 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e455a23fe9 | ||
|
|
920dc31795 | ||
|
|
bb3d99bdca | ||
|
|
b01cac4649 | ||
|
|
4b0634d1d0 |
@@ -1,12 +0,0 @@
|
|||||||
Place Tor.xcframework here
|
|
||||||
|
|
||||||
Instructions
|
|
||||||
- Obtain a prebuilt Tor Apple xcframework (iCepa/Onion Browser lineage) or build your own minimal client-only Tor.
|
|
||||||
- Rename it (if needed) to `Tor.xcframework` and drop it in this `Frameworks/` directory.
|
|
||||||
- Regenerate the Xcode project if you use XcodeGen (`project.yml` already references `Frameworks/Tor.xcframework`).
|
|
||||||
- Build the app; `TorManager` will automatically bootstrap Tor and route all networking through it.
|
|
||||||
|
|
||||||
Notes
|
|
||||||
- For iOS, the framework will be embedded and code-signed automatically.
|
|
||||||
- For macOS, it will be linked and embedded as well (you may prefer a system tor for smaller bundles).
|
|
||||||
|
|
||||||
@@ -173,6 +173,10 @@
|
|||||||
F455F011B3B648ADA233F998 /* BinaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */; };
|
F455F011B3B648ADA233F998 /* BinaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */; };
|
||||||
FB8819B4C84FAFEF5C36B216 /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136696FC4436A02D98CE6A77 /* KeychainManager.swift */; };
|
FB8819B4C84FAFEF5C36B216 /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136696FC4436A02D98CE6A77 /* KeychainManager.swift */; };
|
||||||
FBC409E105493C491531B59A /* NostrProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5A9FF4AEA8A923317ED26A /* NostrProtocol.swift */; };
|
FBC409E105493C491531B59A /* NostrProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5A9FF4AEA8A923317ED26A /* NostrProtocol.swift */; };
|
||||||
|
A1B2C3D4E5F60123456789BA /* MockKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60123456789AA /* MockKeychain.swift */; };
|
||||||
|
A1B2C3D4E5F60123456789BB /* MockKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60123456789AA /* MockKeychain.swift */; };
|
||||||
|
A1B2C3D4E5F60123456789BC /* MockIdentityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60123456789AB /* MockIdentityManager.swift */; };
|
||||||
|
A1B2C3D4E5F60123456789BD /* MockIdentityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60123456789AB /* MockIdentityManager.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -307,6 +311,8 @@
|
|||||||
FC75901A0F0073B5BB8356E7 /* TestConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConstants.swift; sourceTree = "<group>"; };
|
FC75901A0F0073B5BB8356E7 /* TestConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConstants.swift; sourceTree = "<group>"; };
|
||||||
FDC18D910D6FF2E8B1B6C885 /* SecureIdentityStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdentityStateManager.swift; sourceTree = "<group>"; };
|
FDC18D910D6FF2E8B1B6C885 /* SecureIdentityStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdentityStateManager.swift; sourceTree = "<group>"; };
|
||||||
FE7CCF2BD78A3F3DAE6DA145 /* MockBLEService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBLEService.swift; sourceTree = "<group>"; };
|
FE7CCF2BD78A3F3DAE6DA145 /* MockBLEService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBLEService.swift; sourceTree = "<group>"; };
|
||||||
|
A1B2C3D4E5F60123456789AA /* MockKeychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockKeychain.swift; sourceTree = "<group>"; };
|
||||||
|
A1B2C3D4E5F60123456789AB /* MockIdentityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockIdentityManager.swift; sourceTree = "<group>"; };
|
||||||
FF7AF93D874001FBD94C8306 /* bitchat-macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "bitchat-macOS.entitlements"; sourceTree = "<group>"; };
|
FF7AF93D874001FBD94C8306 /* bitchat-macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "bitchat-macOS.entitlements"; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
@@ -464,6 +470,8 @@
|
|||||||
children = (
|
children = (
|
||||||
C27328EE574221395B2B8E87 /* MockBluetoothMeshService.swift */,
|
C27328EE574221395B2B8E87 /* MockBluetoothMeshService.swift */,
|
||||||
FE7CCF2BD78A3F3DAE6DA145 /* MockBLEService.swift */,
|
FE7CCF2BD78A3F3DAE6DA145 /* MockBLEService.swift */,
|
||||||
|
A1B2C3D4E5F60123456789AA /* MockKeychain.swift */,
|
||||||
|
A1B2C3D4E5F60123456789AB /* MockIdentityManager.swift */,
|
||||||
);
|
);
|
||||||
path = Mocks;
|
path = Mocks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -949,6 +957,8 @@
|
|||||||
047502932E547ACC0083520F /* LocationChannelsTests.swift in Sources */,
|
047502932E547ACC0083520F /* LocationChannelsTests.swift in Sources */,
|
||||||
048A4C2B2E5FCE0300162C4A /* GeohashBookmarksStoreTests.swift in Sources */,
|
048A4C2B2E5FCE0300162C4A /* GeohashBookmarksStoreTests.swift in Sources */,
|
||||||
6C803BF930E7E19BE6E99EAA /* MockBLEService.swift in Sources */,
|
6C803BF930E7E19BE6E99EAA /* MockBLEService.swift in Sources */,
|
||||||
|
A1B2C3D4E5F60123456789BB /* MockKeychain.swift in Sources */,
|
||||||
|
A1B2C3D4E5F60123456789BD /* MockIdentityManager.swift in Sources */,
|
||||||
765254F56997F01054699AC0 /* NoiseProtocolTests.swift in Sources */,
|
765254F56997F01054699AC0 /* NoiseProtocolTests.swift in Sources */,
|
||||||
968181D255CA7A804340B4DA /* NostrProtocolTests.swift in Sources */,
|
968181D255CA7A804340B4DA /* NostrProtocolTests.swift in Sources */,
|
||||||
ED83C7AC1E6BEF15389C0132 /* PrivateChatE2ETests.swift in Sources */,
|
ED83C7AC1E6BEF15389C0132 /* PrivateChatE2ETests.swift in Sources */,
|
||||||
@@ -973,6 +983,9 @@
|
|||||||
047502922E547ACC0083520F /* LocationChannelsTests.swift in Sources */,
|
047502922E547ACC0083520F /* LocationChannelsTests.swift in Sources */,
|
||||||
048A4C2C2E5FCE0300162C4A /* GeohashBookmarksStoreTests.swift in Sources */,
|
048A4C2C2E5FCE0300162C4A /* GeohashBookmarksStoreTests.swift in Sources */,
|
||||||
3849CA6D99B2D536636DF4A6 /* MockBLEService.swift in Sources */,
|
3849CA6D99B2D536636DF4A6 /* MockBLEService.swift in Sources */,
|
||||||
|
A1B2C3D4E5F60123456789BA /* MockKeychain.swift in Sources */,
|
||||||
|
|
||||||
|
A1B2C3D4E5F60123456789BC /* MockIdentityManager.swift in Sources */,
|
||||||
BC4DC75F4FB823FF40569676 /* NoiseProtocolTests.swift in Sources */,
|
BC4DC75F4FB823FF40569676 /* NoiseProtocolTests.swift in Sources */,
|
||||||
EE8C3ECADAB3083A2687D50B /* NostrProtocolTests.swift in Sources */,
|
EE8C3ECADAB3083A2687D50B /* NostrProtocolTests.swift in Sources */,
|
||||||
0AE840940F21AFC07C226636 /* PrivateChatE2ETests.swift in Sources */,
|
0AE840940F21AFC07C226636 /* PrivateChatE2ETests.swift in Sources */,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import UserNotifications
|
|||||||
|
|
||||||
@main
|
@main
|
||||||
struct BitchatApp: App {
|
struct BitchatApp: App {
|
||||||
@StateObject private var chatViewModel = ChatViewModel()
|
@StateObject private var chatViewModel: ChatViewModel
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
@Environment(\.scenePhase) var scenePhase
|
@Environment(\.scenePhase) var scenePhase
|
||||||
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||||
@@ -23,6 +23,14 @@ struct BitchatApp: App {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
let keychain = KeychainManager()
|
||||||
|
_chatViewModel = StateObject(
|
||||||
|
wrappedValue: ChatViewModel(
|
||||||
|
keychain: keychain,
|
||||||
|
identityManager: SecureIdentityStateManager(keychain)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
UNUserNotificationCenter.current().delegate = NotificationDelegate.shared
|
UNUserNotificationCenter.current().delegate = NotificationDelegate.shared
|
||||||
// Warm up georelay directory and refresh if stale (once/day)
|
// Warm up georelay directory and refresh if stale (once/day)
|
||||||
GeoRelayDirectory.shared.prefetchIfNeeded()
|
GeoRelayDirectory.shared.prefetchIfNeeded()
|
||||||
|
|||||||
@@ -93,13 +93,51 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
|
|
||||||
|
protocol SecureIdentityStateManagerProtocol {
|
||||||
|
// MARK: Secure Loading/Saving
|
||||||
|
func forceSave()
|
||||||
|
|
||||||
|
// MARK: Social Identity Management
|
||||||
|
func getSocialIdentity(for fingerprint: String) -> SocialIdentity?
|
||||||
|
|
||||||
|
// MARK: Cryptographic Identities
|
||||||
|
func upsertCryptographicIdentity(fingerprint: String, noisePublicKey: Data, signingPublicKey: Data?, claimedNickname: String?)
|
||||||
|
func getCryptoIdentitiesByPeerIDPrefix(_ peerID: String) -> [CryptographicIdentity]
|
||||||
|
func updateSocialIdentity(_ identity: SocialIdentity)
|
||||||
|
|
||||||
|
// MARK: Favorites Management
|
||||||
|
func getFavorites() -> Set<String>
|
||||||
|
func setFavorite(_ fingerprint: String, isFavorite: Bool)
|
||||||
|
func isFavorite(fingerprint: String) -> Bool
|
||||||
|
|
||||||
|
// MARK: Blocked Users Management
|
||||||
|
func isBlocked(fingerprint: String) -> Bool
|
||||||
|
func setBlocked(_ fingerprint: String, isBlocked: Bool)
|
||||||
|
|
||||||
|
// MARK: Geohash (Nostr) Blocking
|
||||||
|
func isNostrBlocked(pubkeyHexLowercased: String) -> Bool
|
||||||
|
func setNostrBlocked(_ pubkeyHexLowercased: String, isBlocked: Bool)
|
||||||
|
func getBlockedNostrPubkeys() -> Set<String>
|
||||||
|
|
||||||
|
// MARK: Ephemeral Session Management
|
||||||
|
func registerEphemeralSession(peerID: String, handshakeState: HandshakeState)
|
||||||
|
func updateHandshakeState(peerID: String, state: HandshakeState)
|
||||||
|
|
||||||
|
// MARK: Cleanup
|
||||||
|
func clearAllIdentityData()
|
||||||
|
func removeEphemeralSession(peerID: String)
|
||||||
|
|
||||||
|
// MARK: Verification
|
||||||
|
func setVerified(fingerprint: String, verified: Bool)
|
||||||
|
func isVerified(fingerprint: String) -> Bool
|
||||||
|
func getVerifiedFingerprints() -> Set<String>
|
||||||
|
}
|
||||||
|
|
||||||
/// Singleton manager for secure identity state persistence and retrieval.
|
/// Singleton manager for secure identity state persistence and retrieval.
|
||||||
/// Provides thread-safe access to identity mappings with encryption at rest.
|
/// Provides thread-safe access to identity mappings with encryption at rest.
|
||||||
/// All identity data is stored encrypted in the device Keychain for security.
|
/// All identity data is stored encrypted in the device Keychain for security.
|
||||||
final class SecureIdentityStateManager {
|
final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
|
||||||
static let shared = SecureIdentityStateManager()
|
private let keychain: KeychainManagerProtocol
|
||||||
|
|
||||||
private let keychain = KeychainManager.shared
|
|
||||||
private let cacheKey = "bitchat.identityCache.v2"
|
private let cacheKey = "bitchat.identityCache.v2"
|
||||||
private let encryptionKeyName = "identityCacheEncryptionKey"
|
private let encryptionKeyName = "identityCacheEncryptionKey"
|
||||||
|
|
||||||
@@ -108,9 +146,6 @@ final class SecureIdentityStateManager {
|
|||||||
private var cryptographicIdentities: [String: CryptographicIdentity] = [:]
|
private var cryptographicIdentities: [String: CryptographicIdentity] = [:]
|
||||||
private var cache: IdentityCache = IdentityCache()
|
private var cache: IdentityCache = IdentityCache()
|
||||||
|
|
||||||
// Pending actions before handshake
|
|
||||||
private var pendingActions: [String: PendingActions] = [:]
|
|
||||||
|
|
||||||
// Thread safety
|
// Thread safety
|
||||||
private let queue = DispatchQueue(label: "bitchat.identity.state", attributes: .concurrent)
|
private let queue = DispatchQueue(label: "bitchat.identity.state", attributes: .concurrent)
|
||||||
|
|
||||||
@@ -122,7 +157,9 @@ final class SecureIdentityStateManager {
|
|||||||
// Encryption key
|
// Encryption key
|
||||||
private let encryptionKey: SymmetricKey
|
private let encryptionKey: SymmetricKey
|
||||||
|
|
||||||
private init() {
|
init(_ keychain: KeychainManagerProtocol) {
|
||||||
|
self.keychain = keychain
|
||||||
|
|
||||||
// Generate or retrieve encryption key from keychain
|
// Generate or retrieve encryption key from keychain
|
||||||
let loadedKey: SymmetricKey
|
let loadedKey: SymmetricKey
|
||||||
|
|
||||||
@@ -146,9 +183,13 @@ final class SecureIdentityStateManager {
|
|||||||
loadIdentityCache()
|
loadIdentityCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
forceSave()
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Secure Loading/Saving
|
// MARK: - Secure Loading/Saving
|
||||||
|
|
||||||
func loadIdentityCache() {
|
private func loadIdentityCache() {
|
||||||
guard let encryptedData = keychain.getIdentityKey(forKey: cacheKey) else {
|
guard let encryptedData = keychain.getIdentityKey(forKey: cacheKey) else {
|
||||||
// No existing cache, start fresh
|
// No existing cache, start fresh
|
||||||
return
|
return
|
||||||
@@ -164,12 +205,7 @@ final class SecureIdentityStateManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
private func saveIdentityCache() {
|
||||||
// Force save any pending changes
|
|
||||||
forceSave()
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveIdentityCache() {
|
|
||||||
// Mark that we need to save
|
// Mark that we need to save
|
||||||
pendingSave = true
|
pendingSave = true
|
||||||
|
|
||||||
@@ -201,26 +237,8 @@ final class SecureIdentityStateManager {
|
|||||||
// Force immediate save (for app termination)
|
// Force immediate save (for app termination)
|
||||||
func forceSave() {
|
func forceSave() {
|
||||||
saveTimer?.invalidate()
|
saveTimer?.invalidate()
|
||||||
if pendingSave {
|
|
||||||
performSave()
|
performSave()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Identity Resolution
|
|
||||||
|
|
||||||
func resolveIdentity(peerID: String, claimedNickname: String) -> IdentityHint {
|
|
||||||
queue.sync {
|
|
||||||
// Check if we have candidates based on nickname
|
|
||||||
if let fingerprints = cache.nicknameIndex[claimedNickname] {
|
|
||||||
if fingerprints.count == 1 {
|
|
||||||
return .likelyKnown(fingerprint: fingerprints.first!)
|
|
||||||
} else {
|
|
||||||
return .ambiguous(candidates: fingerprints)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return .unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Social Identity Management
|
// MARK: - Social Identity Management
|
||||||
|
|
||||||
@@ -301,11 +319,6 @@ final class SecureIdentityStateManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve cryptographic identity by fingerprint
|
|
||||||
func getCryptographicIdentity(for fingerprint: String) -> CryptographicIdentity? {
|
|
||||||
queue.sync { cryptographicIdentities[fingerprint] }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find cryptographic identities whose fingerprint prefix matches a peerID (16-hex) short ID
|
/// Find cryptographic identities whose fingerprint prefix matches a peerID (16-hex) short ID
|
||||||
func getCryptoIdentitiesByPeerIDPrefix(_ peerID: String) -> [CryptographicIdentity] {
|
func getCryptoIdentitiesByPeerIDPrefix(_ peerID: String) -> [CryptographicIdentity] {
|
||||||
queue.sync {
|
queue.sync {
|
||||||
@@ -315,12 +328,6 @@ final class SecureIdentityStateManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAllSocialIdentities() -> [SocialIdentity] {
|
|
||||||
queue.sync {
|
|
||||||
return Array(cache.socialIdentities.values)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateSocialIdentity(_ identity: SocialIdentity) {
|
func updateSocialIdentity(_ identity: SocialIdentity) {
|
||||||
queue.async(flags: .barrier) {
|
queue.async(flags: .barrier) {
|
||||||
self.cache.socialIdentities[identity.fingerprint] = identity
|
self.cache.socialIdentities[identity.fingerprint] = identity
|
||||||
@@ -469,53 +476,6 @@ final class SecureIdentityStateManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getHandshakeState(peerID: String) -> HandshakeState? {
|
|
||||||
queue.sync {
|
|
||||||
return ephemeralSessions[peerID]?.handshakeState
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Pending Actions
|
|
||||||
|
|
||||||
func setPendingAction(peerID: String, action: PendingActions) {
|
|
||||||
queue.async(flags: .barrier) {
|
|
||||||
self.pendingActions[peerID] = action
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyPendingActions(peerID: String, fingerprint: String) {
|
|
||||||
queue.async(flags: .barrier) {
|
|
||||||
guard let actions = self.pendingActions[peerID] else { return }
|
|
||||||
|
|
||||||
// Get or create social identity
|
|
||||||
var identity = self.cache.socialIdentities[fingerprint] ?? SocialIdentity(
|
|
||||||
fingerprint: fingerprint,
|
|
||||||
localPetname: nil,
|
|
||||||
claimedNickname: "Unknown",
|
|
||||||
trustLevel: .unknown,
|
|
||||||
isFavorite: false,
|
|
||||||
isBlocked: false,
|
|
||||||
notes: nil
|
|
||||||
)
|
|
||||||
|
|
||||||
// Apply pending actions
|
|
||||||
if let toggleFavorite = actions.toggleFavorite {
|
|
||||||
identity.isFavorite = toggleFavorite
|
|
||||||
}
|
|
||||||
if let trustLevel = actions.setTrustLevel {
|
|
||||||
identity.trustLevel = trustLevel
|
|
||||||
}
|
|
||||||
if let petname = actions.setPetname {
|
|
||||||
identity.localPetname = petname
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save updated identity
|
|
||||||
self.cache.socialIdentities[fingerprint] = identity
|
|
||||||
self.pendingActions.removeValue(forKey: peerID)
|
|
||||||
self.saveIdentityCache()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Cleanup
|
// MARK: - Cleanup
|
||||||
|
|
||||||
func clearAllIdentityData() {
|
func clearAllIdentityData() {
|
||||||
@@ -525,7 +485,6 @@ final class SecureIdentityStateManager {
|
|||||||
self.cache = IdentityCache()
|
self.cache = IdentityCache()
|
||||||
self.ephemeralSessions.removeAll()
|
self.ephemeralSessions.removeAll()
|
||||||
self.cryptographicIdentities.removeAll()
|
self.cryptographicIdentities.removeAll()
|
||||||
self.pendingActions.removeAll()
|
|
||||||
|
|
||||||
// Delete from keychain
|
// Delete from keychain
|
||||||
let deleted = self.keychain.deleteIdentityKey(forKey: self.cacheKey)
|
let deleted = self.keychain.deleteIdentityKey(forKey: self.cacheKey)
|
||||||
@@ -536,7 +495,6 @@ final class SecureIdentityStateManager {
|
|||||||
func removeEphemeralSession(peerID: String) {
|
func removeEphemeralSession(peerID: String) {
|
||||||
queue.async(flags: .barrier) {
|
queue.async(flags: .barrier) {
|
||||||
self.ephemeralSessions.removeValue(forKey: peerID)
|
self.ephemeralSessions.removeValue(forKey: peerID)
|
||||||
self.pendingActions.removeValue(forKey: peerID)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -490,6 +490,7 @@ final class NoiseSymmetricState {
|
|||||||
final class NoiseHandshakeState {
|
final class NoiseHandshakeState {
|
||||||
private let role: NoiseRole
|
private let role: NoiseRole
|
||||||
private let pattern: NoisePattern
|
private let pattern: NoisePattern
|
||||||
|
private let keychain: KeychainManagerProtocol
|
||||||
private var symmetricState: NoiseSymmetricState
|
private var symmetricState: NoiseSymmetricState
|
||||||
|
|
||||||
// Keys
|
// Keys
|
||||||
@@ -505,9 +506,16 @@ final class NoiseHandshakeState {
|
|||||||
private var messagePatterns: [[NoiseMessagePattern]] = []
|
private var messagePatterns: [[NoiseMessagePattern]] = []
|
||||||
private var currentPattern = 0
|
private var currentPattern = 0
|
||||||
|
|
||||||
init(role: NoiseRole, pattern: NoisePattern, localStaticKey: Curve25519.KeyAgreement.PrivateKey? = nil, remoteStaticKey: Curve25519.KeyAgreement.PublicKey? = nil) {
|
init(
|
||||||
|
role: NoiseRole,
|
||||||
|
pattern: NoisePattern,
|
||||||
|
keychain: KeychainManagerProtocol,
|
||||||
|
localStaticKey: Curve25519.KeyAgreement.PrivateKey? = nil,
|
||||||
|
remoteStaticKey: Curve25519.KeyAgreement.PublicKey? = nil
|
||||||
|
) {
|
||||||
self.role = role
|
self.role = role
|
||||||
self.pattern = pattern
|
self.pattern = pattern
|
||||||
|
self.keychain = keychain
|
||||||
|
|
||||||
// Initialize static keys
|
// Initialize static keys
|
||||||
if let localKey = localStaticKey {
|
if let localKey = localStaticKey {
|
||||||
@@ -578,7 +586,7 @@ final class NoiseHandshakeState {
|
|||||||
var sharedData = shared.withUnsafeBytes { Data($0) }
|
var sharedData = shared.withUnsafeBytes { Data($0) }
|
||||||
symmetricState.mixKey(sharedData)
|
symmetricState.mixKey(sharedData)
|
||||||
// Clear sensitive shared secret
|
// Clear sensitive shared secret
|
||||||
KeychainManager.secureClear(&sharedData)
|
keychain.secureClear(&sharedData)
|
||||||
|
|
||||||
case .es:
|
case .es:
|
||||||
// DH(ephemeral, static) - direction depends on role
|
// DH(ephemeral, static) - direction depends on role
|
||||||
@@ -626,7 +634,7 @@ final class NoiseHandshakeState {
|
|||||||
var sharedData = shared.withUnsafeBytes { Data($0) }
|
var sharedData = shared.withUnsafeBytes { Data($0) }
|
||||||
symmetricState.mixKey(sharedData)
|
symmetricState.mixKey(sharedData)
|
||||||
// Clear sensitive shared secret
|
// Clear sensitive shared secret
|
||||||
KeychainManager.secureClear(&sharedData)
|
keychain.secureClear(&sharedData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -714,7 +722,7 @@ final class NoiseHandshakeState {
|
|||||||
var sharedData = shared.withUnsafeBytes { Data($0) }
|
var sharedData = shared.withUnsafeBytes { Data($0) }
|
||||||
symmetricState.mixKey(sharedData)
|
symmetricState.mixKey(sharedData)
|
||||||
// Clear sensitive shared secret
|
// Clear sensitive shared secret
|
||||||
KeychainManager.secureClear(&sharedData)
|
keychain.secureClear(&sharedData)
|
||||||
} else {
|
} else {
|
||||||
guard let localStatic = localStaticPrivate,
|
guard let localStatic = localStaticPrivate,
|
||||||
let remoteEphemeral = remoteEphemeralPublic else {
|
let remoteEphemeral = remoteEphemeralPublic else {
|
||||||
@@ -724,7 +732,7 @@ final class NoiseHandshakeState {
|
|||||||
var sharedData = shared.withUnsafeBytes { Data($0) }
|
var sharedData = shared.withUnsafeBytes { Data($0) }
|
||||||
symmetricState.mixKey(sharedData)
|
symmetricState.mixKey(sharedData)
|
||||||
// Clear sensitive shared secret
|
// Clear sensitive shared secret
|
||||||
KeychainManager.secureClear(&sharedData)
|
keychain.secureClear(&sharedData)
|
||||||
}
|
}
|
||||||
|
|
||||||
case .se:
|
case .se:
|
||||||
@@ -737,7 +745,7 @@ final class NoiseHandshakeState {
|
|||||||
var sharedData = shared.withUnsafeBytes { Data($0) }
|
var sharedData = shared.withUnsafeBytes { Data($0) }
|
||||||
symmetricState.mixKey(sharedData)
|
symmetricState.mixKey(sharedData)
|
||||||
// Clear sensitive shared secret
|
// Clear sensitive shared secret
|
||||||
KeychainManager.secureClear(&sharedData)
|
keychain.secureClear(&sharedData)
|
||||||
} else {
|
} else {
|
||||||
guard let localEphemeral = localEphemeralPrivate,
|
guard let localEphemeral = localEphemeralPrivate,
|
||||||
let remoteStatic = remoteStaticPublic else {
|
let remoteStatic = remoteStaticPublic else {
|
||||||
@@ -747,7 +755,7 @@ final class NoiseHandshakeState {
|
|||||||
var sharedData = shared.withUnsafeBytes { Data($0) }
|
var sharedData = shared.withUnsafeBytes { Data($0) }
|
||||||
symmetricState.mixKey(sharedData)
|
symmetricState.mixKey(sharedData)
|
||||||
// Clear sensitive shared secret
|
// Clear sensitive shared secret
|
||||||
KeychainManager.secureClear(&sharedData)
|
keychain.secureClear(&sharedData)
|
||||||
}
|
}
|
||||||
|
|
||||||
case .ss:
|
case .ss:
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ enum NoiseSessionState: Equatable {
|
|||||||
class NoiseSession {
|
class NoiseSession {
|
||||||
let peerID: String
|
let peerID: String
|
||||||
let role: NoiseRole
|
let role: NoiseRole
|
||||||
|
private let keychain: KeychainManagerProtocol
|
||||||
private var state: NoiseSessionState = .uninitialized
|
private var state: NoiseSessionState = .uninitialized
|
||||||
private var handshakeState: NoiseHandshakeState?
|
private var handshakeState: NoiseHandshakeState?
|
||||||
private var sendCipher: NoiseCipherState?
|
private var sendCipher: NoiseCipherState?
|
||||||
@@ -52,9 +53,16 @@ class NoiseSession {
|
|||||||
// Thread safety
|
// Thread safety
|
||||||
private let sessionQueue = DispatchQueue(label: "chat.bitchat.noise.session", attributes: .concurrent)
|
private let sessionQueue = DispatchQueue(label: "chat.bitchat.noise.session", attributes: .concurrent)
|
||||||
|
|
||||||
init(peerID: String, role: NoiseRole, localStaticKey: Curve25519.KeyAgreement.PrivateKey, remoteStaticKey: Curve25519.KeyAgreement.PublicKey? = nil) {
|
init(
|
||||||
|
peerID: String,
|
||||||
|
role: NoiseRole,
|
||||||
|
keychain: KeychainManagerProtocol,
|
||||||
|
localStaticKey: Curve25519.KeyAgreement.PrivateKey,
|
||||||
|
remoteStaticKey: Curve25519.KeyAgreement.PublicKey? = nil
|
||||||
|
) {
|
||||||
self.peerID = peerID
|
self.peerID = peerID
|
||||||
self.role = role
|
self.role = role
|
||||||
|
self.keychain = keychain
|
||||||
self.localStaticKey = localStaticKey
|
self.localStaticKey = localStaticKey
|
||||||
self.remoteStaticPublicKey = remoteStaticKey
|
self.remoteStaticPublicKey = remoteStaticKey
|
||||||
}
|
}
|
||||||
@@ -71,6 +79,7 @@ class NoiseSession {
|
|||||||
handshakeState = NoiseHandshakeState(
|
handshakeState = NoiseHandshakeState(
|
||||||
role: role,
|
role: role,
|
||||||
pattern: .XX,
|
pattern: .XX,
|
||||||
|
keychain: keychain,
|
||||||
localStaticKey: localStaticKey,
|
localStaticKey: localStaticKey,
|
||||||
remoteStaticKey: nil
|
remoteStaticKey: nil
|
||||||
)
|
)
|
||||||
@@ -98,6 +107,7 @@ class NoiseSession {
|
|||||||
handshakeState = NoiseHandshakeState(
|
handshakeState = NoiseHandshakeState(
|
||||||
role: role,
|
role: role,
|
||||||
pattern: .XX,
|
pattern: .XX,
|
||||||
|
keychain: keychain,
|
||||||
localStaticKey: localStaticKey,
|
localStaticKey: localStaticKey,
|
||||||
remoteStaticKey: nil
|
remoteStaticKey: nil
|
||||||
)
|
)
|
||||||
@@ -230,13 +240,13 @@ class NoiseSession {
|
|||||||
// Clear sent handshake messages
|
// Clear sent handshake messages
|
||||||
for i in 0..<sentHandshakeMessages.count {
|
for i in 0..<sentHandshakeMessages.count {
|
||||||
var message = sentHandshakeMessages[i]
|
var message = sentHandshakeMessages[i]
|
||||||
KeychainManager.secureClear(&message)
|
keychain.secureClear(&message)
|
||||||
}
|
}
|
||||||
sentHandshakeMessages.removeAll()
|
sentHandshakeMessages.removeAll()
|
||||||
|
|
||||||
// Clear handshake hash
|
// Clear handshake hash
|
||||||
if var hash = handshakeHash {
|
if var hash = handshakeHash {
|
||||||
KeychainManager.secureClear(&hash)
|
keychain.secureClear(&hash)
|
||||||
}
|
}
|
||||||
handshakeHash = nil
|
handshakeHash = nil
|
||||||
|
|
||||||
@@ -252,14 +262,16 @@ class NoiseSession {
|
|||||||
final class NoiseSessionManager {
|
final class NoiseSessionManager {
|
||||||
private var sessions: [String: NoiseSession] = [:]
|
private var sessions: [String: NoiseSession] = [:]
|
||||||
private let localStaticKey: Curve25519.KeyAgreement.PrivateKey
|
private let localStaticKey: Curve25519.KeyAgreement.PrivateKey
|
||||||
|
private let keychain: KeychainManagerProtocol
|
||||||
private let managerQueue = DispatchQueue(label: "chat.bitchat.noise.manager", attributes: .concurrent)
|
private let managerQueue = DispatchQueue(label: "chat.bitchat.noise.manager", attributes: .concurrent)
|
||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
var onSessionEstablished: ((String, Curve25519.KeyAgreement.PublicKey) -> Void)?
|
var onSessionEstablished: ((String, Curve25519.KeyAgreement.PublicKey) -> Void)?
|
||||||
var onSessionFailed: ((String, Error) -> Void)?
|
var onSessionFailed: ((String, Error) -> Void)?
|
||||||
|
|
||||||
init(localStaticKey: Curve25519.KeyAgreement.PrivateKey) {
|
init(localStaticKey: Curve25519.KeyAgreement.PrivateKey, keychain: KeychainManagerProtocol) {
|
||||||
self.localStaticKey = localStaticKey
|
self.localStaticKey = localStaticKey
|
||||||
|
self.keychain = keychain
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Session Management
|
// MARK: - Session Management
|
||||||
@@ -269,6 +281,7 @@ final class NoiseSessionManager {
|
|||||||
let session = SecureNoiseSession(
|
let session = SecureNoiseSession(
|
||||||
peerID: peerID,
|
peerID: peerID,
|
||||||
role: role,
|
role: role,
|
||||||
|
keychain: keychain,
|
||||||
localStaticKey: localStaticKey
|
localStaticKey: localStaticKey
|
||||||
)
|
)
|
||||||
sessions[peerID] = session
|
sessions[peerID] = session
|
||||||
@@ -320,6 +333,7 @@ final class NoiseSessionManager {
|
|||||||
let session = SecureNoiseSession(
|
let session = SecureNoiseSession(
|
||||||
peerID: peerID,
|
peerID: peerID,
|
||||||
role: .initiator,
|
role: .initiator,
|
||||||
|
keychain: keychain,
|
||||||
localStaticKey: localStaticKey
|
localStaticKey: localStaticKey
|
||||||
)
|
)
|
||||||
sessions[peerID] = session
|
sessions[peerID] = session
|
||||||
@@ -370,6 +384,7 @@ final class NoiseSessionManager {
|
|||||||
let newSession = SecureNoiseSession(
|
let newSession = SecureNoiseSession(
|
||||||
peerID: peerID,
|
peerID: peerID,
|
||||||
role: .responder,
|
role: .responder,
|
||||||
|
keychain: keychain,
|
||||||
localStaticKey: localStaticKey
|
localStaticKey: localStaticKey
|
||||||
)
|
)
|
||||||
sessions[peerID] = newSession
|
sessions[peerID] = newSession
|
||||||
|
|||||||
@@ -88,7 +88,8 @@ final class BLEService: NSObject {
|
|||||||
|
|
||||||
var myPeerID: String = ""
|
var myPeerID: String = ""
|
||||||
var myNickname: String = "anon"
|
var myNickname: String = "anon"
|
||||||
private let noiseService = NoiseEncryptionService()
|
private let noiseService: NoiseEncryptionService
|
||||||
|
private let identityManager: SecureIdentityStateManagerProtocol
|
||||||
private var myPeerIDData: Data = Data()
|
private var myPeerIDData: Data = Data()
|
||||||
|
|
||||||
// MARK: - Advertising Privacy
|
// MARK: - Advertising Privacy
|
||||||
@@ -325,7 +326,9 @@ final class BLEService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override init() {
|
init(keychain: KeychainManagerProtocol, identityManager: SecureIdentityStateManagerProtocol) {
|
||||||
|
noiseService = NoiseEncryptionService(keychain: keychain)
|
||||||
|
self.identityManager = identityManager
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
// Derive stable peer ID from Noise static public key fingerprint (first 8 bytes → 16 hex chars)
|
// Derive stable peer ID from Noise static public key fingerprint (first 8 bytes → 16 hex chars)
|
||||||
@@ -1548,7 +1551,7 @@ final class BLEService: NSObject {
|
|||||||
// Derive fingerprint from Noise public key
|
// Derive fingerprint from Noise public key
|
||||||
let hash = SHA256.hash(data: announcement.noisePublicKey)
|
let hash = SHA256.hash(data: announcement.noisePublicKey)
|
||||||
let fingerprint = hash.map { String(format: "%02x", $0) }.joined()
|
let fingerprint = hash.map { String(format: "%02x", $0) }.joined()
|
||||||
SecureIdentityStateManager.shared.upsertCryptographicIdentity(
|
identityManager.upsertCryptographicIdentity(
|
||||||
fingerprint: fingerprint,
|
fingerprint: fingerprint,
|
||||||
noisePublicKey: announcement.noisePublicKey,
|
noisePublicKey: announcement.noisePublicKey,
|
||||||
signingPublicKey: announcement.signingPublicKey,
|
signingPublicKey: announcement.signingPublicKey,
|
||||||
@@ -1630,13 +1633,13 @@ final class BLEService: NSObject {
|
|||||||
// Fallback: verify signature using persisted signing key for this peerID's fingerprint prefix
|
// Fallback: verify signature using persisted signing key for this peerID's fingerprint prefix
|
||||||
if let signature = packet.signature, let packetData = packet.toBinaryDataForSigning() {
|
if let signature = packet.signature, let packetData = packet.toBinaryDataForSigning() {
|
||||||
// Find candidate identities by peerID prefix (16 hex)
|
// Find candidate identities by peerID prefix (16 hex)
|
||||||
let candidates = SecureIdentityStateManager.shared.getCryptoIdentitiesByPeerIDPrefix(peerID)
|
let candidates = identityManager.getCryptoIdentitiesByPeerIDPrefix(peerID)
|
||||||
for candidate in candidates {
|
for candidate in candidates {
|
||||||
if let signingKey = candidate.signingPublicKey,
|
if let signingKey = candidate.signingPublicKey,
|
||||||
noiseService.verifySignature(signature, for: packetData, publicKey: signingKey) {
|
noiseService.verifySignature(signature, for: packetData, publicKey: signingKey) {
|
||||||
accepted = true
|
accepted = true
|
||||||
// Prefer persisted social petname or claimed nickname
|
// Prefer persisted social petname or claimed nickname
|
||||||
if let social = SecureIdentityStateManager.shared.getSocialIdentity(for: candidate.fingerprint) {
|
if let social = identityManager.getSocialIdentity(for: candidate.fingerprint) {
|
||||||
senderNickname = social.localPetname ?? social.claimedNickname
|
senderNickname = social.localPetname ?? social.claimedNickname
|
||||||
} else {
|
} else {
|
||||||
senderNickname = "anon" + String(peerID.prefix(4))
|
senderNickname = "anon" + String(peerID.prefix(4))
|
||||||
|
|||||||
@@ -20,10 +20,12 @@ enum CommandResult {
|
|||||||
final class CommandProcessor {
|
final class CommandProcessor {
|
||||||
weak var chatViewModel: ChatViewModel?
|
weak var chatViewModel: ChatViewModel?
|
||||||
weak var meshService: Transport?
|
weak var meshService: Transport?
|
||||||
|
private let identityManager: SecureIdentityStateManagerProtocol
|
||||||
|
|
||||||
init(chatViewModel: ChatViewModel? = nil, meshService: Transport? = nil) {
|
init(chatViewModel: ChatViewModel? = nil, meshService: Transport? = nil, identityManager: SecureIdentityStateManagerProtocol) {
|
||||||
self.chatViewModel = chatViewModel
|
self.chatViewModel = chatViewModel
|
||||||
self.meshService = meshService
|
self.meshService = meshService
|
||||||
|
self.identityManager = identityManager
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process a command string
|
/// Process a command string
|
||||||
@@ -189,7 +191,7 @@ final class CommandProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Geohash blocked names (prefer visible display names; fallback to #suffix)
|
// Geohash blocked names (prefer visible display names; fallback to #suffix)
|
||||||
let geoBlocked = Array(SecureIdentityStateManager.shared.getBlockedNostrPubkeys())
|
let geoBlocked = Array(identityManager.getBlockedNostrPubkeys())
|
||||||
var geoNames: [String] = []
|
var geoNames: [String] = []
|
||||||
if let vm = chatViewModel {
|
if let vm = chatViewModel {
|
||||||
let visible = vm.visibleGeohashPeople()
|
let visible = vm.visibleGeohashPeople()
|
||||||
@@ -213,14 +215,14 @@ final class CommandProcessor {
|
|||||||
|
|
||||||
if let peerID = chatViewModel?.getPeerIDForNickname(nickname),
|
if let peerID = chatViewModel?.getPeerIDForNickname(nickname),
|
||||||
let fingerprint = meshService?.getFingerprint(for: peerID) {
|
let fingerprint = meshService?.getFingerprint(for: peerID) {
|
||||||
if SecureIdentityStateManager.shared.isBlocked(fingerprint: fingerprint) {
|
if identityManager.isBlocked(fingerprint: fingerprint) {
|
||||||
return .success(message: "\(nickname) is already blocked")
|
return .success(message: "\(nickname) is already blocked")
|
||||||
}
|
}
|
||||||
// Block the user (mesh/noise identity)
|
// Block the user (mesh/noise identity)
|
||||||
if var identity = SecureIdentityStateManager.shared.getSocialIdentity(for: fingerprint) {
|
if var identity = identityManager.getSocialIdentity(for: fingerprint) {
|
||||||
identity.isBlocked = true
|
identity.isBlocked = true
|
||||||
identity.isFavorite = false
|
identity.isFavorite = false
|
||||||
SecureIdentityStateManager.shared.updateSocialIdentity(identity)
|
identityManager.updateSocialIdentity(identity)
|
||||||
} else {
|
} else {
|
||||||
let blockedIdentity = SocialIdentity(
|
let blockedIdentity = SocialIdentity(
|
||||||
fingerprint: fingerprint,
|
fingerprint: fingerprint,
|
||||||
@@ -231,16 +233,16 @@ final class CommandProcessor {
|
|||||||
isBlocked: true,
|
isBlocked: true,
|
||||||
notes: nil
|
notes: nil
|
||||||
)
|
)
|
||||||
SecureIdentityStateManager.shared.updateSocialIdentity(blockedIdentity)
|
identityManager.updateSocialIdentity(blockedIdentity)
|
||||||
}
|
}
|
||||||
return .success(message: "blocked \(nickname). you will no longer receive messages from them")
|
return .success(message: "blocked \(nickname). you will no longer receive messages from them")
|
||||||
}
|
}
|
||||||
// Mesh lookup failed; try geohash (Nostr) participant by display name
|
// Mesh lookup failed; try geohash (Nostr) participant by display name
|
||||||
if let pub = chatViewModel?.nostrPubkeyForDisplayName(nickname) {
|
if let pub = chatViewModel?.nostrPubkeyForDisplayName(nickname) {
|
||||||
if SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: pub) {
|
if identityManager.isNostrBlocked(pubkeyHexLowercased: pub) {
|
||||||
return .success(message: "\(nickname) is already blocked")
|
return .success(message: "\(nickname) is already blocked")
|
||||||
}
|
}
|
||||||
SecureIdentityStateManager.shared.setNostrBlocked(pub, isBlocked: true)
|
identityManager.setNostrBlocked(pub, isBlocked: true)
|
||||||
return .success(message: "blocked \(nickname) in geohash chats")
|
return .success(message: "blocked \(nickname) in geohash chats")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,18 +259,18 @@ final class CommandProcessor {
|
|||||||
|
|
||||||
if let peerID = chatViewModel?.getPeerIDForNickname(nickname),
|
if let peerID = chatViewModel?.getPeerIDForNickname(nickname),
|
||||||
let fingerprint = meshService?.getFingerprint(for: peerID) {
|
let fingerprint = meshService?.getFingerprint(for: peerID) {
|
||||||
if !SecureIdentityStateManager.shared.isBlocked(fingerprint: fingerprint) {
|
if !identityManager.isBlocked(fingerprint: fingerprint) {
|
||||||
return .success(message: "\(nickname) is not blocked")
|
return .success(message: "\(nickname) is not blocked")
|
||||||
}
|
}
|
||||||
SecureIdentityStateManager.shared.setBlocked(fingerprint, isBlocked: false)
|
identityManager.setBlocked(fingerprint, isBlocked: false)
|
||||||
return .success(message: "unblocked \(nickname)")
|
return .success(message: "unblocked \(nickname)")
|
||||||
}
|
}
|
||||||
// Try geohash unblock
|
// Try geohash unblock
|
||||||
if let pub = chatViewModel?.nostrPubkeyForDisplayName(nickname) {
|
if let pub = chatViewModel?.nostrPubkeyForDisplayName(nickname) {
|
||||||
if !SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: pub) {
|
if !identityManager.isNostrBlocked(pubkeyHexLowercased: pub) {
|
||||||
return .success(message: "\(nickname) is not blocked")
|
return .success(message: "\(nickname) is not blocked")
|
||||||
}
|
}
|
||||||
SecureIdentityStateManager.shared.setNostrBlocked(pub, isBlocked: false)
|
identityManager.setNostrBlocked(pub, isBlocked: false)
|
||||||
return .success(message: "unblocked \(nickname) in geohash chats")
|
return .success(message: "unblocked \(nickname) in geohash chats")
|
||||||
}
|
}
|
||||||
return .error(message: "cannot unblock \(nickname): not found")
|
return .error(message: "cannot unblock \(nickname): not found")
|
||||||
|
|||||||
@@ -13,12 +13,16 @@ final class FavoritesPersistenceService: ObservableObject {
|
|||||||
let theyFavoritedUs: Bool
|
let theyFavoritedUs: Bool
|
||||||
let favoritedAt: Date
|
let favoritedAt: Date
|
||||||
let lastUpdated: Date
|
let lastUpdated: Date
|
||||||
|
// Track what we last sent as OUR npub to this peer, to avoid resending unless it changes
|
||||||
|
// Note: we do not track which npub we last sent to them; sending happens only on favorite toggle
|
||||||
|
|
||||||
var isMutual: Bool {
|
var isMutual: Bool {
|
||||||
isFavorite && theyFavoritedUs
|
isFavorite && theyFavoritedUs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We intentionally do not track when we last sent our npub; sending happens only on favorite toggle.
|
||||||
|
|
||||||
private static let storageKey = "chat.bitchat.favorites"
|
private static let storageKey = "chat.bitchat.favorites"
|
||||||
private static let keychainService = "chat.bitchat.favorites"
|
private static let keychainService = "chat.bitchat.favorites"
|
||||||
|
|
||||||
|
|||||||
@@ -9,16 +9,23 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Security
|
import Security
|
||||||
|
|
||||||
final class KeychainManager {
|
protocol KeychainManagerProtocol {
|
||||||
static let shared = KeychainManager()
|
func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool
|
||||||
|
func getIdentityKey(forKey key: String) -> Data?
|
||||||
|
func deleteIdentityKey(forKey key: String) -> Bool
|
||||||
|
func deleteAllKeychainData() -> Bool
|
||||||
|
|
||||||
|
func secureClear(_ data: inout Data)
|
||||||
|
func secureClear(_ string: inout String)
|
||||||
|
|
||||||
|
func verifyIdentityKeyExists() -> Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
final class KeychainManager: KeychainManagerProtocol {
|
||||||
// Use consistent service name for all keychain items
|
// Use consistent service name for all keychain items
|
||||||
private let service = "chat.bitchat"
|
private let service = "chat.bitchat"
|
||||||
private let appGroup = "group.chat.bitchat"
|
private let appGroup = "group.chat.bitchat"
|
||||||
|
|
||||||
private init() {}
|
|
||||||
|
|
||||||
|
|
||||||
private func isSandboxed() -> Bool {
|
private func isSandboxed() -> Bool {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
// More robust sandbox detection using multiple methods
|
// More robust sandbox detection using multiple methods
|
||||||
@@ -310,7 +317,7 @@ final class KeychainManager {
|
|||||||
// MARK: - Security Utilities
|
// MARK: - Security Utilities
|
||||||
|
|
||||||
/// Securely clear sensitive data from memory
|
/// Securely clear sensitive data from memory
|
||||||
static func secureClear(_ data: inout Data) {
|
func secureClear(_ data: inout Data) {
|
||||||
_ = data.withUnsafeMutableBytes { bytes in
|
_ = data.withUnsafeMutableBytes { bytes in
|
||||||
// Use volatile memset to prevent compiler optimization
|
// Use volatile memset to prevent compiler optimization
|
||||||
memset_s(bytes.baseAddress, bytes.count, 0, bytes.count)
|
memset_s(bytes.baseAddress, bytes.count, 0, bytes.count)
|
||||||
@@ -319,7 +326,7 @@ final class KeychainManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Securely clear sensitive string from memory
|
/// Securely clear sensitive string from memory
|
||||||
static func secureClear(_ string: inout String) {
|
func secureClear(_ string: inout String) {
|
||||||
// Convert to mutable data and clear
|
// Convert to mutable data and clear
|
||||||
if var data = string.data(using: .utf8) {
|
if var data = string.data(using: .utf8) {
|
||||||
secureClear(&data)
|
secureClear(&data)
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ final class MessageRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func sendFavoriteNotification(to peerID: String, isFavorite: Bool) {
|
func sendFavoriteNotification(to peerID: String, isFavorite: Bool) {
|
||||||
|
// Route via mesh when connected; else use Nostr
|
||||||
if mesh.isPeerConnected(peerID) {
|
if mesh.isPeerConnected(peerID) {
|
||||||
mesh.sendFavoriteNotification(to: peerID, isFavorite: isFavorite)
|
mesh.sendFavoriteNotification(to: peerID, isFavorite: isFavorite)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ final class NoiseEncryptionService {
|
|||||||
|
|
||||||
// Security components
|
// Security components
|
||||||
private let rateLimiter = NoiseRateLimiter()
|
private let rateLimiter = NoiseRateLimiter()
|
||||||
|
private let keychain: KeychainManagerProtocol
|
||||||
|
|
||||||
// Session maintenance
|
// Session maintenance
|
||||||
private var rekeyTimer: Timer?
|
private var rekeyTimer: Timer?
|
||||||
@@ -181,12 +182,14 @@ final class NoiseEncryptionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init(keychain: KeychainManagerProtocol) {
|
||||||
|
self.keychain = keychain
|
||||||
|
|
||||||
// Load or create static identity key (ONLY from keychain)
|
// Load or create static identity key (ONLY from keychain)
|
||||||
let loadedKey: Curve25519.KeyAgreement.PrivateKey
|
let loadedKey: Curve25519.KeyAgreement.PrivateKey
|
||||||
|
|
||||||
// Try to load from keychain
|
// Try to load from keychain
|
||||||
if let identityData = KeychainManager.shared.getIdentityKey(forKey: "noiseStaticKey"),
|
if let identityData = keychain.getIdentityKey(forKey: "noiseStaticKey"),
|
||||||
let key = try? Curve25519.KeyAgreement.PrivateKey(rawRepresentation: identityData) {
|
let key = try? Curve25519.KeyAgreement.PrivateKey(rawRepresentation: identityData) {
|
||||||
loadedKey = key
|
loadedKey = key
|
||||||
SecureLogger.logKeyOperation(.load, keyType: "noiseStaticKey", success: true)
|
SecureLogger.logKeyOperation(.load, keyType: "noiseStaticKey", success: true)
|
||||||
@@ -197,7 +200,7 @@ final class NoiseEncryptionService {
|
|||||||
let keyData = loadedKey.rawRepresentation
|
let keyData = loadedKey.rawRepresentation
|
||||||
|
|
||||||
// Save to keychain
|
// Save to keychain
|
||||||
let saved = KeychainManager.shared.saveIdentityKey(keyData, forKey: "noiseStaticKey")
|
let saved = keychain.saveIdentityKey(keyData, forKey: "noiseStaticKey")
|
||||||
SecureLogger.logKeyOperation(.create, keyType: "noiseStaticKey", success: saved)
|
SecureLogger.logKeyOperation(.create, keyType: "noiseStaticKey", success: saved)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +212,7 @@ final class NoiseEncryptionService {
|
|||||||
let loadedSigningKey: Curve25519.Signing.PrivateKey
|
let loadedSigningKey: Curve25519.Signing.PrivateKey
|
||||||
|
|
||||||
// Try to load from keychain
|
// Try to load from keychain
|
||||||
if let signingData = KeychainManager.shared.getIdentityKey(forKey: "ed25519SigningKey"),
|
if let signingData = keychain.getIdentityKey(forKey: "ed25519SigningKey"),
|
||||||
let key = try? Curve25519.Signing.PrivateKey(rawRepresentation: signingData) {
|
let key = try? Curve25519.Signing.PrivateKey(rawRepresentation: signingData) {
|
||||||
loadedSigningKey = key
|
loadedSigningKey = key
|
||||||
SecureLogger.logKeyOperation(.load, keyType: "ed25519SigningKey", success: true)
|
SecureLogger.logKeyOperation(.load, keyType: "ed25519SigningKey", success: true)
|
||||||
@@ -220,7 +223,7 @@ final class NoiseEncryptionService {
|
|||||||
let keyData = loadedSigningKey.rawRepresentation
|
let keyData = loadedSigningKey.rawRepresentation
|
||||||
|
|
||||||
// Save to keychain
|
// Save to keychain
|
||||||
let saved = KeychainManager.shared.saveIdentityKey(keyData, forKey: "ed25519SigningKey")
|
let saved = keychain.saveIdentityKey(keyData, forKey: "ed25519SigningKey")
|
||||||
SecureLogger.logKeyOperation(.create, keyType: "ed25519SigningKey", success: saved)
|
SecureLogger.logKeyOperation(.create, keyType: "ed25519SigningKey", success: saved)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +232,7 @@ final class NoiseEncryptionService {
|
|||||||
self.signingPublicKey = signingKey.publicKey
|
self.signingPublicKey = signingKey.publicKey
|
||||||
|
|
||||||
// Initialize session manager
|
// Initialize session manager
|
||||||
self.sessionManager = NoiseSessionManager(localStaticKey: staticIdentityKey)
|
self.sessionManager = NoiseSessionManager(localStaticKey: staticIdentityKey, keychain: keychain)
|
||||||
|
|
||||||
// Set up session callbacks
|
// Set up session callbacks
|
||||||
sessionManager.onSessionEstablished = { [weak self] peerID, remoteStaticKey in
|
sessionManager.onSessionEstablished = { [weak self] peerID, remoteStaticKey in
|
||||||
@@ -266,8 +269,8 @@ final class NoiseEncryptionService {
|
|||||||
/// Clear persistent identity (for panic mode)
|
/// Clear persistent identity (for panic mode)
|
||||||
func clearPersistentIdentity() {
|
func clearPersistentIdentity() {
|
||||||
// Clear from keychain
|
// Clear from keychain
|
||||||
let deletedStatic = KeychainManager.shared.deleteIdentityKey(forKey: "noiseStaticKey")
|
let deletedStatic = keychain.deleteIdentityKey(forKey: "noiseStaticKey")
|
||||||
let deletedSigning = KeychainManager.shared.deleteIdentityKey(forKey: "ed25519SigningKey")
|
let deletedSigning = keychain.deleteIdentityKey(forKey: "ed25519SigningKey")
|
||||||
SecureLogger.logKeyOperation(.delete, keyType: "identity keys", success: deletedStatic && deletedSigning)
|
SecureLogger.logKeyOperation(.delete, keyType: "identity keys", success: deletedStatic && deletedSigning)
|
||||||
SecureLogger.warning("Panic mode activated - identity cleared", category: .security)
|
SecureLogger.warning("Panic mode activated - identity cleared", category: .security)
|
||||||
// Stop rekey timer
|
// Stop rekey timer
|
||||||
|
|||||||
@@ -21,11 +21,16 @@ final class NostrTransport: Transport {
|
|||||||
private var readQueue: [QueuedRead] = []
|
private var readQueue: [QueuedRead] = []
|
||||||
private var isSendingReadAcks = false
|
private var isSendingReadAcks = false
|
||||||
private let readAckInterval: TimeInterval = TransportConfig.nostrReadAckInterval
|
private let readAckInterval: TimeInterval = TransportConfig.nostrReadAckInterval
|
||||||
|
private let keychain: KeychainManagerProtocol
|
||||||
|
|
||||||
var myPeerID: String { senderPeerID }
|
var myPeerID: String { senderPeerID }
|
||||||
var myNickname: String { "" }
|
var myNickname: String { "" }
|
||||||
func setNickname(_ nickname: String) { /* not used for Nostr */ }
|
func setNickname(_ nickname: String) { /* not used for Nostr */ }
|
||||||
|
|
||||||
|
init(keychain: KeychainManagerProtocol) {
|
||||||
|
self.keychain = keychain
|
||||||
|
}
|
||||||
|
|
||||||
func startServices() { /* no-op */ }
|
func startServices() { /* no-op */ }
|
||||||
func stopServices() { /* no-op */ }
|
func stopServices() { /* no-op */ }
|
||||||
func emergencyDisconnectAll() { /* no-op */ }
|
func emergencyDisconnectAll() { /* no-op */ }
|
||||||
@@ -38,11 +43,17 @@ final class NostrTransport: Transport {
|
|||||||
func getFingerprint(for peerID: String) -> String? { nil }
|
func getFingerprint(for peerID: String) -> String? { nil }
|
||||||
func getNoiseSessionState(for peerID: String) -> LazyHandshakeState { .none }
|
func getNoiseSessionState(for peerID: String) -> LazyHandshakeState { .none }
|
||||||
func triggerHandshake(with peerID: String) { /* no-op */ }
|
func triggerHandshake(with peerID: String) { /* no-op */ }
|
||||||
|
|
||||||
// Nostr does not use Noise sessions here; return a cached placeholder to avoid reallocation
|
// Nostr does not use Noise sessions here; return a cached placeholder to avoid reallocation
|
||||||
private static var cachedNoiseService: NoiseEncryptionService = {
|
private static var cachedNoiseService: NoiseEncryptionService?
|
||||||
NoiseEncryptionService()
|
func getNoiseService() -> NoiseEncryptionService {
|
||||||
}()
|
if let noiseService = Self.cachedNoiseService {
|
||||||
func getNoiseService() -> NoiseEncryptionService { Self.cachedNoiseService }
|
return noiseService
|
||||||
|
}
|
||||||
|
let noiseService = NoiseEncryptionService(keychain: keychain)
|
||||||
|
Self.cachedNoiseService = noiseService
|
||||||
|
return noiseService
|
||||||
|
}
|
||||||
|
|
||||||
// Public broadcast not supported over Nostr here
|
// Public broadcast not supported over Nostr here
|
||||||
func sendMessage(_ content: String, mentions: [String]) { /* no-op */ }
|
func sendMessage(_ content: String, mentions: [String]) { /* no-op */ }
|
||||||
|
|||||||
@@ -27,14 +27,16 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
|||||||
private var peerIndex: [String: BitchatPeer] = [:]
|
private var peerIndex: [String: BitchatPeer] = [:]
|
||||||
private var fingerprintCache: [String: String] = [:] // peerID -> fingerprint
|
private var fingerprintCache: [String: String] = [:] // peerID -> fingerprint
|
||||||
private let meshService: Transport
|
private let meshService: Transport
|
||||||
|
private let identityManager: SecureIdentityStateManagerProtocol
|
||||||
weak var messageRouter: MessageRouter?
|
weak var messageRouter: MessageRouter?
|
||||||
private let favoritesService = FavoritesPersistenceService.shared
|
private let favoritesService = FavoritesPersistenceService.shared
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
|
|
||||||
init(meshService: Transport) {
|
init(meshService: Transport, identityManager: SecureIdentityStateManagerProtocol) {
|
||||||
self.meshService = meshService
|
self.meshService = meshService
|
||||||
|
self.identityManager = identityManager
|
||||||
|
|
||||||
// Subscribe to changes from both services
|
// Subscribe to changes from both services
|
||||||
setupSubscriptions()
|
setupSubscriptions()
|
||||||
@@ -174,7 +176,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
|||||||
// Determine reachability based on lastSeen and identity trust
|
// Determine reachability based on lastSeen and identity trust
|
||||||
let now = Date()
|
let now = Date()
|
||||||
let fingerprint = peerInfo.noisePublicKey?.sha256Fingerprint()
|
let fingerprint = peerInfo.noisePublicKey?.sha256Fingerprint()
|
||||||
let isVerified = fingerprint.map { SecureIdentityStateManager.shared.isVerified(fingerprint: $0) } ?? false
|
let isVerified = fingerprint.map { identityManager.isVerified(fingerprint: $0) } ?? false
|
||||||
let isFav = peerInfo.noisePublicKey.flatMap { favorites[$0]?.isFavorite } ?? false
|
let isFav = peerInfo.noisePublicKey.flatMap { favorites[$0]?.isFavorite } ?? false
|
||||||
let retention: TimeInterval = (isVerified || isFav) ? TransportConfig.bleReachabilityRetentionVerifiedSeconds : TransportConfig.bleReachabilityRetentionUnverifiedSeconds
|
let retention: TimeInterval = (isVerified || isFav) ? TransportConfig.bleReachabilityRetentionVerifiedSeconds : TransportConfig.bleReachabilityRetentionUnverifiedSeconds
|
||||||
// A peer is reachable if we recently saw them AND we are attached to the mesh
|
// A peer is reachable if we recently saw them AND we are attached to the mesh
|
||||||
@@ -195,27 +197,6 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
|||||||
let favoriteStatus = favorites[noiseKey] {
|
let favoriteStatus = favorites[noiseKey] {
|
||||||
peer.favoriteStatus = favoriteStatus
|
peer.favoriteStatus = favoriteStatus
|
||||||
peer.nostrPublicKey = favoriteStatus.peerNostrPublicKey
|
peer.nostrPublicKey = favoriteStatus.peerNostrPublicKey
|
||||||
} else {
|
|
||||||
// Check by nickname for reconnected peers
|
|
||||||
let favoriteByNickname = favorites.values.first {
|
|
||||||
$0.peerNickname == peerInfo.nickname
|
|
||||||
}
|
|
||||||
|
|
||||||
if let favorite = favoriteByNickname,
|
|
||||||
let noiseKey = peerInfo.noisePublicKey {
|
|
||||||
SecureLogger.debug("🔄 Found favorite for '\(peerInfo.nickname)' by nickname, updating noise key", category: .session)
|
|
||||||
|
|
||||||
// Update the favorite's key in persistence
|
|
||||||
favoritesService.updateNoisePublicKey(
|
|
||||||
from: favorite.peerNoisePublicKey,
|
|
||||||
to: noiseKey,
|
|
||||||
peerNickname: peerInfo.nickname
|
|
||||||
)
|
|
||||||
|
|
||||||
// Get updated favorite
|
|
||||||
peer.favoriteStatus = favoritesService.getFavoriteStatus(for: noiseKey)
|
|
||||||
peer.nostrPublicKey = peer.favoriteStatus?.peerNostrPublicKey ?? favorite.peerNostrPublicKey
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return peer
|
return peer
|
||||||
@@ -268,7 +249,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
|||||||
guard let fingerprint = getFingerprint(for: peerID) else { return false }
|
guard let fingerprint = getFingerprint(for: peerID) else { return false }
|
||||||
|
|
||||||
// Check SecureIdentityStateManager for block status
|
// Check SecureIdentityStateManager for block status
|
||||||
if let identity = SecureIdentityStateManager.shared.getSocialIdentity(for: fingerprint) {
|
if let identity = identityManager.getSocialIdentity(for: fingerprint) {
|
||||||
return identity.isBlocked
|
return identity.isBlocked
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,7 +326,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
|||||||
guard let fingerprint = getFingerprint(for: peerID) else { return }
|
guard let fingerprint = getFingerprint(for: peerID) else { return }
|
||||||
|
|
||||||
// Get or create social identity
|
// Get or create social identity
|
||||||
var identity = SecureIdentityStateManager.shared.getSocialIdentity(for: fingerprint)
|
var identity = identityManager.getSocialIdentity(for: fingerprint)
|
||||||
?? SocialIdentity(
|
?? SocialIdentity(
|
||||||
fingerprint: fingerprint,
|
fingerprint: fingerprint,
|
||||||
localPetname: nil,
|
localPetname: nil,
|
||||||
@@ -368,7 +349,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SecureIdentityStateManager.shared.updateSocialIdentity(identity)
|
identityManager.updateSocialIdentity(identity)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get fingerprint for peer ID
|
/// Get fingerprint for peer ID
|
||||||
|
|||||||
@@ -344,13 +344,16 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
// MARK: - Services and Storage
|
// MARK: - Services and Storage
|
||||||
|
|
||||||
var meshService: Transport = BLEService()
|
let meshService: Transport
|
||||||
|
let identityManager: SecureIdentityStateManagerProtocol
|
||||||
|
|
||||||
private var nostrRelayManager: NostrRelayManager?
|
private var nostrRelayManager: NostrRelayManager?
|
||||||
// PeerManager replaced by UnifiedPeerService
|
// PeerManager replaced by UnifiedPeerService
|
||||||
private var processedNostrEvents = Set<String>() // Simple deduplication
|
private var processedNostrEvents = Set<String>() // Simple deduplication
|
||||||
private var processedNostrEventOrder: [String] = []
|
private var processedNostrEventOrder: [String] = []
|
||||||
private let maxProcessedNostrEvents = TransportConfig.uiProcessedNostrEventsCap
|
private let maxProcessedNostrEvents = TransportConfig.uiProcessedNostrEventsCap
|
||||||
private let userDefaults = UserDefaults.standard
|
private let userDefaults = UserDefaults.standard
|
||||||
|
private let keychain: KeychainManagerProtocol
|
||||||
private let nicknameKey = "bitchat.nickname"
|
private let nicknameKey = "bitchat.nickname"
|
||||||
// Location channel state (macOS supports manual geohash selection)
|
// Location channel state (macOS supports manual geohash selection)
|
||||||
@Published private var activeChannel: ChannelID = .mesh
|
@Published private var activeChannel: ChannelID = .mesh
|
||||||
@@ -420,6 +423,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
private var geoSamplingSubs: [String: String] = [:] // subID -> geohash
|
private var geoSamplingSubs: [String: String] = [:] // subID -> geohash
|
||||||
private var lastGeoNotificationAt: [String: Date] = [:] // geohash -> last notify time
|
private var lastGeoNotificationAt: [String: Date] = [:] // geohash -> last notify time
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Message Delivery Tracking
|
// MARK: - Message Delivery Tracking
|
||||||
|
|
||||||
// Delivery tracking
|
// Delivery tracking
|
||||||
@@ -484,7 +488,14 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
init() {
|
init(
|
||||||
|
keychain: KeychainManagerProtocol,
|
||||||
|
identityManager: SecureIdentityStateManagerProtocol
|
||||||
|
) {
|
||||||
|
self.keychain = keychain
|
||||||
|
self.identityManager = identityManager
|
||||||
|
self.meshService = BLEService(keychain: keychain, identityManager: identityManager)
|
||||||
|
|
||||||
// Load persisted read receipts
|
// Load persisted read receipts
|
||||||
if let data = UserDefaults.standard.data(forKey: "sentReadReceipts"),
|
if let data = UserDefaults.standard.data(forKey: "sentReadReceipts"),
|
||||||
let receipts = try? JSONDecoder().decode([String].self, from: data) {
|
let receipts = try? JSONDecoder().decode([String].self, from: data) {
|
||||||
@@ -495,10 +506,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize services
|
// Initialize services
|
||||||
self.commandProcessor = CommandProcessor()
|
self.commandProcessor = CommandProcessor(identityManager: identityManager)
|
||||||
self.privateChatManager = PrivateChatManager(meshService: meshService)
|
self.privateChatManager = PrivateChatManager(meshService: meshService)
|
||||||
self.unifiedPeerService = UnifiedPeerService(meshService: meshService)
|
self.unifiedPeerService = UnifiedPeerService(meshService: meshService, identityManager: identityManager)
|
||||||
let nostrTransport = NostrTransport()
|
let nostrTransport = NostrTransport(keychain: keychain)
|
||||||
self.messageRouter = MessageRouter(mesh: meshService, nostr: nostrTransport)
|
self.messageRouter = MessageRouter(mesh: meshService, nostr: nostrTransport)
|
||||||
// Route receipts from PrivateChatManager through MessageRouter
|
// Route receipts from PrivateChatManager through MessageRouter
|
||||||
self.privateChatManager.messageRouter = self.messageRouter
|
self.privateChatManager.messageRouter = self.messageRouter
|
||||||
@@ -976,7 +987,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
let messageId = pm.messageID
|
let messageId = pm.messageID
|
||||||
// Send delivery ACK immediately (once per message ID)
|
// Send delivery ACK immediately (once per message ID)
|
||||||
if !self.sentGeoDeliveryAcks.contains(messageId) {
|
if !self.sentGeoDeliveryAcks.contains(messageId) {
|
||||||
let nt = NostrTransport()
|
let nt = NostrTransport(keychain: keychain)
|
||||||
nt.senderPeerID = self.meshService.myPeerID
|
nt.senderPeerID = self.meshService.myPeerID
|
||||||
nt.sendDeliveryAckGeohash(for: messageId, toRecipientHex: senderPubkey, from: id)
|
nt.sendDeliveryAckGeohash(for: messageId, toRecipientHex: senderPubkey, from: id)
|
||||||
self.sentGeoDeliveryAcks.insert(messageId)
|
self.sentGeoDeliveryAcks.insert(messageId)
|
||||||
@@ -1004,7 +1015,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
deliveryStatus: .delivered(to: self.nickname, at: Date())
|
deliveryStatus: .delivered(to: self.nickname, at: Date())
|
||||||
)
|
)
|
||||||
// Respect geohash blocks
|
// Respect geohash blocks
|
||||||
if SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: senderPubkey) {
|
if identityManager.isNostrBlocked(pubkeyHexLowercased: senderPubkey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if self.privateChats[convKey] == nil { self.privateChats[convKey] = [] }
|
if self.privateChats[convKey] == nil { self.privateChats[convKey] = [] }
|
||||||
@@ -1014,7 +1025,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
if isViewing {
|
if isViewing {
|
||||||
// pared back: omit pre-send READ log
|
// pared back: omit pre-send READ log
|
||||||
if !wasReadBefore {
|
if !wasReadBefore {
|
||||||
let nt = NostrTransport()
|
let nt = NostrTransport(keychain: keychain)
|
||||||
nt.senderPeerID = self.meshService.myPeerID
|
nt.senderPeerID = self.meshService.myPeerID
|
||||||
nt.sendReadReceiptGeohash(messageId, toRecipientHex: senderPubkey, from: id)
|
nt.sendReadReceiptGeohash(messageId, toRecipientHex: senderPubkey, from: id)
|
||||||
self.sentReadReceipts.insert(messageId)
|
self.sentReadReceipts.insert(messageId)
|
||||||
@@ -1237,7 +1248,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update identity state manager with handshake completion
|
// Update identity state manager with handshake completion
|
||||||
SecureIdentityStateManager.shared.updateHandshakeState(peerID: peerID, state: .completed(fingerprint: fingerprintStr))
|
identityManager.updateHandshakeState(peerID: peerID, state: .completed(fingerprint: fingerprintStr))
|
||||||
|
|
||||||
// Update encryption status now that we have the fingerprint
|
// Update encryption status now that we have the fingerprint
|
||||||
updateEncryptionStatus(for: peerID)
|
updateEncryptionStatus(for: peerID)
|
||||||
@@ -1246,9 +1257,9 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
let peerNicknames = meshService.getPeerNicknames()
|
let peerNicknames = meshService.getPeerNicknames()
|
||||||
if let nickname = peerNicknames[peerID], nickname != "Unknown" && nickname != "anon\(peerID.prefix(4))" {
|
if let nickname = peerNicknames[peerID], nickname != "Unknown" && nickname != "anon\(peerID.prefix(4))" {
|
||||||
// Update or create social identity with the claimed nickname
|
// Update or create social identity with the claimed nickname
|
||||||
if var identity = SecureIdentityStateManager.shared.getSocialIdentity(for: fingerprintStr) {
|
if var identity = identityManager.getSocialIdentity(for: fingerprintStr) {
|
||||||
identity.claimedNickname = nickname
|
identity.claimedNickname = nickname
|
||||||
SecureIdentityStateManager.shared.updateSocialIdentity(identity)
|
identityManager.updateSocialIdentity(identity)
|
||||||
} else {
|
} else {
|
||||||
let newIdentity = SocialIdentity(
|
let newIdentity = SocialIdentity(
|
||||||
fingerprint: fingerprintStr,
|
fingerprint: fingerprintStr,
|
||||||
@@ -1259,7 +1270,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
isBlocked: false,
|
isBlocked: false,
|
||||||
notes: nil
|
notes: nil
|
||||||
)
|
)
|
||||||
SecureIdentityStateManager.shared.updateSocialIdentity(newIdentity)
|
identityManager.updateSocialIdentity(newIdentity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1620,7 +1631,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
self.geoNicknames[event.pubkey.lowercased()] = nick
|
self.geoNicknames[event.pubkey.lowercased()] = nick
|
||||||
}
|
}
|
||||||
// If this pubkey is blocked, skip mapping, participants, and timeline
|
// If this pubkey is blocked, skip mapping, participants, and timeline
|
||||||
if SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: event.pubkey) {
|
if identityManager.isNostrBlocked(pubkeyHexLowercased: event.pubkey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Store mapping for geohash DM initiation
|
// Store mapping for geohash DM initiation
|
||||||
@@ -1698,7 +1709,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
SecureLogger.info("GeoDM: recv PM <- sender=\(senderPubkey.prefix(8))… mid=\(messageId.prefix(8))…", category: .session)
|
SecureLogger.info("GeoDM: recv PM <- sender=\(senderPubkey.prefix(8))… mid=\(messageId.prefix(8))…", category: .session)
|
||||||
// Send delivery ACK immediately (even if duplicate), once per messageID
|
// Send delivery ACK immediately (even if duplicate), once per messageID
|
||||||
if !self.sentGeoDeliveryAcks.contains(messageId) {
|
if !self.sentGeoDeliveryAcks.contains(messageId) {
|
||||||
let nostrTransport = NostrTransport()
|
let nostrTransport = NostrTransport(keychain: keychain)
|
||||||
nostrTransport.senderPeerID = self.meshService.myPeerID
|
nostrTransport.senderPeerID = self.meshService.myPeerID
|
||||||
// pared back: omit pre-send log
|
// pared back: omit pre-send log
|
||||||
nostrTransport.sendDeliveryAckGeohash(for: messageId, toRecipientHex: senderPubkey, from: id)
|
nostrTransport.sendDeliveryAckGeohash(for: messageId, toRecipientHex: senderPubkey, from: id)
|
||||||
@@ -1733,7 +1744,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
if isViewing {
|
if isViewing {
|
||||||
// pared back: omit pre-send READ log
|
// pared back: omit pre-send READ log
|
||||||
if !wasReadBefore {
|
if !wasReadBefore {
|
||||||
let nostrTransport = NostrTransport()
|
let nostrTransport = NostrTransport(keychain: keychain)
|
||||||
nostrTransport.senderPeerID = self.meshService.myPeerID
|
nostrTransport.senderPeerID = self.meshService.myPeerID
|
||||||
nostrTransport.sendReadReceiptGeohash(messageId, toRecipientHex: senderPubkey, from: id)
|
nostrTransport.sendReadReceiptGeohash(messageId, toRecipientHex: senderPubkey, from: id)
|
||||||
self.sentReadReceipts.insert(messageId)
|
self.sentReadReceipts.insert(messageId)
|
||||||
@@ -1818,7 +1829,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Prune expired entries
|
// Prune expired entries
|
||||||
map = map.filter { $0.value >= cutoff }
|
map = map.filter { $0.value >= cutoff }
|
||||||
// Remove blocked Nostr pubkeys
|
// Remove blocked Nostr pubkeys
|
||||||
map = map.filter { !SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: $0.key) }
|
map = map.filter { !identityManager.isNostrBlocked(pubkeyHexLowercased: $0.key) }
|
||||||
geoParticipants[gh] = map
|
geoParticipants[gh] = map
|
||||||
// Build display list
|
// Build display list
|
||||||
let people = map
|
let people = map
|
||||||
@@ -1851,7 +1862,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
let cutoff = Date().addingTimeInterval(-TransportConfig.uiRecentCutoffFiveMinutesSeconds)
|
let cutoff = Date().addingTimeInterval(-TransportConfig.uiRecentCutoffFiveMinutesSeconds)
|
||||||
let map = (geoParticipants[gh] ?? [:])
|
let map = (geoParticipants[gh] ?? [:])
|
||||||
.filter { $0.value >= cutoff }
|
.filter { $0.value >= cutoff }
|
||||||
.filter { !SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: $0.key) }
|
.filter { !identityManager.isNostrBlocked(pubkeyHexLowercased: $0.key) }
|
||||||
let people = map
|
let people = map
|
||||||
.map { (pub, seen) in GeoPerson(id: pub, displayName: displayNameForNostrPubkey(pub), lastSeen: seen) }
|
.map { (pub, seen) in GeoPerson(id: pub, displayName: displayNameForNostrPubkey(pub), lastSeen: seen) }
|
||||||
.sorted { $0.lastSeen > $1.lastSeen }
|
.sorted { $0.lastSeen > $1.lastSeen }
|
||||||
@@ -1868,12 +1879,12 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Geohash block helpers
|
// Geohash block helpers
|
||||||
@MainActor
|
@MainActor
|
||||||
func isGeohashUserBlocked(pubkeyHexLowercased: String) -> Bool {
|
func isGeohashUserBlocked(pubkeyHexLowercased: String) -> Bool {
|
||||||
return SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: pubkeyHexLowercased)
|
return identityManager.isNostrBlocked(pubkeyHexLowercased: pubkeyHexLowercased)
|
||||||
}
|
}
|
||||||
@MainActor
|
@MainActor
|
||||||
func blockGeohashUser(pubkeyHexLowercased: String, displayName: String) {
|
func blockGeohashUser(pubkeyHexLowercased: String, displayName: String) {
|
||||||
let hex = pubkeyHexLowercased.lowercased()
|
let hex = pubkeyHexLowercased.lowercased()
|
||||||
SecureIdentityStateManager.shared.setNostrBlocked(hex, isBlocked: true)
|
identityManager.setNostrBlocked(hex, isBlocked: true)
|
||||||
|
|
||||||
// Remove from participants for all geohashes
|
// Remove from participants for all geohashes
|
||||||
for (gh, var map) in geoParticipants {
|
for (gh, var map) in geoParticipants {
|
||||||
@@ -1921,7 +1932,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
@MainActor
|
@MainActor
|
||||||
func unblockGeohashUser(pubkeyHexLowercased: String, displayName: String) {
|
func unblockGeohashUser(pubkeyHexLowercased: String, displayName: String) {
|
||||||
SecureIdentityStateManager.shared.setNostrBlocked(pubkeyHexLowercased, isBlocked: false)
|
identityManager.setNostrBlocked(pubkeyHexLowercased, isBlocked: false)
|
||||||
addSystemMessage("unblocked \(displayName) in geohash chats")
|
addSystemMessage("unblocked \(displayName) in geohash chats")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1970,7 +1981,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
let content = event.content.trimmingCharacters(in: .whitespacesAndNewlines)
|
let content = event.content.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
guard !content.isEmpty else { return }
|
guard !content.isEmpty else { return }
|
||||||
// Respect geohash blocks
|
// Respect geohash blocks
|
||||||
if SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: event.pubkey.lowercased()) { return }
|
if identityManager.isNostrBlocked(pubkeyHexLowercased: event.pubkey.lowercased()) { return }
|
||||||
// Skip self identity for this geohash
|
// Skip self identity for this geohash
|
||||||
if let my = try? NostrIdentityBridge.deriveIdentity(forGeohash: gh), my.publicKeyHex.lowercased() == event.pubkey.lowercased() { return }
|
if let my = try? NostrIdentityBridge.deriveIdentity(forGeohash: gh), my.publicKeyHex.lowercased() == event.pubkey.lowercased() { return }
|
||||||
// Only trigger when there were zero participants in this geohash recently
|
// Only trigger when there were zero participants in this geohash recently
|
||||||
@@ -2122,7 +2133,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Respect geohash blocks
|
// Respect geohash blocks
|
||||||
if SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: recipientHex) {
|
if identityManager.isNostrBlocked(pubkeyHexLowercased: recipientHex) {
|
||||||
if let msgIdx = privateChats[peerID]?.firstIndex(where: { $0.id == messageID }) {
|
if let msgIdx = privateChats[peerID]?.firstIndex(where: { $0.id == messageID }) {
|
||||||
privateChats[peerID]?[msgIdx].deliveryStatus = .failed(reason: "user is blocked")
|
privateChats[peerID]?[msgIdx].deliveryStatus = .failed(reason: "user is blocked")
|
||||||
}
|
}
|
||||||
@@ -2140,7 +2151,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
SecureLogger.debug("GeoDM: local send mid=\(messageID.prefix(8))… to=\(recipientHex.prefix(8))… conv=\(peerID)", category: .session)
|
SecureLogger.debug("GeoDM: local send mid=\(messageID.prefix(8))… to=\(recipientHex.prefix(8))… conv=\(peerID)", category: .session)
|
||||||
let nostrTransport = NostrTransport()
|
let nostrTransport = NostrTransport(keychain: keychain)
|
||||||
nostrTransport.senderPeerID = meshService.myPeerID
|
nostrTransport.senderPeerID = meshService.myPeerID
|
||||||
nostrTransport.sendPrivateMessageGeohash(content: content, toRecipientHex: recipientHex, from: id, messageID: messageID)
|
nostrTransport.sendPrivateMessageGeohash(content: content, toRecipientHex: recipientHex, from: id, messageID: messageID)
|
||||||
if let msgIdx = privateChats[peerID]?.firstIndex(where: { $0.id == messageID }) {
|
if let msgIdx = privateChats[peerID]?.firstIndex(where: { $0.id == messageID }) {
|
||||||
@@ -2798,15 +2809,15 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
meshService.stopServices()
|
meshService.stopServices()
|
||||||
|
|
||||||
// Force save any pending identity changes (verifications, favorites, etc)
|
// Force save any pending identity changes (verifications, favorites, etc)
|
||||||
SecureIdentityStateManager.shared.forceSave()
|
identityManager.forceSave()
|
||||||
|
|
||||||
// Verify identity key is still there
|
// Verify identity key is still there
|
||||||
_ = KeychainManager.shared.verifyIdentityKeyExists()
|
_ = keychain.verifyIdentityKeyExists()
|
||||||
|
|
||||||
// No need to force synchronize here
|
// No need to force synchronize here
|
||||||
|
|
||||||
// Verify identity key after save
|
// Verify identity key after save
|
||||||
_ = KeychainManager.shared.verifyIdentityKeyExists()
|
_ = keychain.verifyIdentityKeyExists()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func appWillTerminate() {
|
@objc private func appWillTerminate() {
|
||||||
@@ -2857,7 +2868,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
for message in messages where message.senderPeerID == peerID && !message.isRelay {
|
for message in messages where message.senderPeerID == peerID && !message.isRelay {
|
||||||
if !sentReadReceipts.contains(message.id) {
|
if !sentReadReceipts.contains(message.id) {
|
||||||
SecureLogger.debug("GeoDM: sending READ for mid=\(message.id.prefix(8))… to=\(recipientHex.prefix(8))…", category: .session)
|
SecureLogger.debug("GeoDM: sending READ for mid=\(message.id.prefix(8))… to=\(recipientHex.prefix(8))…", category: .session)
|
||||||
let nostrTransport = NostrTransport()
|
let nostrTransport = NostrTransport(keychain: keychain)
|
||||||
nostrTransport.senderPeerID = meshService.myPeerID
|
nostrTransport.senderPeerID = meshService.myPeerID
|
||||||
nostrTransport.sendReadReceiptGeohash(message.id, toRecipientHex: recipientHex, from: id)
|
nostrTransport.sendReadReceiptGeohash(message.id, toRecipientHex: recipientHex, from: id)
|
||||||
sentReadReceipts.insert(message.id)
|
sentReadReceipts.insert(message.id)
|
||||||
@@ -3003,7 +3014,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
privateChatManager.unreadMessages.removeAll()
|
privateChatManager.unreadMessages.removeAll()
|
||||||
|
|
||||||
// Delete all keychain data (including Noise and Nostr keys)
|
// Delete all keychain data (including Noise and Nostr keys)
|
||||||
_ = KeychainManager.shared.deleteAllKeychainData()
|
_ = keychain.deleteAllKeychainData()
|
||||||
|
|
||||||
// Clear UserDefaults identity data
|
// Clear UserDefaults identity data
|
||||||
userDefaults.removeObject(forKey: "bitchat.noiseIdentityKey")
|
userDefaults.removeObject(forKey: "bitchat.noiseIdentityKey")
|
||||||
@@ -3019,14 +3030,14 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
// Clear favorites and peer mappings
|
// Clear favorites and peer mappings
|
||||||
// Clear through SecureIdentityStateManager instead of directly
|
// Clear through SecureIdentityStateManager instead of directly
|
||||||
SecureIdentityStateManager.shared.clearAllIdentityData()
|
identityManager.clearAllIdentityData()
|
||||||
peerIDToPublicKeyFingerprint.removeAll()
|
peerIDToPublicKeyFingerprint.removeAll()
|
||||||
|
|
||||||
// Clear persistent favorites from keychain
|
// Clear persistent favorites from keychain
|
||||||
FavoritesPersistenceService.shared.clearAllFavorites()
|
FavoritesPersistenceService.shared.clearAllFavorites()
|
||||||
|
|
||||||
// Clear identity data from secure storage
|
// Clear identity data from secure storage
|
||||||
SecureIdentityStateManager.shared.clearAllIdentityData()
|
identityManager.clearAllIdentityData()
|
||||||
|
|
||||||
// Clear autocomplete state
|
// Clear autocomplete state
|
||||||
autocompleteSuggestions.removeAll()
|
autocompleteSuggestions.removeAll()
|
||||||
@@ -4252,7 +4263,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
// Try to resolve through fingerprint and social identity
|
// Try to resolve through fingerprint and social identity
|
||||||
if let fingerprint = getFingerprint(for: peerID) {
|
if let fingerprint = getFingerprint(for: peerID) {
|
||||||
if let identity = SecureIdentityStateManager.shared.getSocialIdentity(for: fingerprint) {
|
if let identity = identityManager.getSocialIdentity(for: fingerprint) {
|
||||||
// Prefer local petname if set
|
// Prefer local petname if set
|
||||||
if let petname = identity.localPetname {
|
if let petname = identity.localPetname {
|
||||||
return petname
|
return petname
|
||||||
@@ -4284,7 +4295,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
guard let fingerprint = getFingerprint(for: peerID) else { return }
|
guard let fingerprint = getFingerprint(for: peerID) else { return }
|
||||||
|
|
||||||
// Update secure storage with verified status
|
// Update secure storage with verified status
|
||||||
SecureIdentityStateManager.shared.setVerified(fingerprint: fingerprint, verified: true)
|
identityManager.setVerified(fingerprint: fingerprint, verified: true)
|
||||||
|
|
||||||
// Update local set for UI
|
// Update local set for UI
|
||||||
verifiedFingerprints.insert(fingerprint)
|
verifiedFingerprints.insert(fingerprint)
|
||||||
@@ -4296,8 +4307,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
@MainActor
|
@MainActor
|
||||||
func unverifyFingerprint(for peerID: String) {
|
func unverifyFingerprint(for peerID: String) {
|
||||||
guard let fingerprint = getFingerprint(for: peerID) else { return }
|
guard let fingerprint = getFingerprint(for: peerID) else { return }
|
||||||
SecureIdentityStateManager.shared.setVerified(fingerprint: fingerprint, verified: false)
|
identityManager.setVerified(fingerprint: fingerprint, verified: false)
|
||||||
SecureIdentityStateManager.shared.forceSave()
|
identityManager.forceSave()
|
||||||
verifiedFingerprints.remove(fingerprint)
|
verifiedFingerprints.remove(fingerprint)
|
||||||
updateEncryptionStatus(for: peerID)
|
updateEncryptionStatus(for: peerID)
|
||||||
}
|
}
|
||||||
@@ -4305,7 +4316,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
@MainActor
|
@MainActor
|
||||||
func loadVerifiedFingerprints() {
|
func loadVerifiedFingerprints() {
|
||||||
// Load verified fingerprints directly from secure storage
|
// Load verified fingerprints directly from secure storage
|
||||||
verifiedFingerprints = SecureIdentityStateManager.shared.getVerifiedFingerprints()
|
verifiedFingerprints = identityManager.getVerifiedFingerprints()
|
||||||
// Log snapshot for debugging persistence
|
// Log snapshot for debugging persistence
|
||||||
let sample = Array(verifiedFingerprints.prefix(TransportConfig.uiFingerprintSampleCount)).map { $0.prefix(8) }.joined(separator: ", ")
|
let sample = Array(verifiedFingerprints.prefix(TransportConfig.uiFingerprintSampleCount)).map { $0.prefix(8) }.joined(separator: ", ")
|
||||||
SecureLogger.info("🔐 Verified loaded: \(verifiedFingerprints.count) [\(sample)]", category: .security)
|
SecureLogger.info("🔐 Verified loaded: \(verifiedFingerprints.count) [\(sample)]", category: .security)
|
||||||
@@ -4505,8 +4516,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
if let fp = getFingerprint(for: peerID) {
|
if let fp = getFingerprint(for: peerID) {
|
||||||
let short = fp.prefix(8)
|
let short = fp.prefix(8)
|
||||||
SecureLogger.info("🔐 Marking verified fingerprint: \(short)", category: .security)
|
SecureLogger.info("🔐 Marking verified fingerprint: \(short)", category: .security)
|
||||||
SecureIdentityStateManager.shared.setVerified(fingerprint: fp, verified: true)
|
identityManager.setVerified(fingerprint: fp, verified: true)
|
||||||
SecureIdentityStateManager.shared.forceSave()
|
identityManager.forceSave()
|
||||||
verifiedFingerprints.insert(fp)
|
verifiedFingerprints.insert(fp)
|
||||||
let name = unifiedPeerService.getPeer(by: peerID)?.nickname ?? resolveNickname(for: peerID)
|
let name = unifiedPeerService.getPeer(by: peerID)?.nickname ?? resolveNickname(for: peerID)
|
||||||
NotificationService.shared.sendLocalNotification(
|
NotificationService.shared.sendLocalNotification(
|
||||||
@@ -4597,18 +4608,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
isConnected = true
|
isConnected = true
|
||||||
|
|
||||||
// Register ephemeral session with identity manager
|
// Register ephemeral session with identity manager
|
||||||
SecureIdentityStateManager.shared.registerEphemeralSession(peerID: peerID)
|
identityManager.registerEphemeralSession(peerID: peerID, handshakeState: .none)
|
||||||
|
|
||||||
// Check if we favorite this peer and resend notification on reconnect
|
// Intentionally do not resend favorites on reconnect.
|
||||||
// This ensures Nostr key mapping is maintained across reconnections
|
// We only send our npub when a favorite is toggled on, or if our npub changes.
|
||||||
if let peer = unifiedPeerService.getPeer(by: peerID),
|
|
||||||
let favoriteStatus = FavoritesPersistenceService.shared.getFavoriteStatus(for: peer.noisePublicKey),
|
|
||||||
favoriteStatus.isFavorite {
|
|
||||||
// Resend favorite notification with our Nostr key after a short delay
|
|
||||||
try? await Task.sleep(nanoseconds: TransportConfig.uiAsyncMediumSleepNs) // 0.5 seconds
|
|
||||||
meshService.sendFavoriteNotification(to: peerID, isFavorite: true)
|
|
||||||
SecureLogger.debug("📤 Resent favorite notification to reconnected peer \(peerID)", category: .session)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force UI refresh
|
// Force UI refresh
|
||||||
objectWillChange.send()
|
objectWillChange.send()
|
||||||
@@ -4630,7 +4633,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
SecureLogger.debug("👋 Peer disconnected: \(peerID)", category: .session)
|
SecureLogger.debug("👋 Peer disconnected: \(peerID)", category: .session)
|
||||||
|
|
||||||
// Remove ephemeral session from identity manager
|
// Remove ephemeral session from identity manager
|
||||||
SecureIdentityStateManager.shared.removeEphemeralSession(peerID: peerID)
|
identityManager.removeEphemeralSession(peerID: peerID)
|
||||||
|
|
||||||
// If the open PM is tied to this short peer ID, switch UI context to the full Noise key (offline favorite)
|
// 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]
|
var derivedStableKeyHex: String? = shortIDToNoiseKey[peerID]
|
||||||
@@ -4737,7 +4740,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
// Register ephemeral sessions for all connected peers
|
// Register ephemeral sessions for all connected peers
|
||||||
for peerID in peers {
|
for peerID in peers {
|
||||||
SecureIdentityStateManager.shared.registerEphemeralSession(peerID: peerID)
|
self.identityManager.registerEphemeralSession(peerID: peerID, handshakeState: .none)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schedule UI refresh to ensure offline favorites are shown
|
// Schedule UI refresh to ensure offline favorites are shown
|
||||||
@@ -4889,7 +4892,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isFavorite(fingerprint: String) -> Bool {
|
func isFavorite(fingerprint: String) -> Bool {
|
||||||
return SecureIdentityStateManager.shared.isFavorite(fingerprint: fingerprint)
|
return identityManager.isFavorite(fingerprint: fingerprint)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Delivery Tracking
|
// MARK: - Delivery Tracking
|
||||||
@@ -4957,6 +4960,26 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
messages.append(systemMessage)
|
messages.append(systemMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add a system message to the mesh timeline only (never geohash).
|
||||||
|
/// If mesh is currently active, also append to the visible `messages`.
|
||||||
|
@MainActor
|
||||||
|
private func addMeshOnlySystemMessage(_ content: String) {
|
||||||
|
let systemMessage = BitchatMessage(
|
||||||
|
sender: "system",
|
||||||
|
content: content,
|
||||||
|
timestamp: Date(),
|
||||||
|
isRelay: false
|
||||||
|
)
|
||||||
|
// Persist to mesh timeline
|
||||||
|
meshTimeline.append(systemMessage)
|
||||||
|
trimMeshTimelineIfNeeded()
|
||||||
|
// Only show inline if mesh is the active channel
|
||||||
|
if case .mesh = activeChannel {
|
||||||
|
messages.append(systemMessage)
|
||||||
|
}
|
||||||
|
objectWillChange.send()
|
||||||
|
}
|
||||||
|
|
||||||
/// Public helper to add a system message to the public chat timeline.
|
/// Public helper to add a system message to the public chat timeline.
|
||||||
/// Also persists the message into the active channel's backing store so it survives timeline rebinds.
|
/// Also persists the message into the active channel's backing store so it survives timeline rebinds.
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -5199,7 +5222,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
messageRouter.sendDeliveryAck(messageId, to: key.hexEncodedString())
|
messageRouter.sendDeliveryAck(messageId, to: key.hexEncodedString())
|
||||||
} else if let id = try? NostrIdentityBridge.getCurrentNostrIdentity() {
|
} else if let id = try? NostrIdentityBridge.getCurrentNostrIdentity() {
|
||||||
// Fallback: no Noise mapping yet — send directly to sender's Nostr pubkey
|
// Fallback: no Noise mapping yet — send directly to sender's Nostr pubkey
|
||||||
let nt = NostrTransport()
|
let nt = NostrTransport(keychain: keychain)
|
||||||
nt.senderPeerID = meshService.myPeerID
|
nt.senderPeerID = meshService.myPeerID
|
||||||
nt.sendDeliveryAckGeohash(for: messageId, toRecipientHex: senderPubkey, from: id)
|
nt.sendDeliveryAckGeohash(for: messageId, toRecipientHex: senderPubkey, from: id)
|
||||||
SecureLogger.debug("Sent DELIVERED ack directly to Nostr pub=\(senderPubkey.prefix(8))… for mid=\(messageId.prefix(8))…", category: .session)
|
SecureLogger.debug("Sent DELIVERED ack directly to Nostr pub=\(senderPubkey.prefix(8))… for mid=\(messageId.prefix(8))…", category: .session)
|
||||||
@@ -5221,7 +5244,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
messageRouter.sendReadReceipt(receipt, to: key.hexEncodedString())
|
messageRouter.sendReadReceipt(receipt, to: key.hexEncodedString())
|
||||||
sentReadReceipts.insert(messageId)
|
sentReadReceipts.insert(messageId)
|
||||||
} else if let id = try? NostrIdentityBridge.getCurrentNostrIdentity() {
|
} else if let id = try? NostrIdentityBridge.getCurrentNostrIdentity() {
|
||||||
let nt = NostrTransport()
|
let nt = NostrTransport(keychain: keychain)
|
||||||
nt.senderPeerID = meshService.myPeerID
|
nt.senderPeerID = meshService.myPeerID
|
||||||
nt.sendReadReceiptGeohash(messageId, toRecipientHex: senderPubkey, from: id)
|
nt.sendReadReceiptGeohash(messageId, toRecipientHex: senderPubkey, from: id)
|
||||||
sentReadReceipts.insert(messageId)
|
sentReadReceipts.insert(messageId)
|
||||||
@@ -5384,8 +5407,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
SecureLogger.warning("⚠️ Cannot get Noise key for peer \(peerID)", category: .session)
|
SecureLogger.warning("⚠️ Cannot get Noise key for peer \(peerID)", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Determine prior state to avoid duplicate system messages on repeated notifications
|
||||||
|
let prior = FavoritesPersistenceService.shared.getFavoriteStatus(for: finalNoiseKey)?.theyFavoritedUs ?? false
|
||||||
|
|
||||||
// Update the favorite relationship
|
// Update the favorite relationship (idempotent storage)
|
||||||
FavoritesPersistenceService.shared.updatePeerFavoritedUs(
|
FavoritesPersistenceService.shared.updatePeerFavoritedUs(
|
||||||
peerNoisePublicKey: finalNoiseKey,
|
peerNoisePublicKey: finalNoiseKey,
|
||||||
favorited: isFavorite,
|
favorited: isFavorite,
|
||||||
@@ -5393,14 +5418,16 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
peerNostrPublicKey: nostrPubkey
|
peerNostrPublicKey: nostrPubkey
|
||||||
)
|
)
|
||||||
|
|
||||||
// If they favorited us and provided their Nostr key, ensure it's stored
|
// If they favorited us and provided their Nostr key, ensure it's stored (log only)
|
||||||
if isFavorite && nostrPubkey != nil {
|
if isFavorite && nostrPubkey != nil {
|
||||||
SecureLogger.info("💾 Storing Nostr key association for \(senderNickname): \(nostrPubkey!.prefix(16))...", category: .session)
|
SecureLogger.info("💾 Storing Nostr key association for \(senderNickname): \(nostrPubkey!.prefix(16))...", category: .session)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show system message
|
// Only show a system message when the state changes, and only in mesh
|
||||||
|
if prior != isFavorite {
|
||||||
let action = isFavorite ? "favorited" : "unfavorited"
|
let action = isFavorite ? "favorited" : "unfavorited"
|
||||||
addSystemMessage("\(senderNickname) \(action) you")
|
addMeshOnlySystemMessage("\(senderNickname) \(action) you")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -5596,7 +5623,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Check geohash (Nostr) blocks using mapping to full pubkey
|
// Check geohash (Nostr) blocks using mapping to full pubkey
|
||||||
if peerID.hasPrefix("nostr") {
|
if peerID.hasPrefix("nostr") {
|
||||||
if let full = nostrKeyMapping[peerID]?.lowercased() {
|
if let full = nostrKeyMapping[peerID]?.lowercased() {
|
||||||
if SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: full) { return true }
|
if identityManager.isNostrBlocked(pubkeyHexLowercased: full) { return true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -1247,15 +1247,15 @@ struct ContentView: View {
|
|||||||
!fav.peerNickname.isEmpty { return fav.peerNickname }
|
!fav.peerNickname.isEmpty { return fav.peerNickname }
|
||||||
// Fallback: resolve from persisted social identity via fingerprint mapping
|
// Fallback: resolve from persisted social identity via fingerprint mapping
|
||||||
if headerPeerID.count == 16 {
|
if headerPeerID.count == 16 {
|
||||||
let candidates = SecureIdentityStateManager.shared.getCryptoIdentitiesByPeerIDPrefix(headerPeerID)
|
let candidates = viewModel.identityManager.getCryptoIdentitiesByPeerIDPrefix(headerPeerID)
|
||||||
if let id = candidates.first,
|
if let id = candidates.first,
|
||||||
let social = SecureIdentityStateManager.shared.getSocialIdentity(for: id.fingerprint) {
|
let social = viewModel.identityManager.getSocialIdentity(for: id.fingerprint) {
|
||||||
if let pet = social.localPetname, !pet.isEmpty { return pet }
|
if let pet = social.localPetname, !pet.isEmpty { return pet }
|
||||||
if !social.claimedNickname.isEmpty { return social.claimedNickname }
|
if !social.claimedNickname.isEmpty { return social.claimedNickname }
|
||||||
}
|
}
|
||||||
} else if headerPeerID.count == 64, let keyData = Data(hexString: headerPeerID) {
|
} else if headerPeerID.count == 64, let keyData = Data(hexString: headerPeerID) {
|
||||||
let fp = keyData.sha256Fingerprint()
|
let fp = keyData.sha256Fingerprint()
|
||||||
if let social = SecureIdentityStateManager.shared.getSocialIdentity(for: fp) {
|
if let social = viewModel.identityManager.getSocialIdentity(for: fp) {
|
||||||
if let pet = social.localPetname, !pet.isEmpty { return pet }
|
if let pet = social.localPetname, !pet.isEmpty { return pet }
|
||||||
if !social.claimedNickname.isEmpty { return social.claimedNickname }
|
if !social.claimedNickname.isEmpty { return social.claimedNickname }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ struct FingerprintView: View {
|
|||||||
if peerID.count == 64, let data = Data(hexString: peerID) {
|
if peerID.count == 64, let data = Data(hexString: peerID) {
|
||||||
if let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: data), !fav.peerNickname.isEmpty { return fav.peerNickname }
|
if let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: data), !fav.peerNickname.isEmpty { return fav.peerNickname }
|
||||||
let fp = data.sha256Fingerprint()
|
let fp = data.sha256Fingerprint()
|
||||||
if let social = SecureIdentityStateManager.shared.getSocialIdentity(for: fp) {
|
if let social = viewModel.identityManager.getSocialIdentity(for: fp) {
|
||||||
if let pet = social.localPetname, !pet.isEmpty { return pet }
|
if let pet = social.localPetname, !pet.isEmpty { return pet }
|
||||||
if !social.claimedNickname.isEmpty { return social.claimedNickname }
|
if !social.claimedNickname.isEmpty { return social.claimedNickname }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,22 @@ import XCTest
|
|||||||
|
|
||||||
final class CommandProcessorTests: XCTestCase {
|
final class CommandProcessorTests: XCTestCase {
|
||||||
|
|
||||||
|
var identityManager: MockIdentityManager!
|
||||||
|
|
||||||
|
override func setUp() {
|
||||||
|
super.setUp()
|
||||||
|
// Provide a minimal identity manager for commands that query identity/block lists
|
||||||
|
identityManager = MockIdentityManager(MockKeychain())
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tearDown() {
|
||||||
|
identityManager = nil
|
||||||
|
super.tearDown()
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func test_slap_notFoundGrammar() {
|
func test_slap_notFoundGrammar() {
|
||||||
let processor = CommandProcessor(chatViewModel: nil, meshService: nil)
|
let processor = CommandProcessor(chatViewModel: nil, meshService: nil, identityManager: identityManager)
|
||||||
let result = processor.process("/slap @system")
|
let result = processor.process("/slap @system")
|
||||||
switch result {
|
switch result {
|
||||||
case .error(let message):
|
case .error(let message):
|
||||||
@@ -17,7 +30,7 @@ final class CommandProcessorTests: XCTestCase {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func test_hug_notFoundGrammar() {
|
func test_hug_notFoundGrammar() {
|
||||||
let processor = CommandProcessor(chatViewModel: nil, meshService: nil)
|
let processor = CommandProcessor(chatViewModel: nil, meshService: nil, identityManager: identityManager)
|
||||||
let result = processor.process("/hug @system")
|
let result = processor.process("/hug @system")
|
||||||
switch result {
|
switch result {
|
||||||
case .error(let message):
|
case .error(let message):
|
||||||
@@ -29,7 +42,7 @@ final class CommandProcessorTests: XCTestCase {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func test_slap_usageMessage() {
|
func test_slap_usageMessage() {
|
||||||
let processor = CommandProcessor(chatViewModel: nil, meshService: nil)
|
let processor = CommandProcessor(chatViewModel: nil, meshService: nil, identityManager: identityManager)
|
||||||
let result = processor.process("/slap")
|
let result = processor.process("/slap")
|
||||||
switch result {
|
switch result {
|
||||||
case .error(let message):
|
case .error(let message):
|
||||||
@@ -39,4 +52,3 @@ final class CommandProcessorTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ final class PrivateChatE2ETests: XCTestCase {
|
|||||||
var bob: MockBluetoothMeshService!
|
var bob: MockBluetoothMeshService!
|
||||||
var charlie: MockBluetoothMeshService!
|
var charlie: MockBluetoothMeshService!
|
||||||
|
|
||||||
|
private var mockKeychain: MockKeychain!
|
||||||
|
|
||||||
override func setUp() {
|
override func setUp() {
|
||||||
super.setUp()
|
super.setUp()
|
||||||
MockBLEService.resetTestBus()
|
MockBLEService.resetTestBus()
|
||||||
@@ -24,6 +26,7 @@ final class PrivateChatE2ETests: XCTestCase {
|
|||||||
alice = createMockService(peerID: TestConstants.testPeerID1, nickname: TestConstants.testNickname1)
|
alice = createMockService(peerID: TestConstants.testPeerID1, nickname: TestConstants.testNickname1)
|
||||||
bob = createMockService(peerID: TestConstants.testPeerID2, nickname: TestConstants.testNickname2)
|
bob = createMockService(peerID: TestConstants.testPeerID2, nickname: TestConstants.testNickname2)
|
||||||
charlie = createMockService(peerID: TestConstants.testPeerID3, nickname: TestConstants.testNickname3)
|
charlie = createMockService(peerID: TestConstants.testPeerID3, nickname: TestConstants.testNickname3)
|
||||||
|
mockKeychain = MockKeychain()
|
||||||
|
|
||||||
// Delivery tracking is now handled internally by BLEService
|
// Delivery tracking is now handled internally by BLEService
|
||||||
}
|
}
|
||||||
@@ -32,6 +35,7 @@ final class PrivateChatE2ETests: XCTestCase {
|
|||||||
alice = nil
|
alice = nil
|
||||||
bob = nil
|
bob = nil
|
||||||
charlie = nil
|
charlie = nil
|
||||||
|
mockKeychain = nil
|
||||||
super.tearDown()
|
super.tearDown()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,8 +120,8 @@ final class PrivateChatE2ETests: XCTestCase {
|
|||||||
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
|
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
|
||||||
let bobKey = Curve25519.KeyAgreement.PrivateKey()
|
let bobKey = Curve25519.KeyAgreement.PrivateKey()
|
||||||
|
|
||||||
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey)
|
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain)
|
||||||
let bobManager = NoiseSessionManager(localStaticKey: bobKey)
|
let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain)
|
||||||
|
|
||||||
// Establish encrypted session
|
// Establish encrypted session
|
||||||
do {
|
do {
|
||||||
|
|||||||
@@ -11,6 +11,21 @@ import XCTest
|
|||||||
|
|
||||||
final class FragmentationTests: XCTestCase {
|
final class FragmentationTests: XCTestCase {
|
||||||
|
|
||||||
|
private var mockKeychain: MockKeychain!
|
||||||
|
private var mockIdentityManager: MockIdentityManager!
|
||||||
|
|
||||||
|
override func setUp() {
|
||||||
|
super.setUp()
|
||||||
|
mockKeychain = MockKeychain()
|
||||||
|
mockIdentityManager = MockIdentityManager(mockKeychain)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tearDown() {
|
||||||
|
mockKeychain = nil
|
||||||
|
mockIdentityManager = nil
|
||||||
|
super.tearDown()
|
||||||
|
}
|
||||||
|
|
||||||
private final class CaptureDelegate: BitchatDelegate {
|
private final class CaptureDelegate: BitchatDelegate {
|
||||||
var publicMessages: [(peerID: String, nickname: String, content: String)] = []
|
var publicMessages: [(peerID: String, nickname: String, content: String)] = []
|
||||||
func didReceiveMessage(_ message: BitchatMessage) {}
|
func didReceiveMessage(_ message: BitchatMessage) {}
|
||||||
@@ -75,7 +90,7 @@ final class FragmentationTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func test_reassembly_from_fragments_delivers_public_message() {
|
func test_reassembly_from_fragments_delivers_public_message() {
|
||||||
let ble = BLEService()
|
let ble = BLEService(keychain: mockKeychain, identityManager: mockIdentityManager)
|
||||||
let capture = CaptureDelegate()
|
let capture = CaptureDelegate()
|
||||||
ble.delegate = capture
|
ble.delegate = capture
|
||||||
|
|
||||||
@@ -106,7 +121,7 @@ final class FragmentationTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func test_duplicate_fragment_does_not_break_reassembly() {
|
func test_duplicate_fragment_does_not_break_reassembly() {
|
||||||
let ble = BLEService()
|
let ble = BLEService(keychain: mockKeychain, identityManager: mockIdentityManager)
|
||||||
let capture = CaptureDelegate()
|
let capture = CaptureDelegate()
|
||||||
ble.delegate = capture
|
ble.delegate = capture
|
||||||
|
|
||||||
@@ -132,7 +147,7 @@ final class FragmentationTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func test_invalid_fragment_header_is_ignored() {
|
func test_invalid_fragment_header_is_ignored() {
|
||||||
let ble = BLEService()
|
let ble = BLEService(keychain: mockKeychain, identityManager: mockIdentityManager)
|
||||||
let capture = CaptureDelegate()
|
let capture = CaptureDelegate()
|
||||||
ble.delegate = capture
|
ble.delegate = capture
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ final class IntegrationTests: XCTestCase {
|
|||||||
|
|
||||||
var nodes: [String: MockBluetoothMeshService] = [:]
|
var nodes: [String: MockBluetoothMeshService] = [:]
|
||||||
var noiseManagers: [String: NoiseSessionManager] = [:]
|
var noiseManagers: [String: NoiseSessionManager] = [:]
|
||||||
|
private var mockKeychain: MockKeychain!
|
||||||
|
|
||||||
override func setUp() {
|
override func setUp() {
|
||||||
super.setUp()
|
super.setUp()
|
||||||
@@ -21,6 +22,7 @@ final class IntegrationTests: XCTestCase {
|
|||||||
// broadcast propagation across a larger mesh. Integration-only.
|
// broadcast propagation across a larger mesh. Integration-only.
|
||||||
MockBLEService.resetTestBus()
|
MockBLEService.resetTestBus()
|
||||||
MockBLEService.autoFloodEnabled = true
|
MockBLEService.autoFloodEnabled = true
|
||||||
|
mockKeychain = MockKeychain()
|
||||||
|
|
||||||
// Create a network of nodes
|
// Create a network of nodes
|
||||||
createNode("Alice", peerID: TestConstants.testPeerID1)
|
createNode("Alice", peerID: TestConstants.testPeerID1)
|
||||||
@@ -34,6 +36,7 @@ final class IntegrationTests: XCTestCase {
|
|||||||
MockBLEService.autoFloodEnabled = false
|
MockBLEService.autoFloodEnabled = false
|
||||||
nodes.removeAll()
|
nodes.removeAll()
|
||||||
noiseManagers.removeAll()
|
noiseManagers.removeAll()
|
||||||
|
mockKeychain = nil
|
||||||
super.tearDown()
|
super.tearDown()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,7 +310,7 @@ final class IntegrationTests: XCTestCase {
|
|||||||
|
|
||||||
// Simulate Bob restart by recreating his Noise manager
|
// Simulate Bob restart by recreating his Noise manager
|
||||||
let bobKey = Curve25519.KeyAgreement.PrivateKey()
|
let bobKey = Curve25519.KeyAgreement.PrivateKey()
|
||||||
noiseManagers["Bob"] = NoiseSessionManager(localStaticKey: bobKey)
|
noiseManagers["Bob"] = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain)
|
||||||
|
|
||||||
// Re-establish Noise handshake explicitly via managers
|
// Re-establish Noise handshake explicitly via managers
|
||||||
do {
|
do {
|
||||||
@@ -593,7 +596,7 @@ final class IntegrationTests: XCTestCase {
|
|||||||
|
|
||||||
// Create Noise manager
|
// Create Noise manager
|
||||||
let key = Curve25519.KeyAgreement.PrivateKey()
|
let key = Curve25519.KeyAgreement.PrivateKey()
|
||||||
noiseManagers[name] = NoiseSessionManager(localStaticKey: key)
|
noiseManagers[name] = NoiseSessionManager(localStaticKey: key, keychain: mockKeychain)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func connect(_ node1: String, _ node2: String) {
|
private func connect(_ node1: String, _ node2: String) {
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ final class MockBLEService: NSObject {
|
|||||||
var myPeerID: String = "MOCK1234"
|
var myPeerID: String = "MOCK1234"
|
||||||
var myNickname: String = "MockUser"
|
var myNickname: String = "MockUser"
|
||||||
|
|
||||||
|
private let mockKeychain = MockKeychain()
|
||||||
|
|
||||||
// Test-specific properties
|
// Test-specific properties
|
||||||
var sentMessages: [(message: BitchatMessage, packet: BitchatPacket)] = []
|
var sentMessages: [(message: BitchatMessage, packet: BitchatPacket)] = []
|
||||||
var sentPackets: [BitchatPacket] = []
|
var sentPackets: [BitchatPacket] = []
|
||||||
@@ -272,7 +274,7 @@ final class MockBLEService: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getNoiseService() -> NoiseEncryptionService {
|
func getNoiseService() -> NoiseEncryptionService {
|
||||||
return NoiseEncryptionService()
|
return NoiseEncryptionService(keychain: mockKeychain)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFingerprint(for peerID: String) -> String? {
|
func getFingerprint(for peerID: String) -> String? {
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
//
|
||||||
|
// MockIdentityManager.swift
|
||||||
|
// bitchat
|
||||||
|
//
|
||||||
|
// This is free and unencumbered software released into the public domain.
|
||||||
|
// For more information, see <https://unlicense.org>
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
@testable import bitchat
|
||||||
|
|
||||||
|
final class MockIdentityManager: SecureIdentityStateManagerProtocol {
|
||||||
|
private let keychain: KeychainManagerProtocol
|
||||||
|
|
||||||
|
init(_ keychain: KeychainManagerProtocol) {
|
||||||
|
self.keychain = keychain
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadIdentityCache() {}
|
||||||
|
|
||||||
|
func saveIdentityCache() {}
|
||||||
|
|
||||||
|
func forceSave() {}
|
||||||
|
|
||||||
|
func getSocialIdentity(for fingerprint: String) -> SocialIdentity? {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func upsertCryptographicIdentity(fingerprint: String, noisePublicKey: Data, signingPublicKey: Data?, claimedNickname: String?) {}
|
||||||
|
|
||||||
|
func getCryptoIdentitiesByPeerIDPrefix(_ peerID: String) -> [CryptographicIdentity] {
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateSocialIdentity(_ identity: SocialIdentity) {}
|
||||||
|
|
||||||
|
func getFavorites() -> Set<String> {
|
||||||
|
Set()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setFavorite(_ fingerprint: String, isFavorite: Bool) {}
|
||||||
|
|
||||||
|
func isFavorite(fingerprint: String) -> Bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isBlocked(fingerprint: String) -> Bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
func setBlocked(_ fingerprint: String, isBlocked: Bool) {}
|
||||||
|
|
||||||
|
func isNostrBlocked(pubkeyHexLowercased: String) -> Bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
func setNostrBlocked(_ pubkeyHexLowercased: String, isBlocked: Bool) {}
|
||||||
|
|
||||||
|
func getBlockedNostrPubkeys() -> Set<String> {
|
||||||
|
Set()
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerEphemeralSession(peerID: String, handshakeState: HandshakeState) {}
|
||||||
|
|
||||||
|
func updateHandshakeState(peerID: String, state: HandshakeState) {}
|
||||||
|
|
||||||
|
func clearAllIdentityData() {}
|
||||||
|
|
||||||
|
func removeEphemeralSession(peerID: String) {}
|
||||||
|
|
||||||
|
func setVerified(fingerprint: String, verified: Bool) {}
|
||||||
|
|
||||||
|
func isVerified(fingerprint: String) -> Bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
func getVerifiedFingerprints() -> Set<String> {
|
||||||
|
Set()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
//
|
||||||
|
// MockKeychain.swift
|
||||||
|
// bitchat
|
||||||
|
//
|
||||||
|
// This is free and unencumbered software released into the public domain.
|
||||||
|
// For more information, see <https://unlicense.org>
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
@testable import bitchat
|
||||||
|
|
||||||
|
final class MockKeychain: KeychainManagerProtocol {
|
||||||
|
private var storage: [String: Data] = [:]
|
||||||
|
|
||||||
|
func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool {
|
||||||
|
storage[key] = keyData
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIdentityKey(forKey key: String) -> Data? {
|
||||||
|
storage[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteIdentityKey(forKey key: String) -> Bool {
|
||||||
|
storage.removeValue(forKey: key)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteAllKeychainData() -> Bool {
|
||||||
|
storage.removeAll()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func secureClear(_ data: inout Data) {
|
||||||
|
//
|
||||||
|
data = Data()
|
||||||
|
}
|
||||||
|
|
||||||
|
func secureClear(_ string: inout String) {
|
||||||
|
string = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyIdentityKeyExists() -> Bool {
|
||||||
|
storage["identity_noiseStaticKey"] != nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,16 +16,19 @@ final class NoiseProtocolTests: XCTestCase {
|
|||||||
var bobKey: Curve25519.KeyAgreement.PrivateKey!
|
var bobKey: Curve25519.KeyAgreement.PrivateKey!
|
||||||
var aliceSession: NoiseSession!
|
var aliceSession: NoiseSession!
|
||||||
var bobSession: NoiseSession!
|
var bobSession: NoiseSession!
|
||||||
|
private var mockKeychain: MockKeychain!
|
||||||
|
|
||||||
override func setUp() {
|
override func setUp() {
|
||||||
super.setUp()
|
super.setUp()
|
||||||
aliceKey = Curve25519.KeyAgreement.PrivateKey()
|
aliceKey = Curve25519.KeyAgreement.PrivateKey()
|
||||||
bobKey = Curve25519.KeyAgreement.PrivateKey()
|
bobKey = Curve25519.KeyAgreement.PrivateKey()
|
||||||
|
mockKeychain = MockKeychain()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tearDown() {
|
override func tearDown() {
|
||||||
aliceSession = nil
|
aliceSession = nil
|
||||||
bobSession = nil
|
bobSession = nil
|
||||||
|
mockKeychain = nil
|
||||||
super.tearDown()
|
super.tearDown()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,12 +39,14 @@ final class NoiseProtocolTests: XCTestCase {
|
|||||||
aliceSession = NoiseSession(
|
aliceSession = NoiseSession(
|
||||||
peerID: TestConstants.testPeerID2,
|
peerID: TestConstants.testPeerID2,
|
||||||
role: .initiator,
|
role: .initiator,
|
||||||
|
keychain: mockKeychain,
|
||||||
localStaticKey: aliceKey
|
localStaticKey: aliceKey
|
||||||
)
|
)
|
||||||
|
|
||||||
bobSession = NoiseSession(
|
bobSession = NoiseSession(
|
||||||
peerID: TestConstants.testPeerID1,
|
peerID: TestConstants.testPeerID1,
|
||||||
role: .responder,
|
role: .responder,
|
||||||
|
keychain: mockKeychain,
|
||||||
localStaticKey: bobKey
|
localStaticKey: bobKey
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -80,6 +85,7 @@ final class NoiseProtocolTests: XCTestCase {
|
|||||||
aliceSession = NoiseSession(
|
aliceSession = NoiseSession(
|
||||||
peerID: TestConstants.testPeerID2,
|
peerID: TestConstants.testPeerID2,
|
||||||
role: .initiator,
|
role: .initiator,
|
||||||
|
keychain: mockKeychain,
|
||||||
localStaticKey: aliceKey
|
localStaticKey: aliceKey
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -144,6 +150,7 @@ final class NoiseProtocolTests: XCTestCase {
|
|||||||
aliceSession = NoiseSession(
|
aliceSession = NoiseSession(
|
||||||
peerID: TestConstants.testPeerID2,
|
peerID: TestConstants.testPeerID2,
|
||||||
role: .initiator,
|
role: .initiator,
|
||||||
|
keychain: mockKeychain,
|
||||||
localStaticKey: aliceKey
|
localStaticKey: aliceKey
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -157,7 +164,7 @@ final class NoiseProtocolTests: XCTestCase {
|
|||||||
// MARK: - Session Manager Tests
|
// MARK: - Session Manager Tests
|
||||||
|
|
||||||
func testSessionManagerBasicOperations() throws {
|
func testSessionManagerBasicOperations() throws {
|
||||||
let manager = NoiseSessionManager(localStaticKey: aliceKey)
|
let manager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain)
|
||||||
|
|
||||||
// Create session
|
// Create session
|
||||||
let session = manager.createSession(for: TestConstants.testPeerID2, role: .initiator)
|
let session = manager.createSession(for: TestConstants.testPeerID2, role: .initiator)
|
||||||
@@ -174,7 +181,7 @@ final class NoiseProtocolTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testSessionManagerHandshakeInitiation() throws {
|
func testSessionManagerHandshakeInitiation() throws {
|
||||||
let manager = NoiseSessionManager(localStaticKey: aliceKey)
|
let manager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain)
|
||||||
|
|
||||||
// Initiate handshake
|
// Initiate handshake
|
||||||
let handshakeData = try manager.initiateHandshake(with: TestConstants.testPeerID2)
|
let handshakeData = try manager.initiateHandshake(with: TestConstants.testPeerID2)
|
||||||
@@ -187,8 +194,8 @@ final class NoiseProtocolTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testSessionManagerIncomingHandshake() throws {
|
func testSessionManagerIncomingHandshake() throws {
|
||||||
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey)
|
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain)
|
||||||
let bobManager = NoiseSessionManager(localStaticKey: bobKey)
|
let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain)
|
||||||
|
|
||||||
// Alice initiates
|
// Alice initiates
|
||||||
let message1 = try aliceManager.initiateHandshake(with: TestConstants.testPeerID2)
|
let message1 = try aliceManager.initiateHandshake(with: TestConstants.testPeerID2)
|
||||||
@@ -211,8 +218,8 @@ final class NoiseProtocolTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testSessionManagerEncryptionDecryption() throws {
|
func testSessionManagerEncryptionDecryption() throws {
|
||||||
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey)
|
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain)
|
||||||
let bobManager = NoiseSessionManager(localStaticKey: bobKey)
|
let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain)
|
||||||
|
|
||||||
// Establish sessions
|
// Establish sessions
|
||||||
try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager)
|
try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager)
|
||||||
@@ -256,11 +263,11 @@ final class NoiseProtocolTests: XCTestCase {
|
|||||||
|
|
||||||
func testSessionIsolation() throws {
|
func testSessionIsolation() throws {
|
||||||
// Create two separate session pairs
|
// Create two separate session pairs
|
||||||
let aliceSession1 = NoiseSession(peerID: "peer1", role: .initiator, localStaticKey: aliceKey)
|
let aliceSession1 = NoiseSession(peerID: "peer1", role: .initiator, keychain: mockKeychain, localStaticKey: aliceKey)
|
||||||
let bobSession1 = NoiseSession(peerID: "alice1", role: .responder, localStaticKey: bobKey)
|
let bobSession1 = NoiseSession(peerID: "alice1", role: .responder, keychain: mockKeychain, localStaticKey: bobKey)
|
||||||
|
|
||||||
let aliceSession2 = NoiseSession(peerID: "peer2", role: .initiator, localStaticKey: aliceKey)
|
let aliceSession2 = NoiseSession(peerID: "peer2", role: .initiator, keychain: mockKeychain, localStaticKey: aliceKey)
|
||||||
let bobSession2 = NoiseSession(peerID: "alice2", role: .responder, localStaticKey: bobKey)
|
let bobSession2 = NoiseSession(peerID: "alice2", role: .responder, keychain: mockKeychain, localStaticKey: bobKey)
|
||||||
|
|
||||||
// Establish both pairs
|
// Establish both pairs
|
||||||
try performHandshake(initiator: aliceSession1, responder: bobSession1)
|
try performHandshake(initiator: aliceSession1, responder: bobSession1)
|
||||||
@@ -282,8 +289,8 @@ final class NoiseProtocolTests: XCTestCase {
|
|||||||
|
|
||||||
func testPeerRestartDetection() throws {
|
func testPeerRestartDetection() throws {
|
||||||
// Establish initial sessions
|
// Establish initial sessions
|
||||||
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey)
|
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain)
|
||||||
let bobManager = NoiseSessionManager(localStaticKey: bobKey)
|
let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain)
|
||||||
|
|
||||||
try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager)
|
try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager)
|
||||||
|
|
||||||
@@ -295,7 +302,7 @@ final class NoiseProtocolTests: XCTestCase {
|
|||||||
_ = try aliceManager.decrypt(message2, from: TestConstants.testPeerID2)
|
_ = try aliceManager.decrypt(message2, from: TestConstants.testPeerID2)
|
||||||
|
|
||||||
// Simulate Bob restart by creating new manager with same key
|
// Simulate Bob restart by creating new manager with same key
|
||||||
let bobManagerRestarted = NoiseSessionManager(localStaticKey: bobKey)
|
let bobManagerRestarted = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain)
|
||||||
|
|
||||||
// Bob initiates new handshake after restart
|
// Bob initiates new handshake after restart
|
||||||
let newHandshake1 = try bobManagerRestarted.initiateHandshake(with: TestConstants.testPeerID1)
|
let newHandshake1 = try bobManagerRestarted.initiateHandshake(with: TestConstants.testPeerID1)
|
||||||
@@ -318,8 +325,8 @@ final class NoiseProtocolTests: XCTestCase {
|
|||||||
|
|
||||||
func testNonceDesynchronizationRecovery() throws {
|
func testNonceDesynchronizationRecovery() throws {
|
||||||
// Create two sessions
|
// Create two sessions
|
||||||
aliceSession = NoiseSession(peerID: TestConstants.testPeerID2, role: .initiator, localStaticKey: aliceKey)
|
aliceSession = NoiseSession(peerID: TestConstants.testPeerID2, role: .initiator, keychain: mockKeychain, localStaticKey: aliceKey)
|
||||||
bobSession = NoiseSession(peerID: TestConstants.testPeerID1, role: .responder, localStaticKey: bobKey)
|
bobSession = NoiseSession(peerID: TestConstants.testPeerID1, role: .responder, keychain: mockKeychain, localStaticKey: bobKey)
|
||||||
|
|
||||||
// Establish sessions
|
// Establish sessions
|
||||||
try performHandshake(initiator: aliceSession, responder: bobSession)
|
try performHandshake(initiator: aliceSession, responder: bobSession)
|
||||||
@@ -342,8 +349,8 @@ final class NoiseProtocolTests: XCTestCase {
|
|||||||
|
|
||||||
func testConcurrentEncryption() throws {
|
func testConcurrentEncryption() throws {
|
||||||
// Test thread safety of encryption operations
|
// Test thread safety of encryption operations
|
||||||
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey)
|
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain)
|
||||||
let bobManager = NoiseSessionManager(localStaticKey: bobKey)
|
let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain)
|
||||||
|
|
||||||
try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager)
|
try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager)
|
||||||
|
|
||||||
@@ -380,8 +387,8 @@ final class NoiseProtocolTests: XCTestCase {
|
|||||||
|
|
||||||
func testSessionStaleDetection() throws {
|
func testSessionStaleDetection() throws {
|
||||||
// Test that sessions are properly marked as stale
|
// Test that sessions are properly marked as stale
|
||||||
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey)
|
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain)
|
||||||
let bobManager = NoiseSessionManager(localStaticKey: bobKey)
|
let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain)
|
||||||
|
|
||||||
try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager)
|
try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager)
|
||||||
|
|
||||||
@@ -394,8 +401,8 @@ final class NoiseProtocolTests: XCTestCase {
|
|||||||
|
|
||||||
func testHandshakeAfterDecryptionFailure() throws {
|
func testHandshakeAfterDecryptionFailure() throws {
|
||||||
// Test that handshake is properly initiated after decryption failure
|
// Test that handshake is properly initiated after decryption failure
|
||||||
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey)
|
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain)
|
||||||
let bobManager = NoiseSessionManager(localStaticKey: bobKey)
|
let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain)
|
||||||
|
|
||||||
// Establish sessions
|
// Establish sessions
|
||||||
try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager)
|
try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager)
|
||||||
@@ -413,8 +420,8 @@ final class NoiseProtocolTests: XCTestCase {
|
|||||||
|
|
||||||
func testHandshakeAlwaysAcceptedWithExistingSession() throws {
|
func testHandshakeAlwaysAcceptedWithExistingSession() throws {
|
||||||
// Test that handshake is always accepted even with existing valid session
|
// Test that handshake is always accepted even with existing valid session
|
||||||
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey)
|
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain)
|
||||||
let bobManager = NoiseSessionManager(localStaticKey: bobKey)
|
let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain)
|
||||||
|
|
||||||
// Establish sessions
|
// Establish sessions
|
||||||
try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager)
|
try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager)
|
||||||
@@ -453,8 +460,8 @@ final class NoiseProtocolTests: XCTestCase {
|
|||||||
|
|
||||||
func testNonceDesynchronizationCausesRehandshake() throws {
|
func testNonceDesynchronizationCausesRehandshake() throws {
|
||||||
// Test that nonce desynchronization leads to proper re-handshake
|
// Test that nonce desynchronization leads to proper re-handshake
|
||||||
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey)
|
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain)
|
||||||
let bobManager = NoiseSessionManager(localStaticKey: bobKey)
|
let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain)
|
||||||
|
|
||||||
// Establish sessions
|
// Establish sessions
|
||||||
try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager)
|
try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager)
|
||||||
@@ -499,8 +506,8 @@ final class NoiseProtocolTests: XCTestCase {
|
|||||||
func testHandshakePerformance() throws {
|
func testHandshakePerformance() throws {
|
||||||
measure {
|
measure {
|
||||||
do {
|
do {
|
||||||
let alice = NoiseSession(peerID: "bob", role: .initiator, localStaticKey: aliceKey)
|
let alice = NoiseSession(peerID: "bob", role: .initiator, keychain: mockKeychain, localStaticKey: aliceKey)
|
||||||
let bob = NoiseSession(peerID: "alice", role: .responder, localStaticKey: bobKey)
|
let bob = NoiseSession(peerID: "alice", role: .responder, keychain: mockKeychain, localStaticKey: bobKey)
|
||||||
try performHandshake(initiator: alice, responder: bob)
|
try performHandshake(initiator: alice, responder: bob)
|
||||||
} catch {
|
} catch {
|
||||||
XCTFail("Handshake failed: \(error)")
|
XCTFail("Handshake failed: \(error)")
|
||||||
@@ -530,12 +537,14 @@ final class NoiseProtocolTests: XCTestCase {
|
|||||||
aliceSession = NoiseSession(
|
aliceSession = NoiseSession(
|
||||||
peerID: TestConstants.testPeerID2,
|
peerID: TestConstants.testPeerID2,
|
||||||
role: .initiator,
|
role: .initiator,
|
||||||
|
keychain: mockKeychain,
|
||||||
localStaticKey: aliceKey
|
localStaticKey: aliceKey
|
||||||
)
|
)
|
||||||
|
|
||||||
bobSession = NoiseSession(
|
bobSession = NoiseSession(
|
||||||
peerID: TestConstants.testPeerID1,
|
peerID: TestConstants.testPeerID1,
|
||||||
role: .responder,
|
role: .responder,
|
||||||
|
keychain: mockKeychain,
|
||||||
localStaticKey: bobKey
|
localStaticKey: bobKey
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user