diff options
| author | Christian Cleberg <[email protected]> | 2026-03-17 23:19:43 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-03-17 23:19:43 -0500 |
| commit | 32ad6cab8d58d99ebd8a28e8fa6e6f4e587cb1e5 (patch) | |
| tree | ee36d421d704e508d5617d803b4c8cbdb4804fe1 /Hutch/Extensions/KeychainHelper.swift | |
| parent | 8f2057c53e9009c2529c9c4849c914666c0e4b40 (diff) | |
| download | hutch-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.swift | 103 |
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)." + } + } +} |
