diff --git a/Sources/CodexBar/CookieHeaderStore.swift b/Sources/CodexBar/CookieHeaderStore.swift index ac3905b0..808d7575 100644 --- a/Sources/CodexBar/CookieHeaderStore.swift +++ b/Sources/CodexBar/CookieHeaderStore.swift @@ -153,14 +153,26 @@ struct KeychainCookieHeaderStore: CookieHeaderStoring { throw CookieHeaderStoreError.keychainStatus(updateStatus) } - var addQuery = query - for (key, value) in attributes { - addQuery[key] = value - } - let addStatus = SecItemAdd(addQuery as CFDictionary, nil) - guard addStatus == errSecSuccess else { - Self.log.error("Keychain add failed: \(addStatus)") - throw CookieHeaderStoreError.keychainStatus(addStatus) + // Use ACL helper to add with CodexBar trusted, preventing future prompts + do { + try KeychainACLHelper.addGenericPasswordWithTrustedAccess( + service: self.service, + account: self.account, + data: data, + label: "\(self.account) cookie header" + ) + } catch { + Self.log.error("Keychain add with trusted access failed: \(error)") + // Fallback to standard add + var addQuery = query + for (key, value) in attributes { + addQuery[key] = value + } + let addStatus = SecItemAdd(addQuery as CFDictionary, nil) + guard addStatus == errSecSuccess else { + Self.log.error("Keychain add failed: \(addStatus)") + throw CookieHeaderStoreError.keychainStatus(addStatus) + } } // Update cache diff --git a/Sources/CodexBar/CopilotTokenStore.swift b/Sources/CodexBar/CopilotTokenStore.swift index 4fcff001..e7f11ec0 100644 --- a/Sources/CodexBar/CopilotTokenStore.swift +++ b/Sources/CodexBar/CopilotTokenStore.swift @@ -100,14 +100,26 @@ struct KeychainCopilotTokenStore: CopilotTokenStoring { throw CopilotTokenStoreError.keychainStatus(updateStatus) } - var addQuery = query - for (key, value) in attributes { - addQuery[key] = value - } - let addStatus = SecItemAdd(addQuery as CFDictionary, nil) - guard addStatus == errSecSuccess else { - Self.log.error("Keychain add failed: \(addStatus)") - throw CopilotTokenStoreError.keychainStatus(addStatus) + // Use ACL helper to add with CodexBar trusted, preventing future prompts + do { + try KeychainACLHelper.addGenericPasswordWithTrustedAccess( + service: self.service, + account: self.account, + data: data, + label: "GitHub Copilot Token" + ) + } catch { + Self.log.error("Keychain add with trusted access failed: \(error)") + // Fallback to standard add + var addQuery = query + for (key, value) in attributes { + addQuery[key] = value + } + let addStatus = SecItemAdd(addQuery as CFDictionary, nil) + guard addStatus == errSecSuccess else { + Self.log.error("Keychain add failed: \(addStatus)") + throw CopilotTokenStoreError.keychainStatus(addStatus) + } } } diff --git a/Sources/CodexBar/KeychainACLHelper.swift b/Sources/CodexBar/KeychainACLHelper.swift new file mode 100644 index 00000000..4ce0df71 --- /dev/null +++ b/Sources/CodexBar/KeychainACLHelper.swift @@ -0,0 +1,211 @@ +import CodexBarCore +import Foundation +import Security + +/// Helper to manage keychain ACLs and reduce repeated permission prompts. +/// +/// The core issue: macOS prompts for keychain access every time an app accesses +/// a keychain item unless that app is in the item's "Always allow" list. +/// +/// This helper provides: +/// 1. Creating keychain items with CodexBar already in the trusted apps list +/// 2. Updating existing items to add CodexBar to the trusted list (after user approval) +enum KeychainACLHelper { + private static let log = CodexBarLog.logger(LogCategories.keychainACL) + + /// Error types for ACL operations + enum ACLError: LocalizedError { + case accessCopyFailed(OSStatus) + case aclListCopyFailed(OSStatus) + case aclContentsCopyFailed(OSStatus) + case aclSetContentsFailed(OSStatus) + case noACLFound + case appPathNotFound + case trustedAppCreationFailed(OSStatus) + case accessModificationFailed(OSStatus) + + var errorDescription: String? { + switch self { + case .accessCopyFailed(let status): + return "Failed to copy keychain access: \(status)" + case .aclListCopyFailed(let status): + return "Failed to copy ACL list: \(status)" + case .aclContentsCopyFailed(let status): + return "Failed to copy ACL contents: \(status)" + case .aclSetContentsFailed(let status): + return "Failed to set ACL contents: \(status)" + case .noACLFound: + return "No ACL found for keychain item" + case .appPathNotFound: + return "Could not determine app bundle path" + case .trustedAppCreationFailed(let status): + return "Failed to create trusted app reference: \(status)" + case .accessModificationFailed(let status): + return "Failed to modify access: \(status)" + } + } + } + + /// Get the path to the current app bundle + private static var appPath: String? { + Bundle.main.bundlePath + } + + /// Create a SecAccess that includes CodexBar in the trusted apps list. + /// Items created with this access won't prompt for CodexBar. + static func createAccessWithCodexBarTrusted(description: String) throws -> SecAccess { + guard let appPath = self.appPath else { + throw ACLError.appPathNotFound + } + + var trustedApp: SecTrustedApplication? + let trustedStatus = SecTrustedApplicationCreateFromPath(appPath, &trustedApp) + guard trustedStatus == errSecSuccess, let app = trustedApp else { + throw ACLError.trustedAppCreationFailed(trustedStatus) + } + + var access: SecAccess? + let accessStatus = SecAccessCreate(description as CFString, [app] as CFArray, &access) + guard accessStatus == errSecSuccess, let createdAccess = access else { + throw ACLError.accessCopyFailed(accessStatus) + } + + return createdAccess + } + + /// Add a generic password to keychain with CodexBar pre-authorized. + /// This prevents future prompts for this item. + static func addGenericPasswordWithTrustedAccess( + service: String, + account: String, + data: Data, + label: String? = nil + ) throws { + let access = try self.createAccessWithCodexBarTrusted( + description: label ?? "\(service):\(account)" + ) + + var query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: account, + kSecValueData as String: data, + kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, + kSecAttrAccess as String: access, + ] + + if let label = label { + query[kSecAttrLabel as String] = label + } + + // Delete existing item first + let deleteQuery: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: account, + ] + SecItemDelete(deleteQuery as CFDictionary) + + let status = SecItemAdd(query as CFDictionary, nil) + guard status == errSecSuccess else { + self.log.error("Failed to add keychain item with trusted access: \(status)") + throw ACLError.accessModificationFailed(status) + } + + self.log.info("Added keychain item with CodexBar trusted: \(service):\(account)") + } + + /// Update an existing keychain item to add CodexBar to the trusted apps list. + /// This requires reading the item first (which may prompt), then rewriting it. + /// + /// Note: This only works for items that CodexBar has write access to. + /// External items (like "Chrome Safe Storage") cannot be modified this way. + static func addCodexBarToTrustedApps( + service: String, + account: String + ) throws { + // First, read the existing item (this may prompt the user) + var result: CFTypeRef? + let readQuery: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: account, + kSecMatchLimit as String: kSecMatchLimitOne, + kSecReturnData as String: true, + kSecReturnAttributes as String: true, + kSecReturnRef as String: true, + ] + + let readStatus = SecItemCopyMatching(readQuery as CFDictionary, &result) + guard readStatus == errSecSuccess else { + self.log.error("Failed to read keychain item for ACL update: \(readStatus)") + throw ACLError.accessCopyFailed(readStatus) + } + + guard let itemDict = result as? [String: Any], + let itemData = itemDict[kSecValueData as String] as? Data + else { + throw ACLError.noACLFound + } + + // Rewrite the item with CodexBar in the trusted list + try self.addGenericPasswordWithTrustedAccess( + service: service, + account: account, + data: itemData, + label: itemDict[kSecAttrLabel as String] as? String + ) + + self.log.info("Updated keychain item ACL to trust CodexBar: \(service):\(account)") + } + + /// Migrate all CodexBar-owned keychain items to use trusted access. + /// Call this once after the user grants initial access. + static func migrateCodexBarItemsToTrustedAccess() { + let items: [(service: String, account: String)] = [ + ("com.steipete.CodexBar", "codex-cookie"), + ("com.steipete.CodexBar", "claude-cookie"), + ("com.steipete.CodexBar", "cursor-cookie"), + ("com.steipete.CodexBar", "factory-cookie"), + ("com.steipete.CodexBar", "minimax-cookie"), + ("com.steipete.CodexBar", "minimax-api-token"), + ("com.steipete.CodexBar", "augment-cookie"), + ("com.steipete.CodexBar", "amp-cookie"), + ("com.steipete.CodexBar", "copilot-api-token"), + ("com.steipete.CodexBar", "zai-api-token"), + ("com.steipete.CodexBar", "synthetic-api-key"), + ("com.steipete.CodexBar", "kimi-auth-token"), + ("com.steipete.CodexBar", "kimi-k2-api-key"), + ] + + var migratedCount = 0 + var errorCount = 0 + + for item in items { + do { + try self.addCodexBarToTrustedApps(service: item.service, account: item.account) + migratedCount += 1 + } catch { + // Item might not exist, which is fine + if case ACLError.accessCopyFailed(let status) = error, + status == errSecItemNotFound { + continue + } + errorCount += 1 + self.log.warning("Failed to migrate \(item.service):\(item.account): \(error)") + } + } + + if migratedCount > 0 { + self.log.info("Migrated \(migratedCount) keychain items to trusted access") + } + if errorCount > 0 { + self.log.warning("\(errorCount) items failed to migrate") + } + } +} + +// MARK: - Log Category +extension LogCategories { + static let keychainACL = "keychain-acl" +} diff --git a/Sources/CodexBar/KeychainSetupHelper.swift b/Sources/CodexBar/KeychainSetupHelper.swift new file mode 100644 index 00000000..a317f8bb --- /dev/null +++ b/Sources/CodexBar/KeychainSetupHelper.swift @@ -0,0 +1,125 @@ +import AppKit +import CodexBarCore +import Foundation +import Security +import SweetCookieKit + +/// Helper for guiding users through Chrome Safe Storage keychain setup. +/// +/// Chrome Safe Storage is owned by Chrome, so CodexBar cannot programmatically +/// add itself to the ACL. This helper detects when setup is needed and guides +/// users through the manual process. +enum KeychainSetupHelper { + private static let log = CodexBarLog.logger(LogCategories.keychainSetup) + + /// Status of Chrome Safe Storage access + enum AccessStatus: Equatable { + case allowed + case needsSetup + case notFound + case keychainDisabled + } + + /// Check if Chrome Safe Storage requires user setup + static func checkChromeSafeStorageAccess() -> AccessStatus { + guard !KeychainAccessGate.isDisabled else { + return .keychainDisabled + } + + // Check all known Chrome Safe Storage variants + for label in Browser.safeStorageLabels { + let outcome = KeychainAccessPreflight.checkGenericPassword( + service: label.service, + account: label.account + ) + switch outcome { + case .allowed: + self.log.debug("Chrome Safe Storage access allowed", metadata: ["service": label.service]) + return .allowed + case .interactionRequired: + self.log.info("Chrome Safe Storage needs setup", metadata: ["service": label.service]) + return .needsSetup + case .notFound, .failure: + continue + } + } + + return .notFound + } + + /// Open Keychain Access app and search for Chrome Safe Storage + static func openKeychainAccessForSetup() { + self.log.info("Opening Keychain Access for Chrome Safe Storage setup") + + // First, open Keychain Access + NSWorkspace.shared.open(URL(fileURLWithPath: "/System/Applications/Utilities/Keychain Access.app")) + + // Give it a moment to open, then use AppleScript to search + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.searchInKeychainAccess(for: "Chrome Safe Storage") + } + } + + /// Use AppleScript to search for an item in Keychain Access + private static func searchInKeychainAccess(for searchTerm: String) { + // AppleScript to activate Keychain Access and trigger search + let script = """ + tell application "Keychain Access" + activate + end tell + + delay 0.3 + + tell application "System Events" + tell process "Keychain Access" + -- Focus the search field (Cmd+F or click search) + keystroke "f" using command down + delay 0.2 + -- Type the search term + keystroke "\(searchTerm)" + end tell + end tell + """ + + if let appleScript = NSAppleScript(source: script) { + var error: NSDictionary? + appleScript.executeAndReturnError(&error) + if let error { + // AppleScript might fail due to permissions, but Keychain Access should still be open + self.log.warning( + "AppleScript search failed (Keychain Access still open)", + metadata: ["error": String(describing: error)] + ) + } + } + } + + /// Instructions for manual setup (for display in UI) + static let setupInstructions: [String] = [ + "1. Double-click \"Chrome Safe Storage\" in the list", + "2. Click the \"Access Control\" tab", + "3. Click \"+\" and add CodexBar.app from /Applications", + "4. Click \"Save Changes\" and enter your password", + ] + + /// Check if any Chromium browser is installed that would need this setup + static func hasChromiumBrowserInstalled() -> Bool { + let chromiumBrowsers = [ + "/Applications/Google Chrome.app", + "/Applications/Microsoft Edge.app", + "/Applications/Brave Browser.app", + "/Applications/Arc.app", + "/Applications/Vivaldi.app", + "/Applications/Chromium.app", + ] + + let fm = FileManager.default + return chromiumBrowsers.contains { fm.fileExists(atPath: $0) } + } +} + +// MARK: - Log Category + +extension LogCategories { + static let keychainSetup = "keychain-setup" +} diff --git a/Sources/CodexBar/KimiK2TokenStore.swift b/Sources/CodexBar/KimiK2TokenStore.swift index 9ad23c02..c054216f 100644 --- a/Sources/CodexBar/KimiK2TokenStore.swift +++ b/Sources/CodexBar/KimiK2TokenStore.swift @@ -92,14 +92,26 @@ struct KeychainKimiK2TokenStore: KimiK2TokenStoring { throw KimiK2TokenStoreError.keychainStatus(updateStatus) } - var addQuery = query - for (key, value) in attributes { - addQuery[key] = value - } - let addStatus = SecItemAdd(addQuery as CFDictionary, nil) - guard addStatus == errSecSuccess else { - Self.log.error("Keychain add failed: \(addStatus)") - throw KimiK2TokenStoreError.keychainStatus(addStatus) + // Use ACL helper to add with CodexBar trusted, preventing future prompts + do { + try KeychainACLHelper.addGenericPasswordWithTrustedAccess( + service: self.service, + account: self.account, + data: data, + label: "Kimi K2 API Key" + ) + } catch { + Self.log.error("Keychain add with trusted access failed: \(error)") + // Fallback to standard add + var addQuery = query + for (key, value) in attributes { + addQuery[key] = value + } + let addStatus = SecItemAdd(addQuery as CFDictionary, nil) + guard addStatus == errSecSuccess else { + Self.log.error("Keychain add failed: \(addStatus)") + throw KimiK2TokenStoreError.keychainStatus(addStatus) + } } } diff --git a/Sources/CodexBar/KimiTokenStore.swift b/Sources/CodexBar/KimiTokenStore.swift index dddcb159..983bd33f 100644 --- a/Sources/CodexBar/KimiTokenStore.swift +++ b/Sources/CodexBar/KimiTokenStore.swift @@ -100,14 +100,26 @@ struct KeychainKimiTokenStore: KimiTokenStoring { throw KimiTokenStoreError.keychainStatus(updateStatus) } - var addQuery = query - for (key, value) in attributes { - addQuery[key] = value - } - let addStatus = SecItemAdd(addQuery as CFDictionary, nil) - guard addStatus == errSecSuccess else { - Self.log.error("Keychain add failed: \(addStatus)") - throw KimiTokenStoreError.keychainStatus(addStatus) + // Use ACL helper to add with CodexBar trusted, preventing future prompts + do { + try KeychainACLHelper.addGenericPasswordWithTrustedAccess( + service: self.service, + account: self.account, + data: data, + label: "Kimi Auth Token" + ) + } catch { + Self.log.error("Keychain add with trusted access failed: \(error)") + // Fallback to standard add + var addQuery = query + for (key, value) in attributes { + addQuery[key] = value + } + let addStatus = SecItemAdd(addQuery as CFDictionary, nil) + guard addStatus == errSecSuccess else { + Self.log.error("Keychain add failed: \(addStatus)") + throw KimiTokenStoreError.keychainStatus(addStatus) + } } } diff --git a/Sources/CodexBar/MiniMaxAPITokenStore.swift b/Sources/CodexBar/MiniMaxAPITokenStore.swift index e4d281b9..3f7cc4a0 100644 --- a/Sources/CodexBar/MiniMaxAPITokenStore.swift +++ b/Sources/CodexBar/MiniMaxAPITokenStore.swift @@ -100,14 +100,26 @@ struct KeychainMiniMaxAPITokenStore: MiniMaxAPITokenStoring { throw MiniMaxAPITokenStoreError.keychainStatus(updateStatus) } - var addQuery = query - for (key, value) in attributes { - addQuery[key] = value - } - let addStatus = SecItemAdd(addQuery as CFDictionary, nil) - guard addStatus == errSecSuccess else { - Self.log.error("Keychain add failed: \(addStatus)") - throw MiniMaxAPITokenStoreError.keychainStatus(addStatus) + // Use ACL helper to add with CodexBar trusted, preventing future prompts + do { + try KeychainACLHelper.addGenericPasswordWithTrustedAccess( + service: self.service, + account: self.account, + data: data, + label: "MiniMax API Token" + ) + } catch { + Self.log.error("Keychain add with trusted access failed: \(error)") + // Fallback to standard add + var addQuery = query + for (key, value) in attributes { + addQuery[key] = value + } + let addStatus = SecItemAdd(addQuery as CFDictionary, nil) + guard addStatus == errSecSuccess else { + Self.log.error("Keychain add failed: \(addStatus)") + throw MiniMaxAPITokenStoreError.keychainStatus(addStatus) + } } } diff --git a/Sources/CodexBar/MiniMaxCookieStore.swift b/Sources/CodexBar/MiniMaxCookieStore.swift index e36bbe71..2fdbe202 100644 --- a/Sources/CodexBar/MiniMaxCookieStore.swift +++ b/Sources/CodexBar/MiniMaxCookieStore.swift @@ -105,14 +105,26 @@ struct KeychainMiniMaxCookieStore: MiniMaxCookieStoring { throw MiniMaxCookieStoreError.keychainStatus(updateStatus) } - var addQuery = query - for (key, value) in attributes { - addQuery[key] = value - } - let addStatus = SecItemAdd(addQuery as CFDictionary, nil) - guard addStatus == errSecSuccess else { - Self.log.error("Keychain add failed: \(addStatus)") - throw MiniMaxCookieStoreError.keychainStatus(addStatus) + // Use ACL helper to add with CodexBar trusted, preventing future prompts + do { + try KeychainACLHelper.addGenericPasswordWithTrustedAccess( + service: self.service, + account: self.account, + data: data, + label: "MiniMax Cookie" + ) + } catch { + Self.log.error("Keychain add with trusted access failed: \(error)") + // Fallback to standard add + var addQuery = query + for (key, value) in attributes { + addQuery[key] = value + } + let addStatus = SecItemAdd(addQuery as CFDictionary, nil) + guard addStatus == errSecSuccess else { + Self.log.error("Keychain add failed: \(addStatus)") + throw MiniMaxCookieStoreError.keychainStatus(addStatus) + } } } diff --git a/Sources/CodexBar/PreferencesAdvancedPane.swift b/Sources/CodexBar/PreferencesAdvancedPane.swift index 1db4897f..3d82d649 100644 --- a/Sources/CodexBar/PreferencesAdvancedPane.swift +++ b/Sources/CodexBar/PreferencesAdvancedPane.swift @@ -6,6 +6,8 @@ struct AdvancedPane: View { @Bindable var settings: SettingsStore @State private var isInstallingCLI = false @State private var cliStatus: String? + @State private var keychainStatus: KeychainSetupHelper.AccessStatus = .notFound + @State private var showingKeychainInstructions = false var body: some View { ScrollView(.vertical, showsIndicators: true) { @@ -88,11 +90,84 @@ struct AdvancedPane: View { subtitle: "Prevents any Keychain access while enabled.", binding: self.$settings.debugDisableKeychainAccess) } + + // Chrome Safe Storage setup section + if !self.settings.debugDisableKeychainAccess { + Divider() + + SettingsSection( + title: "Browser cookie access", + caption: """ + Chrome-based browsers encrypt cookies with a key stored in Keychain. \ + CodexBar needs "Always Allow" access to read cookies without prompts. + """) { + self.keychainSetupView + } + } } .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 20) .padding(.vertical, 12) } + .onAppear { + self.checkKeychainStatus() + } + .sheet(isPresented: self.$showingKeychainInstructions) { + KeychainSetupInstructionsView(isPresented: self.$showingKeychainInstructions) { + self.checkKeychainStatus() + } + } + } + + @ViewBuilder + private var keychainSetupView: some View { + HStack(spacing: 12) { + switch self.keychainStatus { + case .allowed: + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + Text("Chrome Safe Storage: Access granted") + .font(.footnote) + .foregroundStyle(.secondary) + + case .needsSetup: + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(.orange) + Text("Chrome Safe Storage: Setup needed") + .font(.footnote) + .foregroundStyle(.secondary) + Spacer() + Button("Fix Now") { + self.showingKeychainInstructions = true + } + .buttonStyle(.borderedProminent) + .controlSize(.small) + + case .notFound: + Image(systemName: "info.circle") + .foregroundColor(.secondary) + Text("No Chrome-based browser detected") + .font(.footnote) + .foregroundStyle(.tertiary) + + case .keychainDisabled: + Image(systemName: "lock.slash") + .foregroundColor(.secondary) + Text("Keychain access is disabled") + .font(.footnote) + .foregroundStyle(.tertiary) + } + } + + Button("Check Status") { + self.checkKeychainStatus() + } + .buttonStyle(.link) + .controlSize(.small) + } + + private func checkKeychainStatus() { + self.keychainStatus = KeychainSetupHelper.checkChromeSafeStorageAccess() } } @@ -154,3 +229,75 @@ extension AdvancedPane { return resolved == destination } } + +// MARK: - Keychain Setup Instructions Sheet + +@MainActor +struct KeychainSetupInstructionsView: View { + @Binding var isPresented: Bool + let onComplete: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 20) { + // Header + HStack { + Image(systemName: "key.fill") + .font(.title) + .foregroundColor(.orange) + VStack(alignment: .leading, spacing: 4) { + Text("Fix Keychain Prompts") + .font(.headline) + Text("One-time setup to stop repeated permission dialogs") + .font(.subheadline) + .foregroundStyle(.secondary) + } + Spacer() + } + + Divider() + + // Instructions + VStack(alignment: .leading, spacing: 12) { + Text("Follow these steps:") + .font(.subheadline) + .fontWeight(.medium) + + ForEach(Array(KeychainSetupHelper.setupInstructions.enumerated()), id: \.offset) { _, instruction in + HStack(alignment: .top, spacing: 8) { + Text(instruction) + .font(.callout) + } + } + } + + Divider() + + // Buttons + HStack(spacing: 12) { + Button("Open Keychain Access") { + KeychainSetupHelper.openKeychainAccessForSetup() + } + .buttonStyle(.borderedProminent) + + Spacer() + + Button("Done") { + self.onComplete() + self.isPresented = false + } + } + + // Tip + HStack(spacing: 8) { + Image(systemName: "lightbulb.fill") + .foregroundColor(.yellow) + Text("Tip: You may need to unlock the keychain with your Mac password first.") + .font(.caption) + .foregroundStyle(.secondary) + } + .padding(.top, 8) + } + .padding(24) + .frame(width: 480) + } +} diff --git a/Sources/CodexBar/SyntheticTokenStore.swift b/Sources/CodexBar/SyntheticTokenStore.swift index fb4c78fd..d49f93e1 100644 --- a/Sources/CodexBar/SyntheticTokenStore.swift +++ b/Sources/CodexBar/SyntheticTokenStore.swift @@ -100,14 +100,26 @@ struct KeychainSyntheticTokenStore: SyntheticTokenStoring { throw SyntheticTokenStoreError.keychainStatus(updateStatus) } - var addQuery = query - for (key, value) in attributes { - addQuery[key] = value - } - let addStatus = SecItemAdd(addQuery as CFDictionary, nil) - guard addStatus == errSecSuccess else { - Self.log.error("Keychain add failed: \(addStatus)") - throw SyntheticTokenStoreError.keychainStatus(addStatus) + // Use ACL helper to add with CodexBar trusted, preventing future prompts + do { + try KeychainACLHelper.addGenericPasswordWithTrustedAccess( + service: self.service, + account: self.account, + data: data, + label: "Synthetic API Key" + ) + } catch { + Self.log.error("Keychain add with trusted access failed: \(error)") + // Fallback to standard add + var addQuery = query + for (key, value) in attributes { + addQuery[key] = value + } + let addStatus = SecItemAdd(addQuery as CFDictionary, nil) + guard addStatus == errSecSuccess else { + Self.log.error("Keychain add failed: \(addStatus)") + throw SyntheticTokenStoreError.keychainStatus(addStatus) + } } } diff --git a/Sources/CodexBar/ZaiTokenStore.swift b/Sources/CodexBar/ZaiTokenStore.swift index ee4c3918..7039aaa9 100644 --- a/Sources/CodexBar/ZaiTokenStore.swift +++ b/Sources/CodexBar/ZaiTokenStore.swift @@ -137,14 +137,26 @@ struct KeychainZaiTokenStore: ZaiTokenStoring { throw ZaiTokenStoreError.keychainStatus(updateStatus) } - var addQuery = query - for (key, value) in attributes { - addQuery[key] = value - } - let addStatus = SecItemAdd(addQuery as CFDictionary, nil) - guard addStatus == errSecSuccess else { - Self.log.error("Keychain add failed: \(addStatus)") - throw ZaiTokenStoreError.keychainStatus(addStatus) + // Use ACL helper to add with CodexBar trusted, preventing future prompts + do { + try KeychainACLHelper.addGenericPasswordWithTrustedAccess( + service: self.service, + account: self.account, + data: data, + label: "z.ai API Token" + ) + } catch { + Self.log.error("Keychain add with trusted access failed: \(error)") + // Fallback to standard add + var addQuery = query + for (key, value) in attributes { + addQuery[key] = value + } + let addStatus = SecItemAdd(addQuery as CFDictionary, nil) + guard addStatus == errSecSuccess else { + Self.log.error("Keychain add failed: \(addStatus)") + throw ZaiTokenStoreError.keychainStatus(addStatus) + } } // Update cache