Add ExternalKeyMaterialProvider, Constants, keyMaterial, StorageKeys (+3 more)

This commit is contained in:
Matt Bruce 2026-01-14 10:02:01 -06:00
parent ecae974f4a
commit 3efb77bc32
6 changed files with 132 additions and 32 deletions

View File

@ -44,7 +44,6 @@ SecureStorgageSample/
├── Models/ ├── Models/
│ ├── Credential.swift │ ├── Credential.swift
│ └── SampleLocationData.swift │ └── SampleLocationData.swift
├── StorageKeys.swift # 12 example key definitions
├── StorageKeys/ ├── StorageKeys/
│ ├── UserDefaults/ │ ├── UserDefaults/
│ ├── Keychain/ │ ├── Keychain/
@ -53,6 +52,7 @@ SecureStorgageSample/
│ └── Platform/ │ └── Platform/
├── WatchOptimized.swift # Watch data models ├── WatchOptimized.swift # Watch data models
├── Services/ ├── Services/
│ ├── ExternalKeyMaterialProvider.swift
│ └── WatchConnectivityService.swift │ └── WatchConnectivityService.swift
└── Views/ └── Views/
├── UserDefaultsDemo.swift ├── UserDefaultsDemo.swift
@ -96,6 +96,7 @@ The app demonstrates various storage configurations:
- AES-256-GCM or ChaCha20-Poly1305 encryption - AES-256-GCM or ChaCha20-Poly1305 encryption
- PBKDF2 or HKDF key derivation - PBKDF2 or HKDF key derivation
- Complete file protection - Complete file protection
- External key material example via `KeyMaterialProviding`
### Platform & Sync ### Platform & Sync
- Platform availability (phoneOnly, watchOnly, all) - Platform availability (phoneOnly, watchOnly, all)

View File

@ -0,0 +1,6 @@
import Foundation
import LocalData
nonisolated enum SampleKeyMaterialSources {
nonisolated static let external = KeyMaterialSource(id: "sample.external.key")
}

View File

@ -6,11 +6,18 @@
// //
import SwiftUI import SwiftUI
import LocalData
@main @main
struct SecureStorgageSampleApp: App { struct SecureStorgageSampleApp: App {
init() { init() {
_ = WatchConnectivityService.shared _ = WatchConnectivityService.shared
Task {
await EncryptionHelper.shared.registerKeyMaterialProvider(
ExternalKeyMaterialProvider(),
for: SampleKeyMaterialSources.external
)
}
} }
var body: some Scene { var body: some Scene {

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -14,12 +14,13 @@ struct EncryptedStorageDemo: View {
@State private var statusMessage = "" @State private var statusMessage = ""
@State private var isLoading = false @State private var isLoading = false
@State private var iterations = 10_000 @State private var iterations = 10_000
@State private var useExternalKeyProvider = false
@FocusState private var isFieldFocused: Bool @FocusState private var isFieldFocused: Bool
var body: some View { var body: some View {
Form { Form {
Section { 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) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
@ -29,12 +30,20 @@ struct EncryptedStorageDemo: View {
.lineLimit(3, reservesSpace: true) .lineLimit(3, reservesSpace: true)
.focused($isFieldFocused) .focused($isFieldFocused)
Stepper("PBKDF2 Iterations: \(iterations)", value: $iterations, in: 1000...100000, step: 1000) Toggle("Use External Key Provider", isOn: $useExternalKeyProvider)
.font(.caption)
Text("Higher iterations = more secure but slower") if !useExternalKeyProvider {
.font(.caption2) Stepper("PBKDF2 Iterations: \(iterations)", value: $iterations, in: 1000...100000, step: 1000)
.foregroundStyle(.secondary) .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") { Section("Actions") {
@ -86,16 +95,20 @@ struct EncryptedStorageDemo: View {
} }
Section("Encryption Details") { Section("Encryption Details") {
LabeledContent("Algorithm", value: "AES-256-GCM") LabeledContent("Algorithm", value: useExternalKeyProvider ? "ChaCha20-Poly1305" : "AES-256-GCM")
LabeledContent("Key Derivation", value: "PBKDF2-SHA256") LabeledContent("Key Derivation", value: useExternalKeyProvider ? "HKDF-SHA256" : "PBKDF2-SHA256")
LabeledContent("Iterations", value: "\(iterations)") if !useExternalKeyProvider {
LabeledContent("Iterations", value: "\(iterations)")
} else {
LabeledContent("Key Source", value: "External Provider")
}
LabeledContent("File Protection", value: "Complete") LabeledContent("File Protection", value: "Complete")
} }
Section("Key Configuration") { Section("Key Configuration") {
LabeledContent("Domain", value: "Encrypted File System") LabeledContent("Domain", value: "Encrypted File System")
LabeledContent("Directory", value: "Caches") 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") LabeledContent("Platform", value: "Phone Only")
} }
} }
@ -115,24 +128,18 @@ struct EncryptedStorageDemo: View {
isLoading = true isLoading = true
Task { Task {
do { do {
let key = StorageKeys.SessionLogsKey(iterations: iterations) if useExternalKeyProvider {
let key = StorageKeys.ExternalSessionLogsKey()
// Load existing logs let logs = try await updatedLogs(for: key)
var logs: [String] try await StorageRouter.shared.set(logs, for: key)
do { storedLogs = logs
logs = try await StorageRouter.shared.get(key) } else {
} catch StorageError.notFound { let key = StorageKeys.SessionLogsKey(iterations: iterations)
logs = [] 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 = "" logEntry = ""
statusMessage = "✓ Entry encrypted and saved" statusMessage = "✓ Entry encrypted and saved"
} catch { } catch {
@ -146,8 +153,13 @@ struct EncryptedStorageDemo: View {
isLoading = true isLoading = true
Task { Task {
do { do {
let key = StorageKeys.SessionLogsKey(iterations: iterations) if useExternalKeyProvider {
storedLogs = try await StorageRouter.shared.get(key) 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" statusMessage = "✓ Decrypted \(storedLogs.count) log entries"
} catch StorageError.notFound { } catch StorageError.notFound {
storedLogs = [] storedLogs = []
@ -163,8 +175,13 @@ struct EncryptedStorageDemo: View {
isLoading = true isLoading = true
Task { Task {
do { do {
let key = StorageKeys.SessionLogsKey(iterations: iterations) if useExternalKeyProvider {
try await StorageRouter.shared.remove(key) let key = StorageKeys.ExternalSessionLogsKey()
try await StorageRouter.shared.remove(key)
} else {
let key = StorageKeys.SessionLogsKey(iterations: iterations)
try await StorageRouter.shared.remove(key)
}
storedLogs = [] storedLogs = []
statusMessage = "✓ Encrypted logs cleared" statusMessage = "✓ Encrypted logs cleared"
} catch { } catch {
@ -173,6 +190,19 @@ struct EncryptedStorageDemo: View {
isLoading = false 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 { #Preview {