medmap-ios/medmap/Utilities/KeychainManager.swift
2026-08-27 23:26:14 +01:00

127 lines
4.0 KiB
Swift

//
// KeychainManager.swift
// FluxRead
//
// Created by RSA on 13/01/2026.
//
import Foundation
import Security
class KeychainManager {
static let shared = KeychainManager()
private let service = "com.fluxread.devicetoken"
private let account = "devUID"
private init() {}
/// Save device token to Keychain
func saveDeviceToken(_ token: String) throws {
guard let data = token.data(using: .utf8) else {
throw APIError.custom("Failed to encode token")
}
// Delete existing token first
try? deleteDeviceToken()
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
]
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
throw APIError.custom("Failed to save token to Keychain: \(status)")
}
}
/// Retrieve device token from Keychain
func getDeviceToken() -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecReturnData as String: true
]
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
}
/// Delete device token from Keychain
func deleteDeviceToken() throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account
]
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw APIError.custom("Failed to delete token from Keychain: \(status)")
}
}
// MARK: - APNs Push Token
private let pushTokenAccount = "apnsPushToken"
/// Persist the hex-encoded APNs push token
func savePushToken(_ hexToken: String) throws {
guard let data = hexToken.data(using: .utf8) else {
throw APIError.custom("Failed to encode push token")
}
// Upsert: delete then add
let deleteQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: pushTokenAccount
]
SecItemDelete(deleteQuery as CFDictionary)
let addQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: pushTokenAccount,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
]
let status = SecItemAdd(addQuery as CFDictionary, nil)
guard status == errSecSuccess else {
throw APIError.custom("Failed to save push token: \(status)")
}
}
/// Retrieve the previously stored APNs push token, or nil if not set
func getPushToken() -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: pushTokenAccount,
kSecReturnData as String: true
]
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
}
}