Files
JQC_iOS_App/JanitorialQC/Auth/KeychainHelper.swift
T
2026-05-03 09:29:12 -04:00

83 lines
3.3 KiB
Swift

// Auth/KeychainHelper.swift
// -------------------------
// Thin wrapper around iOS Security framework for storing sensitive data
// (tokens) in the device Keychain.
//
// All methods are marked nonisolated so they can be called from any
// actor context (including APIClient which is an actor) without
// Swift 6 actor-isolation warnings.
// Security framework calls are internally thread-safe.
import Foundation
import Security
import Combine
enum KeychainHelper {
// ── Write ─────────────────────────────────────────────────────────────
@discardableResult
nonisolated static func set(_ value: String, forKey key: String) -> Bool {
guard let data = value.data(using: .utf8) else { return false }
let deleteQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
]
SecItemDelete(deleteQuery as CFDictionary)
let addQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
]
let status = SecItemAdd(addQuery as CFDictionary, nil)
return status == errSecSuccess
}
// ── Read ──────────────────────────────────────────────────────────────
nonisolated static func get(_ key: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
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 string = String(data: data, encoding: .utf8)
else { return nil }
return string
}
// ── Delete ────────────────────────────────────────────────────────────
@discardableResult
nonisolated static func delete(_ key: String) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
]
let status = SecItemDelete(query as CFDictionary)
return status == errSecSuccess || status == errSecItemNotFound
}
// ── Delete All JQC Keys ───────────────────────────────────────────────
nonisolated static func clearAll() {
let keys = [
Constants.Keychain.accessToken,
Constants.Keychain.refreshToken,
Constants.Keychain.userId,
Constants.Keychain.userRole,
Constants.Keychain.username,
Constants.Keychain.displayName,
]
keys.forEach { delete($0) }
}
}