krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

v2.5.1: 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}