Add ExternalKeyMaterialProvider, Constants, keyMaterial, StorageKeys (+3 more)
This commit is contained in:
parent
ecae974f4a
commit
3efb77bc32
@ -44,7 +44,6 @@ SecureStorgageSample/
|
||||
├── Models/
|
||||
│ ├── Credential.swift
|
||||
│ └── SampleLocationData.swift
|
||||
├── StorageKeys.swift # 12 example key definitions
|
||||
├── StorageKeys/
|
||||
│ ├── UserDefaults/
|
||||
│ ├── Keychain/
|
||||
@ -53,6 +52,7 @@ SecureStorgageSample/
|
||||
│ └── Platform/
|
||||
├── WatchOptimized.swift # Watch data models
|
||||
├── Services/
|
||||
│ ├── ExternalKeyMaterialProvider.swift
|
||||
│ └── WatchConnectivityService.swift
|
||||
└── Views/
|
||||
├── UserDefaultsDemo.swift
|
||||
@ -96,6 +96,7 @@ The app demonstrates various storage configurations:
|
||||
- AES-256-GCM or ChaCha20-Poly1305 encryption
|
||||
- PBKDF2 or HKDF key derivation
|
||||
- Complete file protection
|
||||
- External key material example via `KeyMaterialProviding`
|
||||
|
||||
### Platform & Sync
|
||||
- Platform availability (phoneOnly, watchOnly, all)
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
import Foundation
|
||||
import LocalData
|
||||
|
||||
nonisolated enum SampleKeyMaterialSources {
|
||||
nonisolated static let external = KeyMaterialSource(id: "sample.external.key")
|
||||
}
|
||||
@ -6,11 +6,18 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import LocalData
|
||||
|
||||
@main
|
||||
struct SecureStorgageSampleApp: App {
|
||||
init() {
|
||||
_ = WatchConnectivityService.shared
|
||||
Task {
|
||||
await EncryptionHelper.shared.registerKeyMaterialProvider(
|
||||
ExternalKeyMaterialProvider(),
|
||||
for: SampleKeyMaterialSources.external
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
import Foundation
|
||||
import LocalData
|
||||
import Security
|
||||
|
||||
nonisolated
|
||||
struct ExternalKeyMaterialProvider: KeyMaterialProviding {
|
||||
private enum Constants {
|
||||
static let service = "com.example.securestorage.externalkey"
|
||||
static let keyLength = 32
|
||||
}
|
||||
|
||||
func keyMaterial(for keyName: String) async throws -> Data {
|
||||
if let existing = try await KeychainHelper.shared.get(
|
||||
service: Constants.service,
|
||||
key: keyName
|
||||
) {
|
||||
return existing
|
||||
}
|
||||
|
||||
var bytes = [UInt8](repeating: 0, count: Constants.keyLength)
|
||||
let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
|
||||
guard status == errSecSuccess else {
|
||||
throw StorageError.securityApplicationFailed
|
||||
}
|
||||
|
||||
let material = Data(bytes)
|
||||
try await KeychainHelper.shared.set(
|
||||
material,
|
||||
service: Constants.service,
|
||||
key: keyName,
|
||||
accessibility: .afterFirstUnlock,
|
||||
accessControl: nil
|
||||
)
|
||||
|
||||
return material
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import Foundation
|
||||
import LocalData
|
||||
|
||||
extension StorageKeys {
|
||||
/// Stores session logs with encryption using external key material.
|
||||
struct ExternalSessionLogsKey: StorageKey {
|
||||
typealias Value = [String]
|
||||
|
||||
let name = "external_session_logs.json"
|
||||
let domain: StorageDomain = .encryptedFileSystem(directory: .caches)
|
||||
let security: SecurityPolicy = .encrypted(
|
||||
.external(source: SampleKeyMaterialSources.external)
|
||||
)
|
||||
let serializer: Serializer<[String]> = .json
|
||||
let owner = "SampleApp"
|
||||
let availability: PlatformAvailability = .phoneOnly
|
||||
let syncPolicy: SyncPolicy = .never
|
||||
}
|
||||
}
|
||||
@ -14,12 +14,13 @@ struct EncryptedStorageDemo: View {
|
||||
@State private var statusMessage = ""
|
||||
@State private var isLoading = false
|
||||
@State private var iterations = 10_000
|
||||
@State private var useExternalKeyProvider = false
|
||||
@FocusState private var isFieldFocused: Bool
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
Text("Encrypted file storage uses AES-256-GCM encryption with PBKDF2 key derivation. Data is encrypted before being written to disk with complete file protection.")
|
||||
Text("Encrypted file storage uses AES-256-GCM encryption with PBKDF2 key derivation. You can also switch to an external key provider that derives keys via HKDF. Data is encrypted before being written to disk with complete file protection.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
@ -28,13 +29,21 @@ struct EncryptedStorageDemo: View {
|
||||
TextField("Enter log message", text: $logEntry, axis: .vertical)
|
||||
.lineLimit(3, reservesSpace: true)
|
||||
.focused($isFieldFocused)
|
||||
|
||||
Toggle("Use External Key Provider", isOn: $useExternalKeyProvider)
|
||||
|
||||
Stepper("PBKDF2 Iterations: \(iterations)", value: $iterations, in: 1000...100000, step: 1000)
|
||||
.font(.caption)
|
||||
|
||||
Text("Higher iterations = more secure but slower")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
if !useExternalKeyProvider {
|
||||
Stepper("PBKDF2 Iterations: \(iterations)", value: $iterations, in: 1000...100000, step: 1000)
|
||||
.font(.caption)
|
||||
|
||||
Text("Higher iterations = more secure but slower")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
Text("Key material is resolved from a registered provider.")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Section("Actions") {
|
||||
@ -86,16 +95,20 @@ struct EncryptedStorageDemo: View {
|
||||
}
|
||||
|
||||
Section("Encryption Details") {
|
||||
LabeledContent("Algorithm", value: "AES-256-GCM")
|
||||
LabeledContent("Key Derivation", value: "PBKDF2-SHA256")
|
||||
LabeledContent("Iterations", value: "\(iterations)")
|
||||
LabeledContent("Algorithm", value: useExternalKeyProvider ? "ChaCha20-Poly1305" : "AES-256-GCM")
|
||||
LabeledContent("Key Derivation", value: useExternalKeyProvider ? "HKDF-SHA256" : "PBKDF2-SHA256")
|
||||
if !useExternalKeyProvider {
|
||||
LabeledContent("Iterations", value: "\(iterations)")
|
||||
} else {
|
||||
LabeledContent("Key Source", value: "External Provider")
|
||||
}
|
||||
LabeledContent("File Protection", value: "Complete")
|
||||
}
|
||||
|
||||
Section("Key Configuration") {
|
||||
LabeledContent("Domain", value: "Encrypted File System")
|
||||
LabeledContent("Directory", value: "Caches")
|
||||
LabeledContent("Security", value: "AES-256 Encrypted")
|
||||
LabeledContent("Security", value: useExternalKeyProvider ? "External Key Provider" : "AES-256 Encrypted")
|
||||
LabeledContent("Platform", value: "Phone Only")
|
||||
}
|
||||
}
|
||||
@ -115,24 +128,18 @@ struct EncryptedStorageDemo: View {
|
||||
isLoading = true
|
||||
Task {
|
||||
do {
|
||||
let key = StorageKeys.SessionLogsKey(iterations: iterations)
|
||||
|
||||
// Load existing logs
|
||||
var logs: [String]
|
||||
do {
|
||||
logs = try await StorageRouter.shared.get(key)
|
||||
} catch StorageError.notFound {
|
||||
logs = []
|
||||
if useExternalKeyProvider {
|
||||
let key = StorageKeys.ExternalSessionLogsKey()
|
||||
let logs = try await updatedLogs(for: key)
|
||||
try await StorageRouter.shared.set(logs, for: key)
|
||||
storedLogs = logs
|
||||
} else {
|
||||
let key = StorageKeys.SessionLogsKey(iterations: iterations)
|
||||
let logs = try await updatedLogs(for: key)
|
||||
try await StorageRouter.shared.set(logs, for: key)
|
||||
storedLogs = logs
|
||||
}
|
||||
|
||||
// Add new entry with timestamp
|
||||
let timestamp = Date().formatted(date: .abbreviated, time: .standard)
|
||||
logs.append("[\(timestamp)] \(logEntry)")
|
||||
|
||||
// Save encrypted
|
||||
try await StorageRouter.shared.set(logs, for: key)
|
||||
|
||||
storedLogs = logs
|
||||
logEntry = ""
|
||||
statusMessage = "✓ Entry encrypted and saved"
|
||||
} catch {
|
||||
@ -146,8 +153,13 @@ struct EncryptedStorageDemo: View {
|
||||
isLoading = true
|
||||
Task {
|
||||
do {
|
||||
let key = StorageKeys.SessionLogsKey(iterations: iterations)
|
||||
storedLogs = try await StorageRouter.shared.get(key)
|
||||
if useExternalKeyProvider {
|
||||
let key = StorageKeys.ExternalSessionLogsKey()
|
||||
storedLogs = try await StorageRouter.shared.get(key)
|
||||
} else {
|
||||
let key = StorageKeys.SessionLogsKey(iterations: iterations)
|
||||
storedLogs = try await StorageRouter.shared.get(key)
|
||||
}
|
||||
statusMessage = "✓ Decrypted \(storedLogs.count) log entries"
|
||||
} catch StorageError.notFound {
|
||||
storedLogs = []
|
||||
@ -163,8 +175,13 @@ struct EncryptedStorageDemo: View {
|
||||
isLoading = true
|
||||
Task {
|
||||
do {
|
||||
let key = StorageKeys.SessionLogsKey(iterations: iterations)
|
||||
try await StorageRouter.shared.remove(key)
|
||||
if useExternalKeyProvider {
|
||||
let key = StorageKeys.ExternalSessionLogsKey()
|
||||
try await StorageRouter.shared.remove(key)
|
||||
} else {
|
||||
let key = StorageKeys.SessionLogsKey(iterations: iterations)
|
||||
try await StorageRouter.shared.remove(key)
|
||||
}
|
||||
storedLogs = []
|
||||
statusMessage = "✓ Encrypted logs cleared"
|
||||
} catch {
|
||||
@ -173,6 +190,19 @@ struct EncryptedStorageDemo: View {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
private func updatedLogs<Key: StorageKey>(for key: Key) async throws -> [String] where Key.Value == [String] {
|
||||
var logs: [String]
|
||||
do {
|
||||
logs = try await StorageRouter.shared.get(key)
|
||||
} catch StorageError.notFound {
|
||||
logs = []
|
||||
}
|
||||
|
||||
let timestamp = Date().formatted(date: .abbreviated, time: .standard)
|
||||
logs.append("[\(timestamp)] \(logEntry)")
|
||||
return logs
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user