grammargem/Sources/GrammaGem/Licensing/DeviceFingerprint.swift
BigMarc a7bf45b466 Add management window; sales-focused README; FounderGem LLC
- Management app: sidebar window with Dashboard (usage insights), Devices
  (slot usage, deactivate this Mac), Writing Modes (custom + app-aware map),
  Dictionary, Snippets, Shortcuts, AI Model, Privacy, General, License, About.
- New local stores: Preferences (launch-at-login, appearance), UsageStats
  (privacy-friendly local insights), CustomModesStore, SnippetStore.
- License model now tracks device name + activation usage/limit for the
  Devices screen.
- README rewritten as a conversion-focused product page (no internal cost/setup).
- Legal entity set to FounderGem LLC across README, LICENSE, Info.plist, About.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 14:28:23 +02:00

48 lines
1.8 KiB
Swift

import Foundation
import IOKit
import CryptoKit
/// Derives a stable, privacy-preserving device identifier.
///
/// Lemon Squeezy has no hardware fingerprinting of its own, so we build one
/// (spec §6): take the hardware `IOPlatformUUID`, SHA-256 it, and pass the hash
/// as the `instance_name` on activation. Same Mac → same instance → a reinstall
/// doesn't consume a second device slot. The raw UUID never leaves the device;
/// only its hash is sent.
enum DeviceFingerprint {
/// SHA-256 of the hardware platform UUID, hex-encoded. Falls back to a
/// persisted random ID if IOKit is somehow unavailable.
static func stableID() -> String {
if let uuid = platformUUID() {
let digest = SHA256.hash(data: Data(uuid.utf8))
return digest.map { String(format: "%02x", $0) }.joined()
}
return fallbackID()
}
private static func platformUUID() -> String? {
let service = IOServiceGetMatchingService(
kIOMainPortDefault, IOServiceMatching("IOPlatformExpertDevice"))
guard service != 0 else { return nil }
defer { IOObjectRelease(service) }
guard let cf = IORegistryEntryCreateCFProperty(
service, kIOPlatformUUIDKey as CFString, kCFAllocatorDefault, 0)
else { return nil }
return cf.takeRetainedValue() as? String
}
/// A friendly, human-readable name for this Mac (for the Devices screen).
static func deviceName() -> String {
Host.current().localizedName ?? ProcessInfo.processInfo.hostName
}
private static func fallbackID() -> String {
let key = "GrammaGem.fallbackDeviceID"
if let existing = UserDefaults.standard.string(forKey: key) { return existing }
let id = UUID().uuidString
UserDefaults.standard.set(id, forKey: key)
return id
}
}