SecureStorageSample/SecureStorageSample/Views/EncryptedStorageDemo.swift
Matt Bruce f4a4f1a527 comments
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
2026-01-17 12:18:05 -06:00

230 lines
8.5 KiB
Swift

//
// EncryptedStorageDemo.swift
// SecureStorageSample
//
// Demonstrates encrypted file storage with LocalData package.
//
import SwiftUI
import LocalData
@MainActor
/// Demonstrates encrypted file storage with PBKDF2 and external key material options.
struct EncryptedStorageDemo: View {
@State private var logEntry = ""
@State private var storedLogs: [String] = []
@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. 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)
}
Section("Add Log Entry") {
TextField("Enter log message", text: $logEntry, axis: .vertical)
.lineLimit(3, reservesSpace: true)
.focused($isFieldFocused)
Toggle("Use External Key Provider", isOn: $useExternalKeyProvider)
if !useExternalKeyProvider {
Stepper("PBKDF2 Iterations: \(iterations)", value: $iterations, in: 1000...100000, step: 1000)
.font(.caption)
IterationWarningView()
.padding(.top, Design.Spacing.xSmall)
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") {
Button(action: addLogEntry) {
HStack {
Image(systemName: "plus.circle.fill")
Text("Add Encrypted Log Entry")
}
}
.disabled(logEntry.isEmpty || isLoading)
Button(action: loadLogs) {
HStack {
Image(systemName: "lock.open.fill")
Text("Decrypt and Load Logs")
}
}
.disabled(isLoading)
Button(action: clearLogs) {
HStack {
Image(systemName: "trash")
Text("Clear All Logs")
}
}
.foregroundStyle(.red)
.disabled(isLoading)
}
if !storedLogs.isEmpty {
Section("Decrypted Logs (\(storedLogs.count))") {
ForEach(Array(storedLogs.enumerated()), id: \.offset) { index, log in
HStack {
Text("\(index + 1).")
.foregroundStyle(.secondary)
Text(log)
}
.font(.system(.caption, design: .monospaced))
}
}
}
if !statusMessage.isEmpty {
Section {
Text(statusMessage)
.font(.caption)
.foregroundStyle(statusMessage.contains("Error") ? .red : .green)
}
}
Section("Encryption Details") {
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: useExternalKeyProvider ? "External Key Provider" : "AES-256 Encrypted")
LabeledContent("Platform", value: "Phone Only")
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button("Done") {
isFieldFocused = false
}
}
}
}
/// Appends an encrypted log entry using the selected key derivation mode.
private func addLogEntry() {
isLoading = true
Task {
do {
if useExternalKeyProvider {
let key = StorageKey.externalSessionLogs
let logs = try await updatedLogs(for: key)
try await StorageRouter.shared.set(logs, for: key)
storedLogs = logs
} else {
let key = StorageKey.sessionLogsKey(iterations: iterations)
let logs = try await updatedLogs(for: key)
try await StorageRouter.shared.set(logs, for: key)
storedLogs = logs
}
logEntry = ""
statusMessage = "✓ Entry encrypted and saved"
} catch {
statusMessage = "Error: \(error.localizedDescription)"
}
isLoading = false
}
}
/// Decrypts and loads log entries into memory for display.
private func loadLogs() {
isLoading = true
Task {
do {
if useExternalKeyProvider {
let key = StorageKey.externalSessionLogs
storedLogs = try await StorageRouter.shared.get(key)
} else {
let key = StorageKey.sessionLogsKey(iterations: iterations)
storedLogs = try await StorageRouter.shared.get(key)
}
statusMessage = "✓ Decrypted \(storedLogs.count) log entries"
} catch StorageError.notFound {
storedLogs = []
statusMessage = "No encrypted logs found"
} catch {
statusMessage = "Error: \(error.localizedDescription)"
}
isLoading = false
}
}
/// Clears the encrypted log file for a clean slate.
private func clearLogs() {
isLoading = true
Task {
do {
if useExternalKeyProvider {
let key = StorageKey.externalSessionLogs
try await StorageRouter.shared.remove(key)
} else {
let key = StorageKey.sessionLogsKey(iterations: iterations)
try await StorageRouter.shared.remove(key)
}
storedLogs = []
statusMessage = "✓ Encrypted logs cleared"
} catch {
statusMessage = "Error: \(error.localizedDescription)"
}
isLoading = false
}
}
/// Loads existing logs, appends the new entry, and returns the updated payload.
private func updatedLogs(for key: StorageKey<[String]>) async throws -> [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 {
NavigationStack {
EncryptedStorageDemo()
}
}
/// Visual reminder that PBKDF2 iterations must remain stable to decrypt existing data.
private struct IterationWarningView: View {
var body: some View {
Text("PBKDF2 iterations must match the value used during encryption. Changing this after saving will prevent decryption.")
.font(.caption2)
.foregroundStyle(Color.Status.warning)
}
}