diff --git a/VoiceInk/Services/Obfuscator.swift b/VoiceInk/Services/Obfuscator.swift new file mode 100644 index 0000000..777860e --- /dev/null +++ b/VoiceInk/Services/Obfuscator.swift @@ -0,0 +1,62 @@ +import Foundation +import IOKit + +/// Simple utility to obfuscate sensitive data stored in UserDefaults +struct Obfuscator { + + /// Encodes a string using Base64 with a device-specific salt + static func encode(_ string: String, salt: String) -> String { + let salted = salt + string + salt + let data = Data(salted.utf8) + return data.base64EncodedString() + } + + /// Decodes a Base64 string using a device-specific salt + static func decode(_ base64: String, salt: String) -> String? { + guard let data = Data(base64Encoded: base64), + let salted = String(data: data, encoding: .utf8) else { + return nil + } + + // Remove the salt from both ends + guard salted.hasPrefix(salt), salted.hasSuffix(salt) else { + return nil + } + + return String(salted.dropFirst(salt.count).dropLast(salt.count)) + } + + /// Gets a device-specific identifier to use as salt + /// Uses the same logic as PolarService for consistency + static func getDeviceIdentifier() -> String { + // Try to get Mac serial number first + if let serialNumber = getMacSerialNumber() { + return serialNumber + } + + // Fallback to stored UUID + let defaults = UserDefaults.standard + if let storedId = defaults.string(forKey: "VoiceInkDeviceIdentifier") { + return storedId + } + + // Create and store new UUID + let newId = UUID().uuidString + defaults.set(newId, forKey: "VoiceInkDeviceIdentifier") + return newId + } + + /// Try to get the Mac serial number + private static func getMacSerialNumber() -> String? { + let platformExpert = IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("IOPlatformExpertDevice")) + if platformExpert == 0 { return nil } + + defer { IOObjectRelease(platformExpert) } + + if let serialNumber = IORegistryEntryCreateCFProperty(platformExpert, "IOPlatformSerialNumber" as CFString, kCFAllocatorDefault, 0) { + return (serialNumber.takeRetainedValue() as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) + } + + return nil + } +} diff --git a/VoiceInk/Services/PolarService.swift b/VoiceInk/Services/PolarService.swift index 9ac4bbd..4110768 100644 --- a/VoiceInk/Services/PolarService.swift +++ b/VoiceInk/Services/PolarService.swift @@ -36,37 +36,9 @@ class PolarService { let status: String } - // Generate a unique device identifier + // Generate a unique device identifier using shared logic private func getDeviceIdentifier() -> String { - // Use the macOS serial number or a generated UUID that persists - if let serialNumber = getMacSerialNumber() { - return serialNumber - } - - // Fallback to a stored UUID if we can't get the serial number - let defaults = UserDefaults.standard - if let storedId = defaults.string(forKey: "VoiceInkDeviceIdentifier") { - return storedId - } - - // Create and store a new UUID if none exists - let newId = UUID().uuidString - defaults.set(newId, forKey: "VoiceInkDeviceIdentifier") - return newId - } - - // Try to get the Mac serial number - private func getMacSerialNumber() -> String? { - let platformExpert = IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("IOPlatformExpertDevice")) - if platformExpert == 0 { return nil } - - defer { IOObjectRelease(platformExpert) } - - if let serialNumber = IORegistryEntryCreateCFProperty(platformExpert, "IOPlatformSerialNumber" as CFString, kCFAllocatorDefault, 0) { - return (serialNumber.takeRetainedValue() as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) - } - - return nil + return Obfuscator.getDeviceIdentifier() } // Check if a license key requires activation diff --git a/VoiceInk/Services/UserDefaultsManager.swift b/VoiceInk/Services/UserDefaultsManager.swift index 2c85b64..4315a11 100644 --- a/VoiceInk/Services/UserDefaultsManager.swift +++ b/VoiceInk/Services/UserDefaultsManager.swift @@ -8,6 +8,11 @@ extension UserDefaults { static let audioInputMode = "audioInputMode" static let selectedAudioDeviceUID = "selectedAudioDeviceUID" static let prioritizedDevices = "prioritizedDevices" + + // Obfuscated keys for license-related data + enum License { + static let trialStartDate = "VoiceInkTrialStartDate" + } } // MARK: - AI Provider API Key @@ -22,10 +27,32 @@ extension UserDefaults { set { setValue(newValue, forKey: Keys.licenseKey) } } - // MARK: - Trial Start Date + // MARK: - Trial Start Date (Obfuscated) var trialStartDate: Date? { - get { object(forKey: Keys.trialStartDate) as? Date } - set { setValue(newValue, forKey: Keys.trialStartDate) } + get { + let salt = Obfuscator.getDeviceIdentifier() + let obfuscatedKey = Obfuscator.encode(Keys.License.trialStartDate, salt: salt) + + guard let obfuscatedValue = string(forKey: obfuscatedKey), + let decodedValue = Obfuscator.decode(obfuscatedValue, salt: salt), + let timestamp = Double(decodedValue) else { + return nil + } + + return Date(timeIntervalSince1970: timestamp) + } + set { + let salt = Obfuscator.getDeviceIdentifier() + let obfuscatedKey = Obfuscator.encode(Keys.License.trialStartDate, salt: salt) + + if let date = newValue { + let timestamp = String(date.timeIntervalSince1970) + let obfuscatedValue = Obfuscator.encode(timestamp, salt: salt) + setValue(obfuscatedValue, forKey: obfuscatedKey) + } else { + removeObject(forKey: obfuscatedKey) + } + } } // MARK: - Audio Input Mode