Merge branch 'main' into fix/646-harden-hex-parsing

This commit is contained in:
jack
2026-01-04 12:28:39 -10:00
committed by GitHub
6 changed files with 115 additions and 90 deletions
-50
View File
@@ -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)
}
}
+2 -2
View File
@@ -12,9 +12,9 @@ final class NostrIdentityBridge {
private var derivedIdentityCache: [String: NostrIdentity] = [:] private var derivedIdentityCache: [String: NostrIdentity] = [:]
private let cacheLock = NSLock() private let cacheLock = NSLock()
private let keychain: KeychainHelperProtocol private let keychain: KeychainManagerProtocol
init(keychain: KeychainHelperProtocol = KeychainHelper()) { init(keychain: KeychainManagerProtocol = KeychainManager()) {
self.keychain = keychain self.keychain = keychain
} }
@@ -26,7 +26,7 @@ final class FavoritesPersistenceService: ObservableObject {
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"
private let keychain: KeychainHelperProtocol private let keychain: KeychainManagerProtocol
@Published private(set) var favorites: [Data: FavoriteRelationship] = [:] // Noise pubkey -> relationship @Published private(set) var favorites: [Data: FavoriteRelationship] = [:] // Noise pubkey -> relationship
@Published private(set) var mutualFavorites: Set<Data> = [] @Published private(set) var mutualFavorites: Set<Data> = []
@@ -36,7 +36,7 @@ final class FavoritesPersistenceService: ObservableObject {
static let shared = FavoritesPersistenceService() static let shared = FavoritesPersistenceService()
init(keychain: KeychainHelperProtocol = KeychainHelper()) { init(keychain: KeychainManagerProtocol = KeychainManager()) {
self.keychain = keychain self.keychain = keychain
loadFavorites() loadFavorites()
+53
View File
@@ -20,6 +20,14 @@ protocol KeychainManagerProtocol {
func secureClear(_ string: inout String) func secureClear(_ string: inout String)
func verifyIdentityKeyExists() -> Bool 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 { final class KeychainManager: KeychainManagerProtocol {
@@ -314,4 +322,49 @@ final class KeychainManager: KeychainManagerProtocol {
let key = "identity_noiseStaticKey" let key = "identity_noiseStaticKey"
return retrieveData(forKey: key) != nil 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)
}
} }
@@ -10,6 +10,7 @@ import Foundation
final class PreviewKeychainManager: KeychainManagerProtocol { final class PreviewKeychainManager: KeychainManagerProtocol {
private var storage: [String: Data] = [:] private var storage: [String: Data] = [:]
private var serviceStorage: [String: [String: Data]] = [:]
init() {} init() {}
func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool { func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool {
@@ -28,6 +29,7 @@ final class PreviewKeychainManager: KeychainManagerProtocol {
func deleteAllKeychainData() -> Bool { func deleteAllKeychainData() -> Bool {
storage.removeAll() storage.removeAll()
serviceStorage.removeAll()
return true return true
} }
@@ -38,4 +40,21 @@ final class PreviewKeychainManager: KeychainManagerProtocol {
func verifyIdentityKeyExists() -> Bool { func verifyIdentityKeyExists() -> Bool {
storage["identity_noiseStaticKey"] != nil 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)
}
} }
+12 -9
View File
@@ -11,6 +11,7 @@ import Foundation
final class MockKeychain: KeychainManagerProtocol { final class MockKeychain: KeychainManagerProtocol {
private var storage: [String: Data] = [:] private var storage: [String: Data] = [:]
private var serviceStorage: [String: [String: Data]] = [:]
func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool { func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool {
storage[key] = keyData storage[key] = keyData
@@ -28,11 +29,11 @@ final class MockKeychain: KeychainManagerProtocol {
func deleteAllKeychainData() -> Bool { func deleteAllKeychainData() -> Bool {
storage.removeAll() storage.removeAll()
serviceStorage.removeAll()
return true return true
} }
func secureClear(_ data: inout Data) { func secureClear(_ data: inout Data) {
//
data = Data() data = Data()
} }
@@ -43,22 +44,24 @@ final class MockKeychain: KeychainManagerProtocol {
func verifyIdentityKeyExists() -> Bool { func verifyIdentityKeyExists() -> Bool {
storage["identity_noiseStaticKey"] != nil storage["identity_noiseStaticKey"] != nil
} }
}
final class MockKeychainHelper: KeychainHelperProtocol { // MARK: - Generic Data Storage (consolidated from KeychainHelper)
private typealias Service = String
private typealias Key = String
private var storage: [Service: [Key: Data]] = [:]
func save(key: String, data: Data, service: String, accessible: CFString?) { 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? { func load(key: String, service: String) -> Data? {
storage[service]?[key] serviceStorage[service]?[key]
} }
func delete(key: String, service: String) { 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