import Foundation import Security enum ServerListStore { static var appSupportURL: URL { let url = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! return url.appendingPathComponent("VoiceCat", isDirectory: true) } static var serversURL: URL { appSupportURL.appendingPathComponent("servers.json") } static var tofuStorePath: String { appSupportURL.appendingPathComponent("tofu_pins.txt").path } static func load() -> [SavedServer] { try? FileManager.default.createDirectory(at: appSupportURL, withIntermediateDirectories: true) guard let data = try? Data(contentsOf: serversURL), let list = try? JSONDecoder().decode([SavedServer].self, from: data) else { return [] } return list } static func save(_ servers: [SavedServer]) { try? FileManager.default.createDirectory(at: appSupportURL, withIntermediateDirectories: true) if let data = try? JSONEncoder().encode(servers) { try? data.write(to: serversURL, options: .atomic) } } // MARK: - Keychain static func savePassword(_ password: String, tag: String) { guard let data = password.data(using: .utf8) else { return } deletePassword(tag: tag) let query: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrService: "cat.voice.VoiceCatMac", kSecAttrAccount: tag, kSecValueData: data, ] SecItemAdd(query as CFDictionary, nil) } static func loadPassword(tag: String) -> String? { let query: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrService: "cat.voice.VoiceCatMac", kSecAttrAccount: tag, kSecReturnData: true, kSecMatchLimit: kSecMatchLimitOne, ] var result: AnyObject? guard SecItemCopyMatching(query as CFDictionary, &result) == errSecSuccess, let data = result as? Data, let password = String(data: data, encoding: .utf8) else { return nil } return password } static func deletePassword(tag: String) { let query: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrService: "cat.voice.VoiceCatMac", kSecAttrAccount: tag, ] SecItemDelete(query as CFDictionary) } }