127 lines
4.0 KiB
Swift
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
|
|
}
|
|
}
|