diff --git a/bitchat/Nostr/KeychainHelper.swift b/bitchat/Nostr/KeychainHelper.swift deleted file mode 100644 index 01de3dc43..000000000 --- a/bitchat/Nostr/KeychainHelper.swift +++ /dev/null @@ -1,50 +0,0 @@ -import Foundation - -protocol KeychainHelperProtocol { - func save(key: String, data: Data, service: String, accessible: CFString?) - func load(key: String, service: String) -> Data? - func delete(key: String, service: String) -} - -/// Keychain helper for secure storage -struct KeychainHelper: KeychainHelperProtocol { - func save(key: String, data: Data, service: String, accessible: CFString? = nil) { - var query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: service, - kSecAttrAccount as String: key, - kSecValueData as String: data - ] - if let accessible = accessible { - query[kSecAttrAccessible as String] = accessible - } - - SecItemDelete(query as CFDictionary) - SecItemAdd(query as CFDictionary, nil) - } - - func load(key: String, service: String) -> Data? { - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: service, - kSecAttrAccount as String: key, - kSecReturnData as String: true - ] - - var result: AnyObject? - let status = SecItemCopyMatching(query as CFDictionary, &result) - - guard status == errSecSuccess else { return nil } - return result as? Data - } - - func delete(key: String, service: String) { - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: service, - kSecAttrAccount as String: key - ] - - SecItemDelete(query as CFDictionary) - } -} diff --git a/bitchat/Nostr/NostrIdentityBridge.swift b/bitchat/Nostr/NostrIdentityBridge.swift index 37e207798..01d929e00 100644 --- a/bitchat/Nostr/NostrIdentityBridge.swift +++ b/bitchat/Nostr/NostrIdentityBridge.swift @@ -12,9 +12,9 @@ final class NostrIdentityBridge { private var derivedIdentityCache: [String: NostrIdentity] = [:] private let cacheLock = NSLock() - private let keychain: KeychainHelperProtocol + private let keychain: KeychainManagerProtocol - init(keychain: KeychainHelperProtocol = KeychainHelper()) { + init(keychain: KeychainManagerProtocol = KeychainManager()) { self.keychain = keychain } diff --git a/bitchat/Services/FavoritesPersistenceService.swift b/bitchat/Services/FavoritesPersistenceService.swift index e892285e9..1fe6b332e 100644 --- a/bitchat/Services/FavoritesPersistenceService.swift +++ b/bitchat/Services/FavoritesPersistenceService.swift @@ -26,7 +26,7 @@ final class FavoritesPersistenceService: ObservableObject { private static let storageKey = "chat.bitchat.favorites" private static let keychainService = "chat.bitchat.favorites" - private let keychain: KeychainHelperProtocol + private let keychain: KeychainManagerProtocol @Published private(set) var favorites: [Data: FavoriteRelationship] = [:] // Noise pubkey -> relationship @Published private(set) var mutualFavorites: Set = [] @@ -35,8 +35,8 @@ final class FavoritesPersistenceService: ObservableObject { private var cancellables = Set() static let shared = FavoritesPersistenceService() - - init(keychain: KeychainHelperProtocol = KeychainHelper()) { + + init(keychain: KeychainManagerProtocol = KeychainManager()) { self.keychain = keychain loadFavorites() diff --git a/bitchat/Services/KeychainManager.swift b/bitchat/Services/KeychainManager.swift index be6ed7843..c6af3d336 100644 --- a/bitchat/Services/KeychainManager.swift +++ b/bitchat/Services/KeychainManager.swift @@ -15,11 +15,19 @@ protocol KeychainManagerProtocol { 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 + + // MARK: - Generic Data Storage (consolidated from KeychainHelper) + /// Save data with a custom service name + func save(key: String, data: Data, service: String, accessible: CFString?) + /// Load data from a custom service + func load(key: String, service: String) -> Data? + /// Delete data from a custom service + func delete(key: String, service: String) } final class KeychainManager: KeychainManagerProtocol { @@ -309,9 +317,54 @@ final class KeychainManager: KeychainManagerProtocol { } // MARK: - Debug - + func verifyIdentityKeyExists() -> Bool { let key = "identity_noiseStaticKey" return retrieveData(forKey: key) != nil } + + // MARK: - Generic Data Storage (consolidated from KeychainHelper) + + /// Save data with a custom service name + func save(key: String, data: Data, service customService: String, accessible: CFString?) { + var query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: customService, + kSecAttrAccount as String: key, + kSecValueData as String: data + ] + if let accessible = accessible { + query[kSecAttrAccessible as String] = accessible + } + + SecItemDelete(query as CFDictionary) + SecItemAdd(query as CFDictionary, nil) + } + + /// Load data from a custom service + func load(key: String, service customService: String) -> Data? { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: customService, + kSecAttrAccount as String: key, + kSecReturnData as String: true + ] + + var result: AnyObject? + let status = SecItemCopyMatching(query as CFDictionary, &result) + + guard status == errSecSuccess else { return nil } + return result as? Data + } + + /// Delete data from a custom service + func delete(key: String, service customService: String) { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: customService, + kSecAttrAccount as String: key + ] + + SecItemDelete(query as CFDictionary) + } } diff --git a/bitchat/_PreviewHelpers/PreviewKeychainManager.swift b/bitchat/_PreviewHelpers/PreviewKeychainManager.swift index 07542c4e0..490fc69dc 100644 --- a/bitchat/_PreviewHelpers/PreviewKeychainManager.swift +++ b/bitchat/_PreviewHelpers/PreviewKeychainManager.swift @@ -10,32 +10,51 @@ import Foundation final class PreviewKeychainManager: KeychainManagerProtocol { private var storage: [String: Data] = [:] + private var serviceStorage: [String: [String: Data]] = [:] init() {} - + 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() + serviceStorage.removeAll() return true } - + func secureClear(_ data: inout Data) {} - + func secureClear(_ string: inout String) {} - + func verifyIdentityKeyExists() -> Bool { storage["identity_noiseStaticKey"] != nil } + + // MARK: - Generic Data Storage (consolidated from KeychainHelper) + + func save(key: String, data: Data, service: String, accessible: CFString?) { + if serviceStorage[service] == nil { + serviceStorage[service] = [:] + } + serviceStorage[service]?[key] = data + } + + func load(key: String, service: String) -> Data? { + serviceStorage[service]?[key] + } + + func delete(key: String, service: String) { + serviceStorage[service]?.removeValue(forKey: key) + } } diff --git a/bitchatTests/Mocks/MockKeychain.swift b/bitchatTests/Mocks/MockKeychain.swift index f8718c829..c3ffa9d3e 100644 --- a/bitchatTests/Mocks/MockKeychain.swift +++ b/bitchatTests/Mocks/MockKeychain.swift @@ -11,54 +11,57 @@ import Foundation final class MockKeychain: KeychainManagerProtocol { private var storage: [String: Data] = [:] - + private var serviceStorage: [String: [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() + serviceStorage.removeAll() return true } - + func secureClear(_ data: inout Data) { - // data = Data() } - + func secureClear(_ string: inout String) { string = "" } - + func verifyIdentityKeyExists() -> Bool { storage["identity_noiseStaticKey"] != nil } -} -final class MockKeychainHelper: KeychainHelperProtocol { - private typealias Service = String - private typealias Key = String - private var storage: [Service: [Key: Data]] = [:] - + // MARK: - Generic Data Storage (consolidated from KeychainHelper) + func save(key: String, data: Data, service: String, accessible: CFString?) { - storage[service]?[key] = data + if serviceStorage[service] == nil { + serviceStorage[service] = [:] + } + serviceStorage[service]?[key] = data } - + func load(key: String, service: String) -> Data? { - storage[service]?[key] + serviceStorage[service]?[key] } - + func delete(key: String, service: String) { - storage[service]?.removeValue(forKey: key) + serviceStorage[service]?.removeValue(forKey: key) } } + +/// Typealias for backwards compatibility with tests using MockKeychainHelper +typealias MockKeychainHelper = MockKeychain