chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
import CodexBarCore
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
protocol CookieHeaderStoring: Sendable {
|
||||
func loadCookieHeader() throws -> String?
|
||||
func storeCookieHeader(_ header: String?) throws
|
||||
}
|
||||
|
||||
enum CookieHeaderStoreError: LocalizedError {
|
||||
case keychainStatus(OSStatus)
|
||||
case invalidData
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case let .keychainStatus(status):
|
||||
"Keychain error: \(status)"
|
||||
case .invalidData:
|
||||
"Keychain returned invalid data."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct KeychainCookieHeaderStore: CookieHeaderStoring {
|
||||
private static let log = CodexBarLog.logger(LogCategories.cookieHeaderStore)
|
||||
|
||||
private let service = "com.steipete.CodexBar"
|
||||
private let account: String
|
||||
private let promptKind: KeychainPromptContext.Kind
|
||||
|
||||
// Cache to reduce keychain access frequency
|
||||
private nonisolated(unsafe) static var cache: [String: CachedValue] = [:]
|
||||
private static let cacheLock = NSLock()
|
||||
private static let cacheTTL: TimeInterval = 1800 // 30 minutes
|
||||
|
||||
private struct CachedValue {
|
||||
let value: String?
|
||||
let timestamp: Date
|
||||
|
||||
var isExpired: Bool {
|
||||
Date().timeIntervalSince(self.timestamp) > KeychainCookieHeaderStore.cacheTTL
|
||||
}
|
||||
}
|
||||
|
||||
init(account: String, promptKind: KeychainPromptContext.Kind) {
|
||||
self.account = account
|
||||
self.promptKind = promptKind
|
||||
}
|
||||
|
||||
func loadCookieHeader() throws -> String? {
|
||||
guard !KeychainAccessGate.isDisabled else {
|
||||
Self.log.debug("Keychain access disabled; skipping cookie load")
|
||||
return nil
|
||||
}
|
||||
// Check cache first
|
||||
Self.cacheLock.lock()
|
||||
if let cached = Self.cache[self.account], !cached.isExpired {
|
||||
Self.cacheLock.unlock()
|
||||
Self.log.debug("Using cached cookie header for \(self.account)")
|
||||
return cached.value
|
||||
}
|
||||
Self.cacheLock.unlock()
|
||||
var result: CFTypeRef?
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: self.service,
|
||||
kSecAttrAccount as String: self.account,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
kSecReturnData as String: true,
|
||||
]
|
||||
|
||||
if case .interactionRequired = KeychainAccessPreflight
|
||||
.checkGenericPassword(service: self.service, account: self.account)
|
||||
{
|
||||
KeychainPromptHandler.handler?(KeychainPromptContext(
|
||||
kind: self.promptKind,
|
||||
service: self.service,
|
||||
account: self.account))
|
||||
}
|
||||
|
||||
let status = KeychainSecurity.copyMatching(query as CFDictionary, &result)
|
||||
if status == errSecItemNotFound {
|
||||
// Cache the nil result
|
||||
Self.cacheLock.lock()
|
||||
Self.cache[self.account] = CachedValue(value: nil, timestamp: Date())
|
||||
Self.cacheLock.unlock()
|
||||
return nil
|
||||
}
|
||||
guard status == errSecSuccess else {
|
||||
Self.log.error("Keychain read failed: \(status)")
|
||||
throw CookieHeaderStoreError.keychainStatus(status)
|
||||
}
|
||||
|
||||
guard let data = result as? Data else {
|
||||
throw CookieHeaderStoreError.invalidData
|
||||
}
|
||||
let header = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let finalValue = (header?.isEmpty == false) ? header : nil
|
||||
|
||||
// Cache the result
|
||||
Self.cacheLock.lock()
|
||||
Self.cache[self.account] = CachedValue(value: finalValue, timestamp: Date())
|
||||
Self.cacheLock.unlock()
|
||||
|
||||
return finalValue
|
||||
}
|
||||
|
||||
func storeCookieHeader(_ header: String?) throws {
|
||||
guard !KeychainAccessGate.isDisabled else {
|
||||
Self.log.debug("Keychain access disabled; skipping cookie store")
|
||||
return
|
||||
}
|
||||
guard let raw = header?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!raw.isEmpty
|
||||
else {
|
||||
try self.deleteIfPresent()
|
||||
// Invalidate cache
|
||||
Self.cacheLock.lock()
|
||||
Self.cache.removeValue(forKey: self.account)
|
||||
Self.cacheLock.unlock()
|
||||
return
|
||||
}
|
||||
guard CookieHeaderNormalizer.normalize(raw) != nil else {
|
||||
try self.deleteIfPresent()
|
||||
// Invalidate cache
|
||||
Self.cacheLock.lock()
|
||||
Self.cache.removeValue(forKey: self.account)
|
||||
Self.cacheLock.unlock()
|
||||
return
|
||||
}
|
||||
|
||||
let data = raw.data(using: .utf8)!
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: self.service,
|
||||
kSecAttrAccount as String: self.account,
|
||||
]
|
||||
let attributes: [String: Any] = [
|
||||
kSecValueData as String: data,
|
||||
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
|
||||
]
|
||||
|
||||
let updateStatus = KeychainSecurity.update(query as CFDictionary, attributes as CFDictionary)
|
||||
if updateStatus == errSecSuccess {
|
||||
// Update cache
|
||||
Self.cacheLock.lock()
|
||||
Self.cache[self.account] = CachedValue(value: raw, timestamp: Date())
|
||||
Self.cacheLock.unlock()
|
||||
return
|
||||
}
|
||||
if updateStatus != errSecItemNotFound {
|
||||
Self.log.error("Keychain update failed: \(updateStatus)")
|
||||
throw CookieHeaderStoreError.keychainStatus(updateStatus)
|
||||
}
|
||||
|
||||
var addQuery = query
|
||||
for (key, value) in attributes {
|
||||
addQuery[key] = value
|
||||
}
|
||||
let addStatus = KeychainSecurity.add(addQuery as CFDictionary, nil)
|
||||
guard addStatus == errSecSuccess else {
|
||||
Self.log.error("Keychain add failed: \(addStatus)")
|
||||
throw CookieHeaderStoreError.keychainStatus(addStatus)
|
||||
}
|
||||
|
||||
// Update cache
|
||||
Self.cacheLock.lock()
|
||||
Self.cache[self.account] = CachedValue(value: raw, timestamp: Date())
|
||||
Self.cacheLock.unlock()
|
||||
}
|
||||
|
||||
private func deleteIfPresent() throws {
|
||||
guard !KeychainAccessGate.isDisabled else { return }
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: self.service,
|
||||
kSecAttrAccount as String: self.account,
|
||||
]
|
||||
let status = KeychainSecurity.delete(query as CFDictionary)
|
||||
if status == errSecSuccess || status == errSecItemNotFound {
|
||||
// Invalidate cache
|
||||
Self.cacheLock.lock()
|
||||
Self.cache.removeValue(forKey: self.account)
|
||||
Self.cacheLock.unlock()
|
||||
return
|
||||
}
|
||||
Self.log.error("Keychain delete failed: \(status)")
|
||||
throw CookieHeaderStoreError.keychainStatus(status)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user