# KeyStorage Component The KeyStorage component handles secure storage of license keys and related data using Keychain for sensitive data and UserDefaults for non-sensitive metadata. It implements the secure storage architecture from the main system design with encryption and privacy-first approaches. ## Pseudocode ```swift class KeyStorage { // Storage keys private let keychainService = "com.voiceuwu.keys" private let currentKeyIdentifier = "current.license.key" private let inventoryIdentifier = "key.inventory" private let metadataKey = "com.voiceuwu.license.metadata" private let historyKey = "com.voiceuwu.key.history" // Dependencies private let keychain = KeychainWrapper() private let userDefaults = UserDefaults.standard private let encryption = AES256Encryption() // MARK: - Current License Storage func storeKey(_ key: String, level: LicenseLevel) throws { // Step 1: Create key data structure let keyData = KeyData( key: key, level: level, activatedDate: Date(), deviceID: getDeviceIdentifier() ) // Step 2: Encrypt key data let encrypted = try encryption.encrypt(keyData) // Step 3: Store in Keychain with maximum security try keychain.store( data: encrypted, service: keychainService, account: currentKeyIdentifier, accessLevel: .whenUnlockedThisDeviceOnly ) // Step 2: Store metadata in UserDefaults (non-sensitive) let metadata = LicenseMetadata( level: level, activatedDate: Date(), keyHash: hashKey(key) // Store hash, not actual key ) userDefaults.set( try JSONEncoder().encode(metadata), forKey: metadataKey ) // Step 3: Add to activation history addToHistory(keyHash: metadata.keyHash, date: Date()) } func loadCurrentLicense() -> (key: String, level: LicenseLevel)? { // Load key from Keychain guard let keyData = try? keychain.load( service: keychainService, account: currentKeyIdentifier ), let key = String(data: keyData, encoding: .utf8) else { return nil } // Load metadata from UserDefaults guard let metadataData = userDefaults.data(forKey: metadataKey), let metadata = try? JSONDecoder().decode( LicenseMetadata.self, from: metadataData ) else { return nil } return (key, metadata.level) } func clearCurrentLicense() throws { // Remove from Keychain try keychain.delete( service: keychainService, account: currentKeyIdentifier ) // Remove from UserDefaults userDefaults.removeObject(forKey: metadataKey) } // MARK: - Key Inventory Storage func saveInventory(_ keys: [String]) { // Store inventory in Keychain as it contains actual keys let inventoryData = try! JSONEncoder().encode(keys) try? keychain.store( data: inventoryData, service: keychainService, account: inventoryIdentifier, accessLevel: .whenUnlockedThisDeviceOnly ) } func loadInventory() -> [String] { guard let data = try? keychain.load( service: keychainService, account: inventoryIdentifier ), let keys = try? JSONDecoder().decode([String].self, from: data) else { return [] } return keys } // MARK: - Key Usage Tracking func isKeyUsed(_ key: String) -> Bool { let keyHash = hashKey(key) let history = loadHistory() return history.contains { $0.keyHash == keyHash } } private func addToHistory(keyHash: String, date: Date) { var history = loadHistory() let entry = KeyHistoryEntry( keyHash: keyHash, activatedDate: date ) history.append(entry) // Keep only last 100 entries if history.count > 100 { history = Array(history.suffix(100)) } saveHistory(history) } private func loadHistory() -> [KeyHistoryEntry] { guard let data = userDefaults.data(forKey: historyKey), let history = try? JSONDecoder().decode( [KeyHistoryEntry].self, from: data ) else { return [] } return history } private func saveHistory(_ history: [KeyHistoryEntry]) { let data = try! JSONEncoder().encode(history) userDefaults.set(data, forKey: historyKey) } // MARK: - Helper Methods private func hashKey(_ key: String) -> String { // One-way hash for privacy let data = key.data(using: .utf8)! let hash = SHA256.hash(data: data) return hash.compactMap { String(format: "%02x", $0) } .joined() .prefix(16) .uppercased() } private func getDeviceIdentifier() -> String { // Generate consistent device identifier if let existing = UserDefaults.standard.string(forKey: "device.identifier") { return existing } let identifier = UUID().uuidString UserDefaults.standard.set(identifier, forKey: "device.identifier") return identifier } private func generateIdentifier(for key: String) -> String { // Use SHA256 to create consistent identifier let data = Data(key.utf8) let hash = SHA256.hash(data: data) return hash.compactMap { String(format: "%02x", $0) }.joined() } } // MARK: - Supporting Types struct LicenseMetadata: Codable { let level: LicenseLevel let activatedDate: Date let keyHash: String } struct KeyData: Codable { let key: String let level: LicenseLevel let activatedDate: Date let deviceID: String } struct KeyHistoryEntry: Codable { let keyHash: String let activatedDate: Date } // MARK: - Keychain Wrapper class KeychainWrapper { enum AccessLevel { case whenUnlocked case whenUnlockedThisDeviceOnly case afterFirstUnlock var secAttrAccessible: CFString { switch self { case .whenUnlocked: return kSecAttrAccessibleWhenUnlocked case .whenUnlockedThisDeviceOnly: return kSecAttrAccessibleWhenUnlockedThisDeviceOnly case .afterFirstUnlock: return kSecAttrAccessibleAfterFirstUnlock } } } func store(data: Data, service: String, account: String, accessLevel: AccessLevel) throws { let query: [String: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrService: service, kSecAttrAccount: account, kSecValueData: data, kSecAttrAccessible: accessLevel.secAttrAccessible ] // Delete any existing item SecItemDelete(query as CFDictionary) // Add new item let status = SecItemAdd(query as CFDictionary, nil) if status != errSecSuccess { throw KeychainError.storeFailed(status) } } func load(service: String, account: String) throws -> Data? { let query: [String: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrService: service, kSecAttrAccount: account, kSecReturnData: true, kSecMatchLimit: kSecMatchLimitOne ] var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) if status == errSecItemNotFound { return nil } if status != errSecSuccess { throw KeychainError.loadFailed(status) } return result as? Data } func delete(service: String, account: String) throws { let query: [String: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrService: service, kSecAttrAccount: account ] let status = SecItemDelete(query as CFDictionary) if status != errSecSuccess && status != errSecItemNotFound { throw KeychainError.deleteFailed(status) } } } enum KeychainError: Error { case storeFailed(OSStatus) case loadFailed(OSStatus) case deleteFailed(OSStatus) } // MARK: - AES256 Encryption class AES256Encryption { private let key: SymmetricKey init() { // Generate or load encryption key if let existingKey = loadOrGenerateKey() { self.key = existingKey } else { self.key = SymmetricKey(size: .bits256) saveKey(key) } } func encrypt(_ data: T) throws -> Data { let jsonData = try JSONEncoder().encode(data) let sealedBox = try AES.GCM.seal(jsonData, using: key) return sealedBox.combined! } func decrypt(_ data: Data, as type: T.Type) throws -> T { let sealedBox = try AES.GCM.SealedBox(combined: data) let decryptedData = try AES.GCM.open(sealedBox, using: key) return try JSONDecoder().decode(type, from: decryptedData) } private func loadOrGenerateKey() -> SymmetricKey? { // Load from Keychain if exists let query: [String: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrService: "com.voiceuwu.encryption", kSecAttrAccount: "master.key", kSecReturnData: true ] var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) if status == errSecSuccess, let keyData = result as? Data { return SymmetricKey(data: keyData) } return nil } private func saveKey(_ key: SymmetricKey) { let keyData = key.withUnsafeBytes { Data($0) } let query: [String: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrService: "com.voiceuwu.encryption", kSecAttrAccount: "master.key", kSecValueData: keyData, kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly ] SecItemAdd(query as CFDictionary, nil) } } ``` ## Storage Strategy 1. **Keychain (Secure)** - Encrypted license key data - Key inventory (encrypted) - Encryption keys - Any sensitive data 2. **UserDefaults (Non-sensitive)** - License metadata (level, date) - Key hashes (not actual keys) - Activation history - Device identifiers 3. **Privacy Considerations** - Keys are never logged - AES-256 encryption for all sensitive data - Only hashes stored in history - No cloud sync by default - Device-only access level - Consistent device identifiers 4. **Security Measures** - Separate encryption keys - Constant-time comparisons - Secure key derivation - Protection against timing attacks ## Data Persistence - **Current License**: Survives app deletion if using `.afterFirstUnlock` - **Key Inventory**: Cleared on app deletion - **History**: Limited to 100 entries to prevent growth - **Metadata**: Lightweight JSON encoding