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

394 lines
No EOL
12 KiB
Markdown

# 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<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