| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109 |
- import Foundation
- import Security
- /// Stores OAuth tokens in macOS Keychain.
- final class KeychainTokenStore {
- private let service: String
- private let account: String
- private let accessGroup: String?
- init(service: String = Bundle.main.bundleIdentifier ?? "meetings_app",
- account: String = "googleOAuthTokens",
- accessGroup: String? = nil) {
- self.service = service
- self.account = account
- self.accessGroup = accessGroup
- }
-
- private func makeKeychainError(status: OSStatus, message: String) -> NSError {
- let statusMessage = SecCopyErrorMessageString(status, nil) as String? ?? "OSStatus \(status)"
- return NSError(
- domain: NSOSStatusErrorDomain,
- code: Int(status),
- userInfo: [
- NSLocalizedDescriptionKey: message,
- NSLocalizedFailureReasonErrorKey: statusMessage,
- "keychainStatus": Int(status),
- "keychainStatusMessage": statusMessage,
- "keychainService": service,
- "keychainAccount": account,
- "keychainAccessGroup": accessGroup as Any
- ]
- )
- }
- func readTokens() throws -> GoogleOAuthTokens? {
- var query: [String: Any] = [
- kSecClass as String: kSecClassGenericPassword,
- kSecAttrService as String: service,
- kSecAttrAccount as String: account,
- kSecReturnData as String: true,
- kSecMatchLimit as String: kSecMatchLimitOne
- ]
- if let accessGroup {
- query[kSecAttrAccessGroup as String] = accessGroup
- }
- var result: CFTypeRef?
- let status = SecItemCopyMatching(query as CFDictionary, &result)
- if status == errSecItemNotFound { return nil }
- guard status == errSecSuccess else {
- throw makeKeychainError(status: status, message: "Failed to read OAuth tokens from Keychain.".localized)
- }
- guard let data = result as? Data else {
- throw NSError(
- domain: NSOSStatusErrorDomain,
- code: Int(errSecDecode),
- userInfo: [NSLocalizedDescriptionKey: "Keychain returned an invalid token payload.".localized]
- )
- }
- return try JSONDecoder().decode(GoogleOAuthTokens.self, from: data)
- }
- func writeTokens(_ tokens: GoogleOAuthTokens) throws {
- let data = try JSONEncoder().encode(tokens)
- var baseQuery: [String: Any] = [
- kSecClass as String: kSecClassGenericPassword,
- kSecAttrService as String: service,
- kSecAttrAccount as String: account
- ]
- if let accessGroup {
- baseQuery[kSecAttrAccessGroup as String] = accessGroup
- }
- let attributesToUpdate: [String: Any] = [
- kSecValueData as String: data
- ]
- let updateStatus = SecItemUpdate(baseQuery as CFDictionary, attributesToUpdate as CFDictionary)
- if updateStatus == errSecSuccess { return }
- if updateStatus != errSecItemNotFound {
- throw makeKeychainError(status: updateStatus, message: "Failed to update OAuth tokens in Keychain.".localized)
- }
- var addQuery = baseQuery
- addQuery[kSecValueData as String] = data
- addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
- let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
- guard addStatus == errSecSuccess else {
- throw makeKeychainError(status: addStatus, message: "Failed to save OAuth tokens to Keychain.".localized)
- }
- }
- func deleteTokens() throws {
- var query: [String: Any] = [
- kSecClass as String: kSecClassGenericPassword,
- kSecAttrService as String: service,
- kSecAttrAccount as String: account
- ]
- if let accessGroup {
- query[kSecAttrAccessGroup as String] = accessGroup
- }
- let status = SecItemDelete(query as CFDictionary)
- guard status == errSecSuccess || status == errSecItemNotFound else {
- throw makeKeychainError(status: status, message: "Failed to delete OAuth tokens from Keychain.".localized)
- }
- }
- }
|