KeychainTokenStore.swift 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. import Foundation
  2. import Security
  3. /// Stores OAuth tokens in macOS Keychain.
  4. final class KeychainTokenStore {
  5. private let service: String
  6. private let account: String
  7. private let accessGroup: String?
  8. init(service: String = Bundle.main.bundleIdentifier ?? "meetings_app",
  9. account: String = "googleOAuthTokens",
  10. accessGroup: String? = nil) {
  11. self.service = service
  12. self.account = account
  13. self.accessGroup = accessGroup
  14. }
  15. private func makeKeychainError(status: OSStatus, message: String) -> NSError {
  16. let statusMessage = SecCopyErrorMessageString(status, nil) as String? ?? "OSStatus \(status)"
  17. return NSError(
  18. domain: NSOSStatusErrorDomain,
  19. code: Int(status),
  20. userInfo: [
  21. NSLocalizedDescriptionKey: message,
  22. NSLocalizedFailureReasonErrorKey: statusMessage,
  23. "keychainStatus": Int(status),
  24. "keychainStatusMessage": statusMessage,
  25. "keychainService": service,
  26. "keychainAccount": account,
  27. "keychainAccessGroup": accessGroup as Any
  28. ]
  29. )
  30. }
  31. func readTokens() throws -> GoogleOAuthTokens? {
  32. var query: [String: Any] = [
  33. kSecClass as String: kSecClassGenericPassword,
  34. kSecAttrService as String: service,
  35. kSecAttrAccount as String: account,
  36. kSecReturnData as String: true,
  37. kSecMatchLimit as String: kSecMatchLimitOne
  38. ]
  39. if let accessGroup {
  40. query[kSecAttrAccessGroup as String] = accessGroup
  41. }
  42. var result: CFTypeRef?
  43. let status = SecItemCopyMatching(query as CFDictionary, &result)
  44. if status == errSecItemNotFound { return nil }
  45. guard status == errSecSuccess else {
  46. throw makeKeychainError(status: status, message: "Failed to read OAuth tokens from Keychain.".localized)
  47. }
  48. guard let data = result as? Data else {
  49. throw NSError(
  50. domain: NSOSStatusErrorDomain,
  51. code: Int(errSecDecode),
  52. userInfo: [NSLocalizedDescriptionKey: "Keychain returned an invalid token payload.".localized]
  53. )
  54. }
  55. return try JSONDecoder().decode(GoogleOAuthTokens.self, from: data)
  56. }
  57. func writeTokens(_ tokens: GoogleOAuthTokens) throws {
  58. let data = try JSONEncoder().encode(tokens)
  59. var baseQuery: [String: Any] = [
  60. kSecClass as String: kSecClassGenericPassword,
  61. kSecAttrService as String: service,
  62. kSecAttrAccount as String: account
  63. ]
  64. if let accessGroup {
  65. baseQuery[kSecAttrAccessGroup as String] = accessGroup
  66. }
  67. let attributesToUpdate: [String: Any] = [
  68. kSecValueData as String: data
  69. ]
  70. let updateStatus = SecItemUpdate(baseQuery as CFDictionary, attributesToUpdate as CFDictionary)
  71. if updateStatus == errSecSuccess { return }
  72. if updateStatus != errSecItemNotFound {
  73. throw makeKeychainError(status: updateStatus, message: "Failed to update OAuth tokens in Keychain.".localized)
  74. }
  75. var addQuery = baseQuery
  76. addQuery[kSecValueData as String] = data
  77. addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
  78. let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
  79. guard addStatus == errSecSuccess else {
  80. throw makeKeychainError(status: addStatus, message: "Failed to save OAuth tokens to Keychain.".localized)
  81. }
  82. }
  83. func deleteTokens() throws {
  84. var query: [String: Any] = [
  85. kSecClass as String: kSecClassGenericPassword,
  86. kSecAttrService as String: service,
  87. kSecAttrAccount as String: account
  88. ]
  89. if let accessGroup {
  90. query[kSecAttrAccessGroup as String] = accessGroup
  91. }
  92. let status = SecItemDelete(query as CFDictionary)
  93. guard status == errSecSuccess || status == errSecItemNotFound else {
  94. throw makeKeychainError(status: status, message: "Failed to delete OAuth tokens from Keychain.".localized)
  95. }
  96. }
  97. }