KeychainStore.swift
1874 bytes
1import Foundation
2import Security
3
4enum KeychainStore {
5 static func load(account: String) throws -> String? {
6 let query = baseQuery(account: account).merging([
7 kSecReturnData as String: true,
8 kSecMatchLimit as String: kSecMatchLimitOne,
9 ]) { _, new in new }
10
11 var result: CFTypeRef?
12 let status = SecItemCopyMatching(query as CFDictionary, &result)
13
14 if status == errSecItemNotFound {
15 return nil
16 }
17
18 guard status == errSecSuccess,
19 let data = result as? Data,
20 let value = String(data: data, encoding: .utf8) else {
21 throw KeychainError.unavailable(status)
22 }
23
24 return value
25 }
26
27 static func save(_ value: String, account: String) throws {
28 let query = baseQuery(account: account)
29 let attributes = [kSecValueData as String: Data(value.utf8)]
30 let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
31
32 if updateStatus == errSecItemNotFound {
33 let addStatus = SecItemAdd(query.merging(attributes) { _, new in new } as CFDictionary, nil)
34 guard addStatus == errSecSuccess else {
35 throw KeychainError.unavailable(addStatus)
36 }
37 return
38 }
39
40 guard updateStatus == errSecSuccess else {
41 throw KeychainError.unavailable(updateStatus)
42 }
43 }
44
45 private static func baseQuery(account: String) -> [String: Any] {
46 [
47 kSecClass as String: kSecClassGenericPassword,
48 kSecAttrService as String: "com.theedgeofrage.ytrssil",
49 kSecAttrAccount as String: account,
50 ]
51 }
52}
53
54enum KeychainError: LocalizedError {
55 case unavailable(OSStatus)
56
57 var errorDescription: String? {
58 "The API token could not be stored securely."
59 }
60}