From 3efb77bc329658d39c36a482111ce9f1ceba27a3 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 14 Jan 2026 10:02:01 -0600 Subject: [PATCH] Add ExternalKeyMaterialProvider, Constants, keyMaterial, StorageKeys (+3 more) --- README.md | 3 +- .../Models/SampleKeyMaterialSources.swift | 6 ++ .../SecureStorgageSampleApp.swift | 7 ++ .../ExternalKeyMaterialProvider.swift | 37 ++++++++ .../ExternalSessionLogsKey.swift | 19 ++++ .../Views/EncryptedStorageDemo.swift | 92 ++++++++++++------- 6 files changed, 132 insertions(+), 32 deletions(-) create mode 100644 SecureStorgageSample/Models/SampleKeyMaterialSources.swift create mode 100644 SecureStorgageSample/Services/ExternalKeyMaterialProvider.swift create mode 100644 SecureStorgageSample/StorageKeys/EncryptedFileSystem/ExternalSessionLogsKey.swift diff --git a/README.md b/README.md index 4185551..56d3664 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/SecureStorgageSample/Models/SampleKeyMaterialSources.swift b/SecureStorgageSample/Models/SampleKeyMaterialSources.swift new file mode 100644 index 0000000..5d97551 --- /dev/null +++ b/SecureStorgageSample/Models/SampleKeyMaterialSources.swift @@ -0,0 +1,6 @@ +import Foundation +import LocalData + +nonisolated enum SampleKeyMaterialSources { + nonisolated static let external = KeyMaterialSource(id: "sample.external.key") +} diff --git a/SecureStorgageSample/SecureStorgageSampleApp.swift b/SecureStorgageSample/SecureStorgageSampleApp.swift index f66acc6..3f3fcdc 100644 --- a/SecureStorgageSample/SecureStorgageSampleApp.swift +++ b/SecureStorgageSample/SecureStorgageSampleApp.swift @@ -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 { diff --git a/SecureStorgageSample/Services/ExternalKeyMaterialProvider.swift b/SecureStorgageSample/Services/ExternalKeyMaterialProvider.swift new file mode 100644 index 0000000..70f9156 --- /dev/null +++ b/SecureStorgageSample/Services/ExternalKeyMaterialProvider.swift @@ -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 + } +} diff --git a/SecureStorgageSample/StorageKeys/EncryptedFileSystem/ExternalSessionLogsKey.swift b/SecureStorgageSample/StorageKeys/EncryptedFileSystem/ExternalSessionLogsKey.swift new file mode 100644 index 0000000..48c88ac --- /dev/null +++ b/SecureStorgageSample/StorageKeys/EncryptedFileSystem/ExternalSessionLogsKey.swift @@ -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 + } +} diff --git a/SecureStorgageSample/Views/EncryptedStorageDemo.swift b/SecureStorgageSample/Views/EncryptedStorageDemo.swift index 8363721..1f983f8 100644 --- a/SecureStorgageSample/Views/EncryptedStorageDemo.swift +++ b/SecureStorgageSample/Views/EncryptedStorageDemo.swift @@ -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(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 {