Parent directory

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}