import Foundation import Security final class LicenseStore { private let keychainService = "com.xuqm.license" private let defaults: UserDefaults init() { defaults = UserDefaults(suiteName: "xuqm_license") ?? .standard } var token: String? { get { keychainGet("token") } set { keychainSet("token", value: newValue) } } var deviceId: String? { get { keychainGet("deviceId") } set { keychainSet("deviceId", value: newValue) } } var status: String? { get { defaults.string(forKey: "status") } set { defaults.set(newValue, forKey: "status") } } // Stored as Double (milliseconds since epoch) to match Android's currentTimeMillis var statusTime: Double { get { defaults.double(forKey: "statusTime") } set { defaults.set(newValue, forKey: "statusTime") } } var appKey: String? { get { defaults.string(forKey: "appKey") } set { defaults.set(newValue, forKey: "appKey") } } func clear() { token = nil deviceId = nil status = nil statusTime = 0 appKey = nil } private func keychainGet(_ key: String) -> String? { let query: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrService: keychainService, kSecAttrAccount: key, kSecReturnData: true, kSecMatchLimit: kSecMatchLimitOne, ] var result: AnyObject? guard SecItemCopyMatching(query as CFDictionary, &result) == errSecSuccess, let data = result as? Data else { return nil } return String(data: data, encoding: .utf8) } private func keychainSet(_ key: String, value: String?) { guard let value else { keychainDelete(key); return } let data = Data(value.utf8) let query: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrService: keychainService, kSecAttrAccount: key, ] if SecItemCopyMatching(query as CFDictionary, nil) == errSecSuccess { SecItemUpdate(query as CFDictionary, [kSecValueData: data] as CFDictionary) } else { var item = query item[kSecValueData] = data SecItemAdd(item as CFDictionary, nil) } } private func keychainDelete(_ key: String) { let query: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrService: keychainService, kSecAttrAccount: key, ] SecItemDelete(query as CFDictionary) } }