keys-for-all/docs/components/KeyStorage.md
2025-07-22 18:27:21 -07:00

12 KiB

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

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<T: Codable>(_ data: T) throws -> Data {
        let jsonData = try JSONEncoder().encode(data)
        let sealedBox = try AES.GCM.seal(jsonData, using: key)
        return sealedBox.combined!
    }
    
    func decrypt<T: Codable>(_ 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