import Foundation #if os(iOS) import Security #endif /// Small generic Keychain wrapper (iOS) with a UserDefaults fallback on other /// platforms (macOS test target), namespaced by `service`+`key`. enum KeychainStore { static func save(_ value: String, service: String, key: String) { #if os(iOS) guard let data = value.data(using: .utf8) else { return } let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: key ] SecItemDelete(query as CFDictionary) var attributes = query attributes[kSecValueData as String] = data SecItemAdd(attributes as CFDictionary, nil) #else UserDefaults.standard.set(value, forKey: "\(service).\(key)") #endif } static func load(service: String, key: String) -> String? { #if os(iOS) let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, 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 value = String(data: data, encoding: .utf8) else { return nil } return value #else UserDefaults.standard.string(forKey: "\(service).\(key)") #endif } static func delete(service: String, key: String) { #if os(iOS) let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: key ] SecItemDelete(query as CFDictionary) #else UserDefaults.standard.removeObject(forKey: "\(service).\(key)") #endif } }