394 lines
No EOL
12 KiB
Markdown
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 |