import Foundation import Security // 极简 Keychain 封装:单 app 私有 generic-password 条目,无 access group。Keychain *共享* // (跨 app / extension)才需付费 ADP 的 entitlement;本封装不跨 app,故零 entitlement, // 模拟器与真机均可用(见 ios/PLAN.md §9 禁区分级——共享才是账号项,私有不是)。 // // 用途:持久化扫码登录拿到的会话(access_token + user),使 app 重装 / 重建后免反复扫码。 // kSecAttrAccessibleAfterFirstUnlock:设备首次解锁后即可读(含后台),符合“常驻登录”诉求, // 又不在锁屏前暴露。 enum Keychain { static func save(_ data: Data, service: String, account: String) { let base: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account, ] SecItemDelete(base as CFDictionary) // 覆盖写:先删旧条目再加,避免 duplicate item var attrs = base attrs[kSecValueData as String] = data attrs[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock SecItemAdd(attrs as CFDictionary, nil) } static func load(service: String, account: String) -> Data? { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account, kSecReturnData as String: true, kSecMatchLimit as String: kSecMatchLimitOne, ] var out: AnyObject? guard SecItemCopyMatching(query as CFDictionary, &out) == errSecSuccess, let data = out as? Data else { return nil } return data } static func delete(service: String, account: String) { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account, ] SecItemDelete(query as CFDictionary) } }