import Foundation#if os(macOS)import Darwinimport Security#endif public enum KeychainCacheStore { public struct Key: Hashable, Sendable { public let category: String public let identifier: String public init(category: String, identifier: String) { self.category = category self.identifier = identifier } var account: String { "\(self.category).\(self.identifier)" } } public enum LoadResult<Entry> { case found(Entry) case missing case temporarilyUnavailable case invalid } public enum ClearResult: Equatable, Sendable { case removed case missing case failed } public enum KeysResult: Equatable, Sendable { case found([Key]) case temporarilyUnavailable case failed } private static let log = CodexBarLog.logger(LogCategories.keychainCache) private static let cacheService = "com.steipete.codexbar.cache" private static let cacheLabel = "CodexBar Cache" @TaskLocal private static var serviceOverride: String? @TaskLocal private static var forceImplicitTestStore = false #if DEBUG @TaskLocal private static var operationRecorder: OperationRecorder? enum Operation: Equatable, Sendable { case load case store case clear } final class OperationRecorder: @unchecked Sendable { private let lock = NSLock() private var recordedOperations: [Operation] = [] var operations: [Operation] { self.lock.withLock { self.recordedOperations } } func record(_ operation: Operation) { self.lock.withLock { self.recordedOperations.append(operation) } } } #endif #if DEBUG && os(macOS) @TaskLocal private static var loadFailureStatusOverride: OSStatus? @TaskLocal private static var storeFailureStatusOverride: OSStatus? @TaskLocal private static var clearFailureStatusOverride: OSStatus? @TaskLocal private static var keysFailureStatusOverride: OSStatus? #endif private static let testStoreLock = NSLock() private struct TestStoreKey: Hashable { let service: String let account: String } private nonisolated(unsafe) static var testStore: [TestStoreKey: Data]? private nonisolated(unsafe) static var implicitTestStore: [TestStoreKey: Data] = [:] private nonisolated(unsafe) static var testStoreRefCount = 0 public static func load<Entry: Codable>( key: Key, as type: Entry.Type = Entry.self) -> LoadResult<Entry> { #if DEBUG self.operationRecorder?.record(.load) #endif #if DEBUG && os(macOS) if let status = self.loadFailureStatusOverride { return self.loadResultForKeychainReadFailure(status: status, key: key) } #endif if let testResult = loadFromTestStore(key: key, as: type) { return testResult } guard self.canUseRealKeychain else { return .missing } #if os(macOS) var query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: self.serviceName, kSecAttrAccount as String: key.account, kSecMatchLimit as String: kSecMatchLimitOne, kSecReturnData as String: true, ] KeychainNoUIQuery.apply(to: &query) var result: AnyObject? let status = KeychainSecurity.copyMatching(query as CFDictionary, &result) switch status { case errSecSuccess: guard let data = result as? Data, !data.isEmpty else { self.log.error("Keychain cache item was empty (\(key.account))") return .invalid } let decoder = Self.makeDecoder() guard let decoded = try? decoder.decode(Entry.self, from: data) else { self.log.error("Failed to decode keychain cache (\(key.account))") return .invalid } return .found(decoded) default: return self.loadResultForKeychainReadFailure(status: status, key: key) } #else return .missing #endif } public static func store(key: Key, entry: some Codable) { _ = self.storeResult(key: key, entry: entry) } @discardableResult public static func storeResult(key: Key, entry: some Codable) -> Bool { #if DEBUG self.operationRecorder?.record(.store) #endif #if DEBUG && os(macOS) if let status = self.storeFailureStatusOverride { self.log.error("Keychain cache store failed (\(key.account)): \(status)") return false } #endif if let stored = self.storeInTestStore(key: key, entry: entry) { return stored } guard self.canUseRealKeychain else { return false } #if os(macOS) let encoder = Self.makeEncoder() guard let data = try? encoder.encode(entry) else { self.log.error("Failed to encode keychain cache (\(key.account))") return false } var query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: self.serviceName, kSecAttrAccount as String: key.account, ] KeychainNoUIQuery.apply(to: &query) let updateStatus = KeychainSecurity.update( query as CFDictionary, [kSecValueData as String: data] as CFDictionary) if updateStatus == errSecSuccess { return true } if updateStatus != errSecItemNotFound { self.log.error("Keychain cache update failed (\(key.account)): \(updateStatus)") return false } var addQuery = query addQuery[kSecValueData as String] = data addQuery[kSecAttrLabel as String] = self.cacheLabel addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly if let access = self.cacheAccessControl() { addQuery[kSecAttrAccess as String] = access } let addStatus = KeychainSecurity.add(addQuery as CFDictionary, nil) if addStatus != errSecSuccess { self.log.error("Keychain cache add failed (\(key.account)): \(addStatus)") } return addStatus == errSecSuccess #else return false #endif } @discardableResult public static func clear(key: Key) -> Bool { self.clearResult(key: key) == .removed } public static func clearResult(key: Key) -> ClearResult { #if DEBUG self.operationRecorder?.record(.clear) #endif #if DEBUG && os(macOS) if let status = self.clearFailureStatusOverride { return self.clearResultForKeychainDeleteStatus(status, key: key) } #endif if let removed = self.clearTestStore(key: key) { return removed ? .removed : .missing } guard self.canUseRealKeychain else { return .failed } #if os(macOS) var query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: self.serviceName, kSecAttrAccount as String: key.account, ] KeychainNoUIQuery.apply(to: &query) return self.clearResultForKeychainDeleteStatus(KeychainSecurity.delete(query as CFDictionary), key: key) #else return .failed #endif } public static func keys(category: String) -> [Key] { switch self.keysResult(category: category) { case let .found(keys): keys case .temporarilyUnavailable, .failed: [] } } public static func keysResult(category: String) -> KeysResult { #if DEBUG && os(macOS) if let status = self.keysFailureStatusOverride { return self.keysResultForKeychainStatus(status, category: category, result: nil) } #endif if let keys = self.keysFromTestStore(category: category) { return .found(keys) } guard self.canUseRealKeychain else { return .failed } #if os(macOS) var query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: self.serviceName, kSecMatchLimit as String: kSecMatchLimitAll, kSecReturnAttributes as String: true, ] KeychainNoUIQuery.apply(to: &query) var result: AnyObject? let status = KeychainSecurity.copyMatching(query as CFDictionary, &result) return self.keysResultForKeychainStatus(status, category: category, result: result) #else return .failed #endif } public static func withServiceOverrideForTesting<T>( _ service: String?, operation: () throws -> T) rethrows -> T { try self.$serviceOverride.withValue(service) { try operation() } } public static func withServiceOverrideForTesting<T>( _ service: String?, isolation _: isolated (any Actor)? = #isolation, operation: () async throws -> T) async rethrows -> T { try await self.$serviceOverride.withValue(service) { try await operation() } } public static func withCurrentServiceOverrideForTesting<T>( operation: () async throws -> T) async rethrows -> T { let service = self.serviceOverride return try await self.$serviceOverride.withValue(service) { try await operation() } } static func withImplicitTestStoreForTesting<T>( operation: () throws -> T) rethrows -> T { try self.$forceImplicitTestStore.withValue(true) { try operation() } } static func withImplicitTestStoreForTesting<T>( isolation _: isolated (any Actor)? = #isolation, operation: () async throws -> T) async rethrows -> T { try await self.$forceImplicitTestStore.withValue(true) { try await operation() } } public static var currentServiceOverrideForTesting: String? { self.serviceOverride } #if DEBUG static func withOperationRecorderForTesting<T>( _ recorder: OperationRecorder?, operation: () throws -> T) rethrows -> T { try self.$operationRecorder.withValue(recorder) { try operation() } } static func withOperationRecorderForTesting<T>( _ recorder: OperationRecorder?, operation: () async throws -> T) async rethrows -> T { try await self.$operationRecorder.withValue(recorder) { try await operation() } } static var currentOperationRecorderForTesting: OperationRecorder? { self.operationRecorder } #endif static var canUseRealKeychainForTesting: Bool { self.canUseRealKeychain } static var canEnumerateOrDeleteRealKeychainForTesting: Bool { self.canUseRealKeychain } #if DEBUG && os(macOS) public static func withLoadFailureStatusOverrideForTesting<T>( _ status: OSStatus?, operation: () throws -> T) rethrows -> T { try self.$loadFailureStatusOverride.withValue(status) { try operation() } } public static func withStoreFailureStatusOverrideForTesting<T>( _ status: OSStatus?, operation: () throws -> T) rethrows -> T { try self.$storeFailureStatusOverride.withValue(status) { try operation() } } public static func withClearFailureStatusOverrideForTesting<T>( _ status: OSStatus?, operation: () throws -> T) rethrows -> T { try self.$clearFailureStatusOverride.withValue(status) { try operation() } } public static func withClearFailureStatusOverrideForTesting<T>( _ status: OSStatus?, operation: () async throws -> T) async rethrows -> T { try await self.$clearFailureStatusOverride.withValue(status) { try await operation() } } public static func withKeysFailureStatusOverrideForTesting<T>( _ status: OSStatus?, operation: () throws -> T) rethrows -> T { try self.$keysFailureStatusOverride.withValue(status) { try operation() } } #endif static func setTestStoreForTesting(_ enabled: Bool) { self.testStoreLock.lock() defer { self.testStoreLock.unlock() } if enabled { self.testStoreRefCount += 1 if self.testStoreRefCount == 1 { self.testStore = [:] } } else { self.testStoreRefCount = max(0, self.testStoreRefCount - 1) if self.testStoreRefCount == 0 { self.testStore = nil } } } private static var serviceName: String { serviceOverride ?? self.cacheService } private static var canUseRealKeychain: Bool { !KeychainAccessGate.isDisabled } #if DEBUG private static var shouldUseImplicitTestStore: Bool { KeychainTestSafety.isRunningUnderTests( processName: ProcessInfo.processInfo.processName, environment: ProcessInfo.processInfo.environment) && !self.canUseRealKeychain } #else private static var shouldUseImplicitTestStore: Bool { false } #endif private static func makeEncoder() -> JSONEncoder { let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 return encoder } private static func makeDecoder() -> JSONDecoder { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 return decoder } #if os(macOS) static func loadResultForKeychainReadFailure<Entry>( status: OSStatus, key: Key) -> LoadResult<Entry> { switch status { case errSecItemNotFound: return .missing case errSecInteractionNotAllowed: // Keychain is temporarily locked, e.g. immediately after wake from sleep. self.log.info("Keychain cache temporarily locked (\(key.account)), will retry on next access") return .temporarilyUnavailable default: self.log.error("Keychain cache read failed (\(key.account)): \(status)") return .invalid } } static func clearResultForKeychainDeleteStatus(_ status: OSStatus, key: Key) -> ClearResult { switch status { case errSecSuccess: return .removed case errSecItemNotFound: return .missing case errSecInteractionNotAllowed: self.log.info("Keychain cache delete temporarily unavailable (\(key.account))") return .failed default: self.log.error("Keychain cache delete failed (\(key.account)): \(status)") return .failed } } private static func keysResultForKeychainStatus( _ status: OSStatus, category: String, result: AnyObject?) -> KeysResult { switch status { case errSecSuccess: guard let rows = result as? [[String: Any]] else { return .failed } let keys: [Key] = rows.compactMap { row in guard let account = row[kSecAttrAccount as String] as? String else { return nil } return self.key(fromAccount: account, category: category) } return .found(keys) case errSecItemNotFound: return .found([]) case errSecInteractionNotAllowed: self.log.info("Keychain cache keys temporarily unavailable (\(category))") return .temporarilyUnavailable default: self.log.error("Keychain cache key listing failed (\(category)): \(status)") return .failed } } static func trustedApplicationPathsForCacheAccess( bundleURL: URL = Bundle.main.bundleURL, executableURL: URL? = Bundle.main.executableURL, fileExists: (String) -> Bool = { FileManager.default.fileExists(atPath: $0) }) -> [String] {Lines 1–500 of 690. Load more
import Foundation #if os(macOS) import Darwin import Security #endif public enum KeychainCacheStore { public struct Key: Hashable, Sendable { public let category: String public let identifier: String public init(category: String, identifier: String) { self.category = category self.identifier = identifier } var account: String { "\(self.category).\(self.identifier)" } } public enum LoadResult<Entry> { case found(Entry) case missing case temporarilyUnavailable case invalid } public enum ClearResult: Equatable, Sendable { case removed case missing case failed } public enum KeysResult: Equatable, Sendable { case found([Key]) case temporarilyUnavailable case failed } private static let log = CodexBarLog.logger(LogCategories.keychainCache) private static let cacheService = "com.steipete.codexbar.cache" private static let cacheLabel = "CodexBar Cache" @TaskLocal private static var serviceOverride: String? @TaskLocal private static var forceImplicitTestStore = false #if DEBUG @TaskLocal private static var operationRecorder: OperationRecorder? enum Operation: Equatable, Sendable { case load case store case clear } final class OperationRecorder: @unchecked Sendable { private let lock = NSLock() private var recordedOperations: [Operation] = [] var operations: [Operation] { self.lock.withLock { self.recordedOperations } } func record(_ operation: Operation) { self.lock.withLock { self.recordedOperations.append(operation) } } } #endif #if DEBUG && os(macOS) @TaskLocal private static var loadFailureStatusOverride: OSStatus? @TaskLocal private static var storeFailureStatusOverride: OSStatus? @TaskLocal private static var clearFailureStatusOverride: OSStatus? @TaskLocal private static var keysFailureStatusOverride: OSStatus? #endif private static let testStoreLock = NSLock() private struct TestStoreKey: Hashable { let service: String let account: String } private nonisolated(unsafe) static var testStore: [TestStoreKey: Data]? private nonisolated(unsafe) static var implicitTestStore: [TestStoreKey: Data] = [:] private nonisolated(unsafe) static var testStoreRefCount = 0 public static func load<Entry: Codable>( key: Key, as type: Entry.Type = Entry.self) -> LoadResult<Entry> { #if DEBUG self.operationRecorder?.record(.load) #endif #if DEBUG && os(macOS) if let status = self.loadFailureStatusOverride { return self.loadResultForKeychainReadFailure(status: status, key: key) } #endif if let testResult = loadFromTestStore(key: key, as: type) { return testResult } guard self.canUseRealKeychain else { return .missing } #if os(macOS) var query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: self.serviceName, kSecAttrAccount as String: key.account, kSecMatchLimit as String: kSecMatchLimitOne, kSecReturnData as String: true, ] KeychainNoUIQuery.apply(to: &query) var result: AnyObject? let status = KeychainSecurity.copyMatching(query as CFDictionary, &result) switch status { case errSecSuccess: guard let data = result as? Data, !data.isEmpty else { self.log.error("Keychain cache item was empty (\(key.account))") return .invalid } let decoder = Self.makeDecoder() guard let decoded = try? decoder.decode(Entry.self, from: data) else { self.log.error("Failed to decode keychain cache (\(key.account))") return .invalid } return .found(decoded) default: return self.loadResultForKeychainReadFailure(status: status, key: key) } #else return .missing #endif } public static func store(key: Key, entry: some Codable) { _ = self.storeResult(key: key, entry: entry) } @discardableResult public static func storeResult(key: Key, entry: some Codable) -> Bool { #if DEBUG self.operationRecorder?.record(.store) #endif #if DEBUG && os(macOS) if let status = self.storeFailureStatusOverride { self.log.error("Keychain cache store failed (\(key.account)): \(status)") return false } #endif if let stored = self.storeInTestStore(key: key, entry: entry) { return stored } guard self.canUseRealKeychain else { return false } #if os(macOS) let encoder = Self.makeEncoder() guard let data = try? encoder.encode(entry) else { self.log.error("Failed to encode keychain cache (\(key.account))") return false } var query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: self.serviceName, kSecAttrAccount as String: key.account, ] KeychainNoUIQuery.apply(to: &query) let updateStatus = KeychainSecurity.update( query as CFDictionary, [kSecValueData as String: data] as CFDictionary) if updateStatus == errSecSuccess { return true } if updateStatus != errSecItemNotFound { self.log.error("Keychain cache update failed (\(key.account)): \(updateStatus)") return false } var addQuery = query addQuery[kSecValueData as String] = data addQuery[kSecAttrLabel as String] = self.cacheLabel addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly if let access = self.cacheAccessControl() { addQuery[kSecAttrAccess as String] = access } let addStatus = KeychainSecurity.add(addQuery as CFDictionary, nil) if addStatus != errSecSuccess { self.log.error("Keychain cache add failed (\(key.account)): \(addStatus)") } return addStatus == errSecSuccess #else return false #endif } @discardableResult public static func clear(key: Key) -> Bool { self.clearResult(key: key) == .removed } public static func clearResult(key: Key) -> ClearResult { #if DEBUG self.operationRecorder?.record(.clear) #endif #if DEBUG && os(macOS) if let status = self.clearFailureStatusOverride { return self.clearResultForKeychainDeleteStatus(status, key: key) } #endif if let removed = self.clearTestStore(key: key) { return removed ? .removed : .missing } guard self.canUseRealKeychain else { return .failed } #if os(macOS) var query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: self.serviceName, kSecAttrAccount as String: key.account, ] KeychainNoUIQuery.apply(to: &query) return self.clearResultForKeychainDeleteStatus(KeychainSecurity.delete(query as CFDictionary), key: key) #else return .failed #endif } public static func keys(category: String) -> [Key] { switch self.keysResult(category: category) { case let .found(keys): keys case .temporarilyUnavailable, .failed: [] } } public static func keysResult(category: String) -> KeysResult { #if DEBUG && os(macOS) if let status = self.keysFailureStatusOverride { return self.keysResultForKeychainStatus(status, category: category, result: nil) } #endif if let keys = self.keysFromTestStore(category: category) { return .found(keys) } guard self.canUseRealKeychain else { return .failed } #if os(macOS) var query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: self.serviceName, kSecMatchLimit as String: kSecMatchLimitAll, kSecReturnAttributes as String: true, ] KeychainNoUIQuery.apply(to: &query) var result: AnyObject? let status = KeychainSecurity.copyMatching(query as CFDictionary, &result) return self.keysResultForKeychainStatus(status, category: category, result: result) #else return .failed #endif } public static func withServiceOverrideForTesting<T>( _ service: String?, operation: () throws -> T) rethrows -> T { try self.$serviceOverride.withValue(service) { try operation() } } public static func withServiceOverrideForTesting<T>( _ service: String?, isolation _: isolated (any Actor)? = #isolation, operation: () async throws -> T) async rethrows -> T { try await self.$serviceOverride.withValue(service) { try await operation() } } public static func withCurrentServiceOverrideForTesting<T>( operation: () async throws -> T) async rethrows -> T { let service = self.serviceOverride return try await self.$serviceOverride.withValue(service) { try await operation() } } static func withImplicitTestStoreForTesting<T>( operation: () throws -> T) rethrows -> T { try self.$forceImplicitTestStore.withValue(true) { try operation() } } static func withImplicitTestStoreForTesting<T>( isolation _: isolated (any Actor)? = #isolation, operation: () async throws -> T) async rethrows -> T { try await self.$forceImplicitTestStore.withValue(true) { try await operation() } } public static var currentServiceOverrideForTesting: String? { self.serviceOverride } #if DEBUG static func withOperationRecorderForTesting<T>( _ recorder: OperationRecorder?, operation: () throws -> T) rethrows -> T { try self.$operationRecorder.withValue(recorder) { try operation() } } static func withOperationRecorderForTesting<T>( _ recorder: OperationRecorder?, operation: () async throws -> T) async rethrows -> T { try await self.$operationRecorder.withValue(recorder) { try await operation() } } static var currentOperationRecorderForTesting: OperationRecorder? { self.operationRecorder } #endif static var canUseRealKeychainForTesting: Bool { self.canUseRealKeychain } static var canEnumerateOrDeleteRealKeychainForTesting: Bool { self.canUseRealKeychain } #if DEBUG && os(macOS) public static func withLoadFailureStatusOverrideForTesting<T>( _ status: OSStatus?, operation: () throws -> T) rethrows -> T { try self.$loadFailureStatusOverride.withValue(status) { try operation() } } public static func withStoreFailureStatusOverrideForTesting<T>( _ status: OSStatus?, operation: () throws -> T) rethrows -> T { try self.$storeFailureStatusOverride.withValue(status) { try operation() } } public static func withClearFailureStatusOverrideForTesting<T>( _ status: OSStatus?, operation: () throws -> T) rethrows -> T { try self.$clearFailureStatusOverride.withValue(status) { try operation() } } public static func withClearFailureStatusOverrideForTesting<T>( _ status: OSStatus?, operation: () async throws -> T) async rethrows -> T { try await self.$clearFailureStatusOverride.withValue(status) { try await operation() } } public static func withKeysFailureStatusOverrideForTesting<T>( _ status: OSStatus?, operation: () throws -> T) rethrows -> T { try self.$keysFailureStatusOverride.withValue(status) { try operation() } } #endif static func setTestStoreForTesting(_ enabled: Bool) { self.testStoreLock.lock() defer { self.testStoreLock.unlock() } if enabled { self.testStoreRefCount += 1 if self.testStoreRefCount == 1 { self.testStore = [:] } } else { self.testStoreRefCount = max(0, self.testStoreRefCount - 1) if self.testStoreRefCount == 0 { self.testStore = nil } } } private static var serviceName: String { serviceOverride ?? self.cacheService } private static var canUseRealKeychain: Bool { !KeychainAccessGate.isDisabled } #if DEBUG private static var shouldUseImplicitTestStore: Bool { KeychainTestSafety.isRunningUnderTests( processName: ProcessInfo.processInfo.processName, environment: ProcessInfo.processInfo.environment) && !self.canUseRealKeychain } #else private static var shouldUseImplicitTestStore: Bool { false } #endif private static func makeEncoder() -> JSONEncoder { let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 return encoder } private static func makeDecoder() -> JSONDecoder { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 return decoder } #if os(macOS) static func loadResultForKeychainReadFailure<Entry>( status: OSStatus, key: Key) -> LoadResult<Entry> { switch status { case errSecItemNotFound: return .missing case errSecInteractionNotAllowed: // Keychain is temporarily locked, e.g. immediately after wake from sleep. self.log.info("Keychain cache temporarily locked (\(key.account)), will retry on next access") return .temporarilyUnavailable default: self.log.error("Keychain cache read failed (\(key.account)): \(status)") return .invalid } } static func clearResultForKeychainDeleteStatus(_ status: OSStatus, key: Key) -> ClearResult { switch status { case errSecSuccess: return .removed case errSecItemNotFound: return .missing case errSecInteractionNotAllowed: self.log.info("Keychain cache delete temporarily unavailable (\(key.account))") return .failed default: self.log.error("Keychain cache delete failed (\(key.account)): \(status)") return .failed } } private static func keysResultForKeychainStatus( _ status: OSStatus, category: String, result: AnyObject?) -> KeysResult { switch status { case errSecSuccess: guard let rows = result as? [[String: Any]] else { return .failed } let keys: [Key] = rows.compactMap { row in guard let account = row[kSecAttrAccount as String] as? String else { return nil } return self.key(fromAccount: account, category: category) } return .found(keys) case errSecItemNotFound: return .found([]) case errSecInteractionNotAllowed: self.log.info("Keychain cache keys temporarily unavailable (\(category))") return .temporarilyUnavailable default: self.log.error("Keychain cache key listing failed (\(category)): \(status)") return .failed } } static func trustedApplicationPathsForCacheAccess( bundleURL: URL = Bundle.main.bundleURL, executableURL: URL? = Bundle.main.executableURL, fileExists: (String) -> Bool = { FileManager.default.fileExists(atPath: $0) }) -> [String] {