KeychainService.swift 2.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
  1. import Foundation
  2. import Security
  3. /// Minimal Keychain wrapper for storing the Chad Music API key.
  4. enum KeychainService {
  5. private static let service = "com.mixboard.chad-music"
  6. private static let apiKeyAccount = "api-key"
  7. // MARK: - API Key
  8. static func saveAPIKey(_ key: String) throws {
  9. guard let data = key.data(using: .utf8) else { return }
  10. // Delete existing item first
  11. let deleteQuery: [String: Any] = [
  12. kSecClass as String: kSecClassGenericPassword,
  13. kSecAttrService as String: service,
  14. kSecAttrAccount as String: apiKeyAccount,
  15. ]
  16. SecItemDelete(deleteQuery as CFDictionary)
  17. // Add new item
  18. let addQuery: [String: Any] = [
  19. kSecClass as String: kSecClassGenericPassword,
  20. kSecAttrService as String: service,
  21. kSecAttrAccount as String: apiKeyAccount,
  22. kSecValueData as String: data,
  23. kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked,
  24. ]
  25. let status = SecItemAdd(addQuery as CFDictionary, nil)
  26. guard status == errSecSuccess else {
  27. throw KeychainError.saveFailed(status)
  28. }
  29. }
  30. static func loadAPIKey() -> String? {
  31. let query: [String: Any] = [
  32. kSecClass as String: kSecClassGenericPassword,
  33. kSecAttrService as String: service,
  34. kSecAttrAccount as String: apiKeyAccount,
  35. kSecReturnData as String: true,
  36. kSecMatchLimit as String: kSecMatchLimitOne,
  37. ]
  38. var result: AnyObject?
  39. let status = SecItemCopyMatching(query as CFDictionary, &result)
  40. guard status == errSecSuccess, let data = result as? Data else { return nil }
  41. return String(data: data, encoding: .utf8)
  42. }
  43. static func deleteAPIKey() {
  44. let query: [String: Any] = [
  45. kSecClass as String: kSecClassGenericPassword,
  46. kSecAttrService as String: service,
  47. kSecAttrAccount as String: apiKeyAccount,
  48. ]
  49. SecItemDelete(query as CFDictionary)
  50. }
  51. // MARK: - Errors
  52. enum KeychainError: LocalizedError {
  53. case saveFailed(OSStatus)
  54. var errorDescription: String? {
  55. switch self {
  56. case .saveFailed(let status):
  57. "Keychain save failed with status \(status)"
  58. }
  59. }
  60. }
  61. }