krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v2: Hutch/Extensions/KeychainHelper.swift · raw
1import Foundation
2@preconcurrency import Security
3
4enum KeychainHelper: Sendable {
5
6 private static let service = "net.cleberg.Hutch"
7 private static let tokenAccount = "srht-access-token"
8
9 // MARK: - Save
10
11 static func saveToken(_ token: String) throws {
12 guard let data = token.data(using: .utf8) else {
13 throw KeychainError.encodingFailed
14 }
15
16 // Delete any existing item first
17 let deleteQuery: [String: Any] = [
18 kSecClass as String: kSecClassGenericPassword,
19 kSecAttrService as String: service,
20 kSecAttrAccount as String: tokenAccount
21 ]
22 SecItemDelete(deleteQuery as CFDictionary)
23
24 let addQuery: [String: Any] = [
25 kSecClass as String: kSecClassGenericPassword,
26 kSecAttrService as String: service,
27 kSecAttrAccount as String: tokenAccount,
28 kSecValueData as String: data,
29 kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
30 ]
31
32 let status = SecItemAdd(addQuery as CFDictionary, nil)
33 guard status == errSecSuccess else {
34 throw KeychainError.saveFailed(status)
35 }
36 }
37
38 // MARK: - Load
39
40 static func loadToken() -> String? {
41 let query: [String: Any] = [
42 kSecClass as String: kSecClassGenericPassword,
43 kSecAttrService as String: service,
44 kSecAttrAccount as String: tokenAccount,
45 kSecReturnData as String: true,
46 kSecMatchLimit as String: kSecMatchLimitOne
47 ]
48
49 var result: AnyObject?
50 let status = SecItemCopyMatching(query as CFDictionary, &result)
51
52 guard status == errSecSuccess,
53 let data = result as? Data,
54 let token = String(data: data, encoding: .utf8) else {
55 return nil
56 }
57 return token
58 }
59
60 // MARK: - Delete
61
62 static func deleteToken() throws {
63 let query: [String: Any] = [
64 kSecClass as String: kSecClassGenericPassword,
65 kSecAttrService as String: service,
66 kSecAttrAccount as String: tokenAccount
67 ]
68
69 let status = SecItemDelete(query as CFDictionary)
70 guard status == errSecSuccess || status == errSecItemNotFound else {
71 throw KeychainError.deleteFailed(status)
72 }
73 }
74
75 static func deleteAll() throws {
76 let query: [String: Any] = [
77 kSecClass as String: kSecClassGenericPassword,
78 kSecAttrService as String: service
79 ]
80
81 let status = SecItemDelete(query as CFDictionary)
82 guard status == errSecSuccess || status == errSecItemNotFound else {
83 throw KeychainError.deleteFailed(status)
84 }
85 }
86}
87
88enum KeychainError: LocalizedError {
89 case encodingFailed
90 case saveFailed(OSStatus)
91 case deleteFailed(OSStatus)
92
93 var errorDescription: String? {
94 switch self {
95 case .encodingFailed:
96 "Failed to encode token data."
97 case .saveFailed(let status):
98 "Keychain save failed with status \(status)."
99 case .deleteFailed(let status):
100 "Keychain delete failed with status \(status)."
101 }
102 }
103}