summaryrefslogtreecommitdiff
path: root/Hutch/Extensions/KeychainHelper.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-03-17 23:19:43 -0500
committerChristian Cleberg <[email protected]>2026-03-17 23:19:43 -0500
commit32ad6cab8d58d99ebd8a28e8fa6e6f4e587cb1e5 (patch)
treeee36d421d704e508d5617d803b4c8cbdb4804fe1 /Hutch/Extensions/KeychainHelper.swift
parent8f2057c53e9009c2529c9c4849c914666c0e4b40 (diff)
downloadhutch-32ad6cab8d58d99ebd8a28e8fa6e6f4e587cb1e5.tar.gz
hutch-32ad6cab8d58d99ebd8a28e8fa6e6f4e587cb1e5.tar.bz2
hutch-32ad6cab8d58d99ebd8a28e8fa6e6f4e587cb1e5.zip
v1.0
Diffstat (limited to 'Hutch/Extensions/KeychainHelper.swift')
-rw-r--r--Hutch/Extensions/KeychainHelper.swift103
1 files changed, 103 insertions, 0 deletions
diff --git a/Hutch/Extensions/KeychainHelper.swift b/Hutch/Extensions/KeychainHelper.swift
new file mode 100644
index 0000000..01f8e5a
--- /dev/null
+++ b/Hutch/Extensions/KeychainHelper.swift
@@ -0,0 +1,103 @@
+import Foundation
+@preconcurrency import Security
+
+enum KeychainHelper: Sendable {
+
+ private static let service = "net.cleberg.Hutch"
+ private static let tokenAccount = "srht-access-token"
+
+ // MARK: - Save
+
+ static func saveToken(_ token: String) throws {
+ guard let data = token.data(using: .utf8) else {
+ throw KeychainError.encodingFailed
+ }
+
+ // Delete any existing item first
+ let deleteQuery: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: service,
+ kSecAttrAccount as String: tokenAccount
+ ]
+ SecItemDelete(deleteQuery as CFDictionary)
+
+ let addQuery: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: service,
+ kSecAttrAccount as String: tokenAccount,
+ kSecValueData as String: data,
+ kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
+ ]
+
+ let status = SecItemAdd(addQuery as CFDictionary, nil)
+ guard status == errSecSuccess else {
+ throw KeychainError.saveFailed(status)
+ }
+ }
+
+ // MARK: - Load
+
+ static func loadToken() -> String? {
+ let query: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: service,
+ kSecAttrAccount as String: tokenAccount,
+ kSecReturnData as String: true,
+ kSecMatchLimit as String: kSecMatchLimitOne
+ ]
+
+ var result: AnyObject?
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
+
+ guard status == errSecSuccess,
+ let data = result as? Data,
+ let token = String(data: data, encoding: .utf8) else {
+ return nil
+ }
+ return token
+ }
+
+ // MARK: - Delete
+
+ static func deleteToken() throws {
+ let query: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: service,
+ kSecAttrAccount as String: tokenAccount
+ ]
+
+ let status = SecItemDelete(query as CFDictionary)
+ guard status == errSecSuccess || status == errSecItemNotFound else {
+ throw KeychainError.deleteFailed(status)
+ }
+ }
+
+ static func deleteAll() throws {
+ let query: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: service
+ ]
+
+ let status = SecItemDelete(query as CFDictionary)
+ guard status == errSecSuccess || status == errSecItemNotFound else {
+ throw KeychainError.deleteFailed(status)
+ }
+ }
+}
+
+enum KeychainError: LocalizedError {
+ case encodingFailed
+ case saveFailed(OSStatus)
+ case deleteFailed(OSStatus)
+
+ var errorDescription: String? {
+ switch self {
+ case .encodingFailed:
+ "Failed to encode token data."
+ case .saveFailed(let status):
+ "Keychain save failed with status \(status)."
+ case .deleteFailed(let status):
+ "Keychain delete failed with status \(status)."
+ }
+ }
+}