Add obfuscated trial storage to prevent perpetual trial
This commit is contained in:
parent
560781c7d0
commit
71fdbdfc75
62
VoiceInk/Services/Obfuscator.swift
Normal file
62
VoiceInk/Services/Obfuscator.swift
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -36,37 +36,9 @@ class PolarService {
|
|||||||
let status: String
|
let status: String
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a unique device identifier
|
// Generate a unique device identifier using shared logic
|
||||||
private func getDeviceIdentifier() -> String {
|
private func getDeviceIdentifier() -> String {
|
||||||
// Use the macOS serial number or a generated UUID that persists
|
return Obfuscator.getDeviceIdentifier()
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if a license key requires activation
|
// Check if a license key requires activation
|
||||||
|
|||||||
@ -8,6 +8,11 @@ extension UserDefaults {
|
|||||||
static let audioInputMode = "audioInputMode"
|
static let audioInputMode = "audioInputMode"
|
||||||
static let selectedAudioDeviceUID = "selectedAudioDeviceUID"
|
static let selectedAudioDeviceUID = "selectedAudioDeviceUID"
|
||||||
static let prioritizedDevices = "prioritizedDevices"
|
static let prioritizedDevices = "prioritizedDevices"
|
||||||
|
|
||||||
|
// Obfuscated keys for license-related data
|
||||||
|
enum License {
|
||||||
|
static let trialStartDate = "VoiceInkTrialStartDate"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - AI Provider API Key
|
// MARK: - AI Provider API Key
|
||||||
@ -22,10 +27,32 @@ extension UserDefaults {
|
|||||||
set { setValue(newValue, forKey: Keys.licenseKey) }
|
set { setValue(newValue, forKey: Keys.licenseKey) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Trial Start Date
|
// MARK: - Trial Start Date (Obfuscated)
|
||||||
var trialStartDate: Date? {
|
var trialStartDate: Date? {
|
||||||
get { object(forKey: Keys.trialStartDate) as? Date }
|
get {
|
||||||
set { setValue(newValue, forKey: Keys.trialStartDate) }
|
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
|
// MARK: - Audio Input Mode
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user