From 2e437dac91546a8b5bd0ffaacb02c64871d9c52f Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Sat, 17 Jan 2026 12:18:05 -0600 Subject: [PATCH] Update SecureStorageSample --- SecureStorageSample/ContentView.swift | 4 ++++ SecureStorageSample/Design/DesignConstants.swift | 3 +++ SecureStorageSample/Models/Credential.swift | 3 ++- SecureStorageSample/Models/ProfileName.swift | 2 ++ .../Models/SampleKeyMaterialSources.swift | 2 ++ SecureStorageSample/Models/SampleLocationData.swift | 3 ++- SecureStorageSample/Models/UnifiedSettings.swift | 2 ++ SecureStorageSample/SecureStorageSampleApp.swift | 7 +++++++ .../Services/AppStorageCatalog.swift | 3 +++ .../Services/ExternalKeyMaterialProvider.swift | 4 ++++ .../Services/WatchConnectivityService.swift | 10 ++++++++++ .../AppGroup/AppGroupUserDefaultsKey.swift | 1 + .../AppGroup/AppGroupUserProfileKey.swift | 2 ++ .../StorageKeys/AppGroup/UserPreferencesKey.swift | 1 + .../ExternalSessionLogsKey.swift | 1 + .../EncryptedFileSystem/PrivateNotesKey.swift | 1 + .../EncryptedFileSystem/SessionLogsKey.swift | 2 ++ .../StorageKeys/FileSystem/CachedDataKey.swift | 1 + .../StorageKeys/FileSystem/CustomEncodedKey.swift | 1 + .../StorageKeys/FileSystem/SettingsPlistKey.swift | 1 + .../StorageKeys/FileSystem/UserProfileFileKey.swift | 2 ++ .../StorageKeys/Keychain/APITokenKey.swift | 1 + .../StorageKeys/Keychain/CredentialsKey.swift | 2 ++ .../Keychain/ExternalKeyMaterialKey.swift | 1 + .../StorageKeys/Keychain/LastLocationKey.swift | 1 + .../Migration/AggregatingMigrationKeys.swift | 6 +++++- .../Migration/ConditionalMigrationKeys.swift | 1 + .../StorageKeys/Migration/MigrationKeys.swift | 6 ++++-- .../Migration/TransformingMigrationKeys.swift | 3 +++ .../StorageKeys/Platform/SyncableSettingKey.swift | 2 ++ .../StorageKeys/Platform/WatchVibrationKey.swift | 1 + .../StorageKeys/UserDefaults/AppVersionKey.swift | 1 + .../Views/AggregatingMigrationDemo.swift | 4 ++++ .../Views/ConditionalMigrationDemo.swift | 3 +++ .../Views/EncryptedStorageDemo.swift | 6 ++++++ SecureStorageSample/Views/FileSystemDemo.swift | 7 +++++++ SecureStorageSample/Views/KeychainDemo.swift | 4 ++++ SecureStorageSample/Views/MigrationDemo.swift | 5 +++++ SecureStorageSample/Views/MigrationHubView.swift | 1 + SecureStorageSample/Views/PlatformSyncDemo.swift | 12 ++++++++++++ .../Views/TransformingMigrationDemo.swift | 4 ++++ SecureStorageSample/Views/UserDefaultsDemo.swift | 7 +++++++ SecureStorageSample/WatchOptimized.swift | 13 ++++++++----- 43 files changed, 137 insertions(+), 10 deletions(-) diff --git a/SecureStorageSample/ContentView.swift b/SecureStorageSample/ContentView.swift index 89c280e..b9a3096 100644 --- a/SecureStorageSample/ContentView.swift +++ b/SecureStorageSample/ContentView.swift @@ -8,6 +8,7 @@ import SwiftUI import LocalData +/// Defines all demo destinations and provides display metadata for the list. enum DemoDestination: Hashable, CaseIterable { case userDefaults case keychain @@ -16,6 +17,8 @@ enum DemoDestination: Hashable, CaseIterable { case sync case migration + /// Type-erased destination view for NavigationStack routing. + /// The enum keeps navigation data-driven and centralized. var view: some View { switch self { case .userDefaults: @@ -68,6 +71,7 @@ enum DemoDestination: Hashable, CaseIterable { } } +/// Root navigation list that links to each LocalData demo screen. struct ContentView: View { var body: some View { NavigationStack { diff --git a/SecureStorageSample/Design/DesignConstants.swift b/SecureStorageSample/Design/DesignConstants.swift index ac1d86a..73e03d0 100644 --- a/SecureStorageSample/Design/DesignConstants.swift +++ b/SecureStorageSample/Design/DesignConstants.swift @@ -1,5 +1,6 @@ import SwiftUI +/// Design tokens for spacing and common UI colors used in the sample. enum Design { enum Spacing { static let xSmall: CGFloat = 4 @@ -10,6 +11,7 @@ enum Design { } extension Color { + /// Semantic colors used for status messaging in demo screens. enum Status { static let success = Color.green static let info = Color.blue @@ -17,6 +19,7 @@ extension Color { static let error = Color.red } + /// Semantic text colors for secondary labels. enum Text { static let secondary = Color.secondary } diff --git a/SecureStorageSample/Models/Credential.swift b/SecureStorageSample/Models/Credential.swift index d9bcaec..d2877d4 100644 --- a/SecureStorageSample/Models/Credential.swift +++ b/SecureStorageSample/Models/Credential.swift @@ -1,6 +1,7 @@ import Foundation -/// Simple credential model for keychain storage demo. +/// Simple credential model for the Keychain demo. +/// Using `Codable` keeps the storage path identical to more complex real-world credentials. nonisolated struct Credential: Codable, Sendable { let username: String diff --git a/SecureStorageSample/Models/ProfileName.swift b/SecureStorageSample/Models/ProfileName.swift index 3a33ba9..d3945dd 100644 --- a/SecureStorageSample/Models/ProfileName.swift +++ b/SecureStorageSample/Models/ProfileName.swift @@ -1,5 +1,7 @@ import Foundation +/// Structured name model used in migration demos. +/// This replaces a legacy single-string name with distinct components. nonisolated struct ProfileName: Codable, Sendable { let firstName: String let lastName: String diff --git a/SecureStorageSample/Models/SampleKeyMaterialSources.swift b/SecureStorageSample/Models/SampleKeyMaterialSources.swift index 5d97551..e4ccfa4 100644 --- a/SecureStorageSample/Models/SampleKeyMaterialSources.swift +++ b/SecureStorageSample/Models/SampleKeyMaterialSources.swift @@ -1,6 +1,8 @@ import Foundation import LocalData +/// Identifiers for custom key material sources used by the sample. +/// LocalData uses these identifiers to look up registered providers. nonisolated enum SampleKeyMaterialSources { nonisolated static let external = KeyMaterialSource(id: "sample.external.key") } diff --git a/SecureStorageSample/Models/SampleLocationData.swift b/SecureStorageSample/Models/SampleLocationData.swift index 8e7f00a..3ef8e54 100644 --- a/SecureStorageSample/Models/SampleLocationData.swift +++ b/SecureStorageSample/Models/SampleLocationData.swift @@ -1,6 +1,7 @@ import Foundation -/// Location data model. +/// Lightweight location model for the Keychain demo. +/// It shows that LocalData can store structured data even in secure domains. nonisolated struct SampleLocationData: Codable, Sendable { let lat: Double diff --git a/SecureStorageSample/Models/UnifiedSettings.swift b/SecureStorageSample/Models/UnifiedSettings.swift index 4206b8c..2ad51d3 100644 --- a/SecureStorageSample/Models/UnifiedSettings.swift +++ b/SecureStorageSample/Models/UnifiedSettings.swift @@ -1,5 +1,7 @@ import Foundation +/// Aggregated settings model produced by migration demos. +/// It combines multiple legacy settings into a single modern payload. nonisolated struct UnifiedSettings: Codable, Sendable { let notificationsEnabled: Bool let theme: String diff --git a/SecureStorageSample/SecureStorageSampleApp.swift b/SecureStorageSample/SecureStorageSampleApp.swift index 58d9162..f95c2c5 100644 --- a/SecureStorageSample/SecureStorageSampleApp.swift +++ b/SecureStorageSample/SecureStorageSampleApp.swift @@ -10,9 +10,13 @@ import LocalData import SharedKit @main +/// App entry point that centralizes LocalData configuration and storage key registration. +/// Keeping this bootstrap in one place makes the sample's storage behavior easy to audit. struct SecureStorageSampleApp: App { init() { + // Log derived identifiers up front so it's obvious which services the sample uses. StorageServiceIdentifiers.logConfiguration() + // Spin up WCSession early so background sync opportunities aren't missed. _ = WatchConnectivityService.shared Task { // 1. Global Encryption Configuration @@ -47,15 +51,18 @@ struct SecureStorageSampleApp: App { ) await StorageRouter.shared.updateStorageConfiguration(storageConfig) + // Register keys once so LocalData can validate and migrate them consistently. do { try await StorageRouter.shared.registerCatalog(AppStorageCatalog(), migrateImmediately: true) } catch { assertionFailure("Storage catalog registration failed: \(error)") } + // Provide external key material for the encrypted storage demo. await StorageRouter.shared.registerKeyMaterialProvider( ExternalKeyMaterialProvider(), for: SampleKeyMaterialSources.external ) + // If a watch is paired, send the current syncable payloads on launch. await StorageRouter.shared.syncRegisteredKeysIfNeeded() } #if DEBUG diff --git a/SecureStorageSample/Services/AppStorageCatalog.swift b/SecureStorageSample/Services/AppStorageCatalog.swift index 8828a43..392ce2c 100644 --- a/SecureStorageSample/Services/AppStorageCatalog.swift +++ b/SecureStorageSample/Services/AppStorageCatalog.swift @@ -3,8 +3,11 @@ import LocalData import SharedKit +/// Catalog of all storage keys used by the sample app. +/// Registering a catalog allows LocalData to audit keys and run migrations up front. struct AppStorageCatalog: StorageKeyCatalog { var allKeys: [AnyStorageKey] { + // Order here is purely for readability in audit reports. [ .key(.appVersion), .key(.userPreferences), diff --git a/SecureStorageSample/Services/ExternalKeyMaterialProvider.swift b/SecureStorageSample/Services/ExternalKeyMaterialProvider.swift index 530c7e3..5330cec 100644 --- a/SecureStorageSample/Services/ExternalKeyMaterialProvider.swift +++ b/SecureStorageSample/Services/ExternalKeyMaterialProvider.swift @@ -2,18 +2,22 @@ import CryptoKit import Foundation import LocalData +/// Supplies external key material for the encrypted storage demo. +/// The provider persists the generated key material so encryption remains stable across launches. nonisolated struct ExternalKeyMaterialProvider: KeyMaterialProviding { private enum Constants { static let keyLength = 32 } + /// Returns a stable 256-bit key, generating and persisting it on first use. func keyMaterial(for keyName: String) async throws -> Data { let key = StorageKey.externalKeyMaterial if let existing = try? await StorageRouter.shared.get(key) { return existing } + // CryptoKit ensures the material is cryptographically random. let symmetricKey = SymmetricKey(size: .bits256) let material = symmetricKey.withUnsafeBytes { Data($0) } guard material.count == Constants.keyLength else { diff --git a/SecureStorageSample/Services/WatchConnectivityService.swift b/SecureStorageSample/Services/WatchConnectivityService.swift index d8723b6..5a41493 100644 --- a/SecureStorageSample/Services/WatchConnectivityService.swift +++ b/SecureStorageSample/Services/WatchConnectivityService.swift @@ -4,6 +4,8 @@ import SharedKit import WatchConnectivity @MainActor +/// iOS-side WatchConnectivity bridge that keeps the watch app in sync with LocalData. +/// This class focuses on coordination and delegates data payload creation to LocalData. final class WatchConnectivityService: NSObject, WCSessionDelegate { static let shared = WatchConnectivityService() @@ -12,6 +14,7 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate { activateIfSupported() } + /// Activates WCSession only on supported devices (no-ops on unsupported hardware). private func activateIfSupported() { guard WCSession.isSupported() else { return } let session = WCSession.default @@ -29,6 +32,7 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate { } else { Logger.debug("iOS WCSession activated with state: \(activationState.rawValue)") } + // Try to sync any previously registered keys as soon as WCSession is ready. Task { await StorageRouter.shared.syncRegisteredKeysIfNeeded() } @@ -44,6 +48,7 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate { func sessionWatchStateDidChange(_ session: WCSession) { Logger.debug("iOS WCSession watch state changed: paired=\(session.isPaired) installed=\(session.isWatchAppInstalled)") + // A watch install or pairing change is a good time to re-send syncable keys. Task { await StorageRouter.shared.syncRegisteredKeysIfNeeded() } @@ -51,12 +56,14 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate { func sessionReachabilityDidChange(_ session: WCSession) { Logger.debug("iOS WCSession reachability changed: reachable=\(session.isReachable)") + // When reachability flips, attempt to push any pending sync payloads. Task { await StorageRouter.shared.syncRegisteredKeysIfNeeded() } } func session(_ session: WCSession, didReceiveMessage message: [String: Any]) { + // Watch may request a sync without needing a reply (fire-and-forget message). if let request = message[WatchSyncMessageKeys.requestSync] as? Bool, request { Logger.debug("iOS received watch sync request") Task { @@ -74,6 +81,7 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate { didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void ) { + // Reply-based handshake: watch expects a payload immediately if reachable. if let request = message[WatchSyncMessageKeys.requestSync] as? Bool, request { Logger.debug("iOS received watch sync request (reply)") Task { @@ -84,6 +92,7 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate { } } + /// Attempts to build a snapshot payload, retrying briefly in case keys are still initializing. private func buildSyncReplyPayload() async -> [String: Any] { let maxAttempts = 3 for attempt in 1...maxAttempts { @@ -104,6 +113,7 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate { } func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any]) { + // transferUserInfo can deliver requests when the watch was previously unreachable. if let request = userInfo[WatchSyncMessageKeys.requestSync] as? Bool, request { Logger.debug("iOS received queued watch sync request") Task { diff --git a/SecureStorageSample/StorageKeys/AppGroup/AppGroupUserDefaultsKey.swift b/SecureStorageSample/StorageKeys/AppGroup/AppGroupUserDefaultsKey.swift index 90055a0..64f970c 100644 --- a/SecureStorageSample/StorageKeys/AppGroup/AppGroupUserDefaultsKey.swift +++ b/SecureStorageSample/StorageKeys/AppGroup/AppGroupUserDefaultsKey.swift @@ -2,6 +2,7 @@ import Foundation import LocalData import SharedKit +/// App Group UserDefaults key used to demonstrate shared preferences between targets. extension StorageKey where Value == String { /// Stores a shared setting in App Group UserDefaults. /// - Domain: App Group UserDefaults diff --git a/SecureStorageSample/StorageKeys/AppGroup/AppGroupUserProfileKey.swift b/SecureStorageSample/StorageKeys/AppGroup/AppGroupUserProfileKey.swift index 3678bb9..d43b82d 100644 --- a/SecureStorageSample/StorageKeys/AppGroup/AppGroupUserProfileKey.swift +++ b/SecureStorageSample/StorageKeys/AppGroup/AppGroupUserProfileKey.swift @@ -2,6 +2,7 @@ import Foundation import LocalData import SharedKit +/// App Group file key for sharing a user profile across app targets. extension StorageKey where Value == UserProfile { /// Stores a shared user profile in the App Group container. /// - Domain: App Group File System @@ -21,6 +22,7 @@ extension StorageKey where Value == UserProfile { syncPolicy: .never ) + /// Creates a version of the key for a different App Group directory. nonisolated static func appGroupUserProfileKey( directory: FileDirectory = .documents ) -> StorageKey { diff --git a/SecureStorageSample/StorageKeys/AppGroup/UserPreferencesKey.swift b/SecureStorageSample/StorageKeys/AppGroup/UserPreferencesKey.swift index 169e090..67a6495 100644 --- a/SecureStorageSample/StorageKeys/AppGroup/UserPreferencesKey.swift +++ b/SecureStorageSample/StorageKeys/AppGroup/UserPreferencesKey.swift @@ -2,6 +2,7 @@ import Foundation import LocalData import SharedKit +/// App Group UserDefaults key for a generic preferences dictionary. extension StorageKey where Value == [String: AnyCodable] { /// Stores user preferences in App Group UserDefaults. /// - Domain: App Group UserDefaults diff --git a/SecureStorageSample/StorageKeys/EncryptedFileSystem/ExternalSessionLogsKey.swift b/SecureStorageSample/StorageKeys/EncryptedFileSystem/ExternalSessionLogsKey.swift index 1467c30..94d7064 100644 --- a/SecureStorageSample/StorageKeys/EncryptedFileSystem/ExternalSessionLogsKey.swift +++ b/SecureStorageSample/StorageKeys/EncryptedFileSystem/ExternalSessionLogsKey.swift @@ -1,6 +1,7 @@ import Foundation import LocalData +/// Encrypted file system key that pulls key material from a registered provider. extension StorageKey where Value == [String] { /// Stores session logs with encryption using external key material. nonisolated static let externalSessionLogs = StorageKey( diff --git a/SecureStorageSample/StorageKeys/EncryptedFileSystem/PrivateNotesKey.swift b/SecureStorageSample/StorageKeys/EncryptedFileSystem/PrivateNotesKey.swift index af04ec1..c8284ca 100644 --- a/SecureStorageSample/StorageKeys/EncryptedFileSystem/PrivateNotesKey.swift +++ b/SecureStorageSample/StorageKeys/EncryptedFileSystem/PrivateNotesKey.swift @@ -1,6 +1,7 @@ import Foundation import LocalData +/// Encrypted file system key for long-lived private notes in Documents. extension StorageKey where Value == String { /// Stores private notes with encryption. nonisolated static let privateNotes = StorageKey( diff --git a/SecureStorageSample/StorageKeys/EncryptedFileSystem/SessionLogsKey.swift b/SecureStorageSample/StorageKeys/EncryptedFileSystem/SessionLogsKey.swift index db7774c..380b37c 100644 --- a/SecureStorageSample/StorageKeys/EncryptedFileSystem/SessionLogsKey.swift +++ b/SecureStorageSample/StorageKeys/EncryptedFileSystem/SessionLogsKey.swift @@ -1,6 +1,7 @@ import Foundation import LocalData +/// Encrypted file system key for log data with configurable key derivation. extension StorageKey where Value == [String] { /// Stores session logs with full encryption. /// Configurable PBKDF2 iterations. @@ -15,6 +16,7 @@ extension StorageKey where Value == [String] { syncPolicy: .never ) + /// Builds a variant with a custom PBKDF2 iteration count for demo purposes. nonisolated static func sessionLogsKey(iterations: Int) -> StorageKey { StorageKey( name: "session_logs.json", diff --git a/SecureStorageSample/StorageKeys/FileSystem/CachedDataKey.swift b/SecureStorageSample/StorageKeys/FileSystem/CachedDataKey.swift index 1623f3b..643bbee 100644 --- a/SecureStorageSample/StorageKeys/FileSystem/CachedDataKey.swift +++ b/SecureStorageSample/StorageKeys/FileSystem/CachedDataKey.swift @@ -1,6 +1,7 @@ import Foundation import LocalData +/// File system key for non-critical cache data stored in Caches. extension StorageKey where Value == Data { /// Stores cached data files. nonisolated static let cachedData = StorageKey( diff --git a/SecureStorageSample/StorageKeys/FileSystem/CustomEncodedKey.swift b/SecureStorageSample/StorageKeys/FileSystem/CustomEncodedKey.swift index ccc5db0..db7bc55 100644 --- a/SecureStorageSample/StorageKeys/FileSystem/CustomEncodedKey.swift +++ b/SecureStorageSample/StorageKeys/FileSystem/CustomEncodedKey.swift @@ -1,6 +1,7 @@ import Foundation import LocalData +/// File system key that demonstrates custom serializer usage. extension StorageKey where Value == String { /// Example using custom serializer for specialized encoding. nonisolated static let customEncoded = StorageKey( diff --git a/SecureStorageSample/StorageKeys/FileSystem/SettingsPlistKey.swift b/SecureStorageSample/StorageKeys/FileSystem/SettingsPlistKey.swift index 293afbd..90b7195 100644 --- a/SecureStorageSample/StorageKeys/FileSystem/SettingsPlistKey.swift +++ b/SecureStorageSample/StorageKeys/FileSystem/SettingsPlistKey.swift @@ -1,6 +1,7 @@ import Foundation import LocalData +/// File system key that persists a property list export. extension StorageKey where Value == [String: AnyCodable] { /// Stores settings as property list. nonisolated static let settingsPlist = StorageKey( diff --git a/SecureStorageSample/StorageKeys/FileSystem/UserProfileFileKey.swift b/SecureStorageSample/StorageKeys/FileSystem/UserProfileFileKey.swift index 2864881..ba66371 100644 --- a/SecureStorageSample/StorageKeys/FileSystem/UserProfileFileKey.swift +++ b/SecureStorageSample/StorageKeys/FileSystem/UserProfileFileKey.swift @@ -2,6 +2,7 @@ import Foundation import LocalData import SharedKit +/// File system key for the shared UserProfile payload. extension StorageKey where Value == UserProfile { /// Stores user profile as JSON file in documents. nonisolated static let userProfileFile = StorageKey( @@ -15,6 +16,7 @@ extension StorageKey where Value == UserProfile { syncPolicy: .automaticSmall ) + /// Builds a profile key for an alternate file directory. nonisolated static func userProfileFileKey(directory: FileDirectory = .documents) -> StorageKey { StorageKey( name: UserProfile.storageKeyName, diff --git a/SecureStorageSample/StorageKeys/Keychain/APITokenKey.swift b/SecureStorageSample/StorageKeys/Keychain/APITokenKey.swift index 0894d30..c119fc7 100644 --- a/SecureStorageSample/StorageKeys/Keychain/APITokenKey.swift +++ b/SecureStorageSample/StorageKeys/Keychain/APITokenKey.swift @@ -2,6 +2,7 @@ import Foundation import LocalData import SharedKit +/// Keychain key for short-lived API tokens that should not sync off-device. extension StorageKey where Value == String { /// Stores API token in keychain. nonisolated static let apiToken = StorageKey( diff --git a/SecureStorageSample/StorageKeys/Keychain/CredentialsKey.swift b/SecureStorageSample/StorageKeys/Keychain/CredentialsKey.swift index e468996..908363f 100644 --- a/SecureStorageSample/StorageKeys/Keychain/CredentialsKey.swift +++ b/SecureStorageSample/StorageKeys/Keychain/CredentialsKey.swift @@ -2,6 +2,7 @@ import Foundation import LocalData import SharedKit +/// Keychain key used for credentials with optional access control variations. extension StorageKey where Value == Credential { /// Stores user credentials securely in keychain. /// Configurable accessibility and access control. @@ -16,6 +17,7 @@ extension StorageKey where Value == Credential { syncPolicy: .never ) + /// Builds a key with custom Keychain accessibility or access control options. nonisolated static func credentialsKey( accessibility: KeychainAccessibility = .afterFirstUnlock, accessControl: KeychainAccessControl? = nil diff --git a/SecureStorageSample/StorageKeys/Keychain/ExternalKeyMaterialKey.swift b/SecureStorageSample/StorageKeys/Keychain/ExternalKeyMaterialKey.swift index 2dcf9fc..f272974 100644 --- a/SecureStorageSample/StorageKeys/Keychain/ExternalKeyMaterialKey.swift +++ b/SecureStorageSample/StorageKeys/Keychain/ExternalKeyMaterialKey.swift @@ -2,6 +2,7 @@ import Foundation import LocalData import SharedKit +/// Keychain key for externally provided key material used in encryption demos. extension StorageKey where Value == Data { /// Stores external key material used for encryption policies. nonisolated static let externalKeyMaterial = StorageKey( diff --git a/SecureStorageSample/StorageKeys/Keychain/LastLocationKey.swift b/SecureStorageSample/StorageKeys/Keychain/LastLocationKey.swift index 0a867dd..edeea26 100644 --- a/SecureStorageSample/StorageKeys/Keychain/LastLocationKey.swift +++ b/SecureStorageSample/StorageKeys/Keychain/LastLocationKey.swift @@ -2,6 +2,7 @@ import Foundation import LocalData import SharedKit +/// Keychain key for sensitive location data with user presence required. extension StorageKey where Value == SampleLocationData { /// Stores sensitive location data in keychain with biometric protection. nonisolated static let lastLocation = StorageKey( diff --git a/SecureStorageSample/StorageKeys/Migration/AggregatingMigrationKeys.swift b/SecureStorageSample/StorageKeys/Migration/AggregatingMigrationKeys.swift index de595e2..4ab8a07 100644 --- a/SecureStorageSample/StorageKeys/Migration/AggregatingMigrationKeys.swift +++ b/SecureStorageSample/StorageKeys/Migration/AggregatingMigrationKeys.swift @@ -1,6 +1,7 @@ import Foundation import LocalData +/// Legacy notification toggle stored in UserDefaults. extension StorageKey where Value == Bool { nonisolated static let legacyNotificationSetting = StorageKey( name: "legacy_notification_setting", @@ -14,6 +15,7 @@ extension StorageKey where Value == Bool { ) } +/// Legacy theme preference stored separately from notifications. extension StorageKey where Value == String { nonisolated static let legacyThemeSetting = StorageKey( name: "legacy_theme_setting", @@ -27,8 +29,9 @@ extension StorageKey where Value == String { ) } +/// Modern unified settings created by aggregating multiple legacy keys. extension StorageKey where Value == UnifiedSettings { - + nonisolated static let modernUnifiedSettings = StorageKey( name: "modern_unified_settings", domain: .fileSystem(directory: .documents), @@ -48,6 +51,7 @@ extension StorageKey where Value == UnifiedSettings { destinationKey: key, sourceKeys: sources ) { sources in + // Merge legacy values into a single strongly-typed settings model. var notificationsEnabled = false var theme = "system" diff --git a/SecureStorageSample/StorageKeys/Migration/ConditionalMigrationKeys.swift b/SecureStorageSample/StorageKeys/Migration/ConditionalMigrationKeys.swift index beb926e..0e6931c 100644 --- a/SecureStorageSample/StorageKeys/Migration/ConditionalMigrationKeys.swift +++ b/SecureStorageSample/StorageKeys/Migration/ConditionalMigrationKeys.swift @@ -1,6 +1,7 @@ import Foundation import LocalData +/// Keys that demonstrate conditional migrations based on app version. extension StorageKey where Value == String { nonisolated static let legacyAppMode = StorageKey( name: "legacy_app_mode", diff --git a/SecureStorageSample/StorageKeys/Migration/MigrationKeys.swift b/SecureStorageSample/StorageKeys/Migration/MigrationKeys.swift index 6febd08..2c18dd6 100644 --- a/SecureStorageSample/StorageKeys/Migration/MigrationKeys.swift +++ b/SecureStorageSample/StorageKeys/Migration/MigrationKeys.swift @@ -1,8 +1,9 @@ import Foundation import LocalData +/// Keys that demonstrate a simple legacy-to-modern migration path. extension StorageKey where Value == String { - /// The legacy key where data starts (in UserDefaults) + /// The legacy key where data starts (in UserDefaults). nonisolated static let legacyMigrationSource = StorageKey( name: "legacy_user_id", domain: .userDefaults(suite: nil), @@ -14,7 +15,7 @@ extension StorageKey where Value == String { syncPolicy: .never ) - /// The modern key where data should end up (in Keychain) + /// The modern key where data should end up (in Keychain). nonisolated static let modernMigrationDestination = StorageKey( name: "secure_user_id", domain: .keychain(service: "com.mbrucedogs.securestorage"), @@ -28,6 +29,7 @@ extension StorageKey where Value == String { availability: .all, syncPolicy: .never, migration: { key in + // Simple "move on first read" migration for legacy identifiers. AnyStorageMigration( SimpleLegacyMigration( destinationKey: key, diff --git a/SecureStorageSample/StorageKeys/Migration/TransformingMigrationKeys.swift b/SecureStorageSample/StorageKeys/Migration/TransformingMigrationKeys.swift index a50d66f..d342eb6 100644 --- a/SecureStorageSample/StorageKeys/Migration/TransformingMigrationKeys.swift +++ b/SecureStorageSample/StorageKeys/Migration/TransformingMigrationKeys.swift @@ -1,6 +1,7 @@ import Foundation import LocalData +/// Legacy string payload that will be transformed into a structured model. extension StorageKey where Value == String { nonisolated static let legacyProfileName = StorageKey( name: "legacy_profile_name", @@ -14,6 +15,7 @@ extension StorageKey where Value == String { ) } +/// Modern structured payload produced by transforming migration. extension StorageKey where Value == ProfileName { nonisolated static let modernProfileName = StorageKey( name: "modern_profile_name", @@ -34,6 +36,7 @@ extension StorageKey where Value == ProfileName { destinationKey: key, sourceKey: sourceKey ) { value in + // Split "First Last" into a structured name for safer usage. let parts = value.split(separator: " ", maxSplits: 1).map(String.init) let firstName = parts.first ?? "" let lastName = parts.count > 1 ? parts[1] : "" diff --git a/SecureStorageSample/StorageKeys/Platform/SyncableSettingKey.swift b/SecureStorageSample/StorageKeys/Platform/SyncableSettingKey.swift index 2c4b85b..99beb9f 100644 --- a/SecureStorageSample/StorageKeys/Platform/SyncableSettingKey.swift +++ b/SecureStorageSample/StorageKeys/Platform/SyncableSettingKey.swift @@ -1,6 +1,7 @@ import Foundation import LocalData +/// UserDefaults key that highlights platform availability and sync policy options. extension StorageKey where Value == String { /// Syncable setting with configurable platform and sync policy. /// Grouped under Platform to highlight availability/sync behavior. @@ -15,6 +16,7 @@ extension StorageKey where Value == String { syncPolicy: .never ) + /// Builds a variant to demonstrate different availability and sync policies. nonisolated static func syncableSettingKey( availability: PlatformAvailability = .all, syncPolicy: SyncPolicy = .never diff --git a/SecureStorageSample/StorageKeys/Platform/WatchVibrationKey.swift b/SecureStorageSample/StorageKeys/Platform/WatchVibrationKey.swift index c0b4890..eebc0cf 100644 --- a/SecureStorageSample/StorageKeys/Platform/WatchVibrationKey.swift +++ b/SecureStorageSample/StorageKeys/Platform/WatchVibrationKey.swift @@ -1,6 +1,7 @@ import Foundation import LocalData +/// UserDefaults key for watch-only preferences, used to demonstrate availability constraints. extension StorageKey where Value == Bool { /// Watch-only setting for vibration. /// Grouped under Platform to highlight watch-only availability. diff --git a/SecureStorageSample/StorageKeys/UserDefaults/AppVersionKey.swift b/SecureStorageSample/StorageKeys/UserDefaults/AppVersionKey.swift index 3fe9f34..f8ec567 100644 --- a/SecureStorageSample/StorageKeys/UserDefaults/AppVersionKey.swift +++ b/SecureStorageSample/StorageKeys/UserDefaults/AppVersionKey.swift @@ -1,6 +1,7 @@ import Foundation import LocalData +/// UserDefaults key used in the demo and as a lightweight migration flag. extension StorageKey where Value == String { /// Stores the app version in standard UserDefaults. /// - Domain: UserDefaults (standard) diff --git a/SecureStorageSample/Views/AggregatingMigrationDemo.swift b/SecureStorageSample/Views/AggregatingMigrationDemo.swift index ced343f..11c541c 100644 --- a/SecureStorageSample/Views/AggregatingMigrationDemo.swift +++ b/SecureStorageSample/Views/AggregatingMigrationDemo.swift @@ -2,6 +2,7 @@ import SwiftUI import LocalData @MainActor +/// Demonstrates aggregating multiple legacy keys into a UnifiedSettings model. struct AggregatingMigrationDemo: View { @State private var notificationsEnabled = false @State private var theme = "" @@ -58,6 +59,7 @@ struct AggregatingMigrationDemo: View { .navigationBarTitleDisplayMode(.inline) } + /// Seeds the legacy keys to simulate pre-migration settings. private func saveToLegacy() { isLoading = true Task { @@ -78,6 +80,7 @@ struct AggregatingMigrationDemo: View { } } + /// Loads the modern unified key, triggering the aggregation migration. private func loadFromModern() { isLoading = true statusMessage = "Loading unified settings..." @@ -95,6 +98,7 @@ struct AggregatingMigrationDemo: View { } } + /// Formats the aggregated settings for display. private func format(_ value: UnifiedSettings) -> String { let notificationsText = value.notificationsEnabled ? "On" : "Off" let themeText = value.theme.isEmpty ? "system" : value.theme diff --git a/SecureStorageSample/Views/ConditionalMigrationDemo.swift b/SecureStorageSample/Views/ConditionalMigrationDemo.swift index 6a7b83f..c4746d2 100644 --- a/SecureStorageSample/Views/ConditionalMigrationDemo.swift +++ b/SecureStorageSample/Views/ConditionalMigrationDemo.swift @@ -2,6 +2,7 @@ import SwiftUI import LocalData @MainActor +/// Demonstrates a migration that only runs when app version criteria are met. struct ConditionalMigrationDemo: View { @State private var legacyValue = "" @State private var modernValue = "" @@ -59,6 +60,7 @@ struct ConditionalMigrationDemo: View { .navigationBarTitleDisplayMode(.inline) } + /// Seeds legacy data that will migrate when the version condition passes. private func saveToLegacy() { isLoading = true Task { @@ -73,6 +75,7 @@ struct ConditionalMigrationDemo: View { } } + /// Loads the modern key to trigger the conditional migration. private func loadFromModern() { isLoading = true statusMessage = "Loading modern mode..." diff --git a/SecureStorageSample/Views/EncryptedStorageDemo.swift b/SecureStorageSample/Views/EncryptedStorageDemo.swift index 71c3993..f6f0fd5 100644 --- a/SecureStorageSample/Views/EncryptedStorageDemo.swift +++ b/SecureStorageSample/Views/EncryptedStorageDemo.swift @@ -9,6 +9,7 @@ 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] = [] @@ -127,6 +128,7 @@ struct EncryptedStorageDemo: View { } } + /// Appends an encrypted log entry using the selected key derivation mode. private func addLogEntry() { isLoading = true Task { @@ -152,6 +154,7 @@ struct EncryptedStorageDemo: View { } } + /// Decrypts and loads log entries into memory for display. private func loadLogs() { isLoading = true Task { @@ -174,6 +177,7 @@ struct EncryptedStorageDemo: View { } } + /// Clears the encrypted log file for a clean slate. private func clearLogs() { isLoading = true Task { @@ -194,6 +198,7 @@ struct EncryptedStorageDemo: View { } } + /// 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 { @@ -214,6 +219,7 @@ struct EncryptedStorageDemo: View { } } +/// 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.") diff --git a/SecureStorageSample/Views/FileSystemDemo.swift b/SecureStorageSample/Views/FileSystemDemo.swift index b2bb3e6..0d80fb3 100644 --- a/SecureStorageSample/Views/FileSystemDemo.swift +++ b/SecureStorageSample/Views/FileSystemDemo.swift @@ -10,6 +10,7 @@ import LocalData import SharedKit @MainActor +/// Demonstrates file storage in Documents/Caches plus App Group file sharing. struct FileSystemDemo: View { @State private var profileName = "" @State private var profileEmail = "" @@ -168,6 +169,7 @@ struct FileSystemDemo: View { } } + /// Saves a UserProfile to the selected file system directory. private func saveProfile() { isLoading = true Task { @@ -188,6 +190,7 @@ struct FileSystemDemo: View { } } + /// Loads a UserProfile from the selected directory and updates the form. private func loadProfile() { isLoading = true Task { @@ -212,6 +215,7 @@ struct FileSystemDemo: View { } } + /// Deletes the profile file from the selected directory. private func deleteProfile() { isLoading = true Task { @@ -227,6 +231,7 @@ struct FileSystemDemo: View { } } + /// Persists a UserProfile into the App Group container. private func saveAppGroupProfile() { isLoading = true Task { @@ -247,6 +252,7 @@ struct FileSystemDemo: View { } } + /// Loads a UserProfile from the App Group container. private func loadAppGroupProfile() { isLoading = true Task { @@ -271,6 +277,7 @@ struct FileSystemDemo: View { } } + /// Deletes the App Group profile file to demonstrate shared cleanup. private func deleteAppGroupProfile() { isLoading = true Task { diff --git a/SecureStorageSample/Views/KeychainDemo.swift b/SecureStorageSample/Views/KeychainDemo.swift index d5bdaee..aa0ac4d 100644 --- a/SecureStorageSample/Views/KeychainDemo.swift +++ b/SecureStorageSample/Views/KeychainDemo.swift @@ -9,6 +9,7 @@ import SwiftUI import LocalData @MainActor +/// Demonstrates Keychain storage with configurable accessibility and access control. struct KeychainDemo: View { @State private var username = "" @State private var password = "" @@ -113,6 +114,7 @@ struct KeychainDemo: View { } } + /// Stores credentials using the currently selected Keychain policies. private func saveCredentials() { isLoading = true Task { @@ -131,6 +133,7 @@ struct KeychainDemo: View { } } + /// Loads credentials using the currently selected Keychain policies. private func loadCredentials() { isLoading = true Task { @@ -157,6 +160,7 @@ struct KeychainDemo: View { } } + /// Removes the stored credentials from the Keychain. private func deleteCredentials() { isLoading = true Task { diff --git a/SecureStorageSample/Views/MigrationDemo.swift b/SecureStorageSample/Views/MigrationDemo.swift index e9846b3..9fd0fda 100644 --- a/SecureStorageSample/Views/MigrationDemo.swift +++ b/SecureStorageSample/Views/MigrationDemo.swift @@ -2,6 +2,7 @@ import SwiftUI import LocalData @MainActor +/// Demonstrates a simple legacy key migration from UserDefaults to Keychain. struct MigrationDemo: View { @State private var legacyValue = "" @State private var modernValue = "" @@ -83,6 +84,7 @@ struct MigrationDemo: View { // MARK: - Actions + /// Seeds the legacy key to simulate pre-migration data. private func saveToLegacy() { isLoading = true Task { @@ -97,6 +99,7 @@ struct MigrationDemo: View { } } + /// Loads the modern key, triggering the configured migration if needed. private func loadFromModern() { isLoading = true statusMessage = "Retrieving from Modern..." @@ -115,6 +118,7 @@ struct MigrationDemo: View { } } + /// Forces a migration sweep to drain legacy data even if the modern key exists. private func runManualMigration() { isLoading = true statusMessage = "Running manual migration..." @@ -132,6 +136,7 @@ struct MigrationDemo: View { } } + /// Checks whether the legacy key still exists after migration. private func checkLegacyExists() { isLoading = true Task { diff --git a/SecureStorageSample/Views/MigrationHubView.swift b/SecureStorageSample/Views/MigrationHubView.swift index a6aa566..df19bc1 100644 --- a/SecureStorageSample/Views/MigrationHubView.swift +++ b/SecureStorageSample/Views/MigrationHubView.swift @@ -1,5 +1,6 @@ import SwiftUI +/// Entry point for the migration demos, grouped by strategy. struct MigrationHubView: View { var body: some View { Form { diff --git a/SecureStorageSample/Views/PlatformSyncDemo.swift b/SecureStorageSample/Views/PlatformSyncDemo.swift index 3ce62e9..2f84261 100644 --- a/SecureStorageSample/Views/PlatformSyncDemo.swift +++ b/SecureStorageSample/Views/PlatformSyncDemo.swift @@ -10,6 +10,7 @@ import LocalData import WatchConnectivity @MainActor +/// Demonstrates platform availability and Watch sync policies for a single key. struct PlatformSyncDemo: View { @State private var settingValue = "" @State private var storedValue = "" @@ -137,6 +138,7 @@ struct PlatformSyncDemo: View { } } + /// Saves the value using the current availability and sync policy selection. private func saveValue() { isLoading = true Task { @@ -157,6 +159,7 @@ struct PlatformSyncDemo: View { } } + /// Loads the value using the current availability and sync policy selection. private func loadValue() { isLoading = true Task { @@ -180,6 +183,7 @@ struct PlatformSyncDemo: View { } } + /// Attempts a read to show the platform access error when applicable. private func testPlatformError() { isLoading = true Task { @@ -202,6 +206,7 @@ struct PlatformSyncDemo: View { } } + /// Refreshes the WCSession status shown in the UI. private func refreshWatchStatus() { watchStatus = WatchStatus.current() } @@ -209,6 +214,7 @@ struct PlatformSyncDemo: View { // MARK: - Display Names +/// UI-friendly names for platform availability options. extension PlatformAvailability { var displayName: String { switch self { @@ -220,6 +226,7 @@ extension PlatformAvailability { } } +/// UI-friendly names for sync policies. extension SyncPolicy { var displayName: String { switch self { @@ -236,6 +243,7 @@ extension SyncPolicy { } } +/// Snapshot of current WatchConnectivity state for display purposes. private struct WatchStatus: Equatable { let isSupported: Bool let isPaired: Bool @@ -273,6 +281,7 @@ private struct WatchStatus: Equatable { } } +/// Read-only UI for displaying WatchConnectivity state. private struct WatchStatusView: View { let status: WatchStatus @@ -305,6 +314,7 @@ private struct WatchStatusView: View { } } +/// Explanatory text for each platform availability option. private struct PlatformAvailabilityDescriptionView: View { let availability: PlatformAvailability @@ -328,6 +338,7 @@ private struct PlatformAvailabilityDescriptionView: View { } } +/// Explanatory text for each sync policy option. private struct SyncPolicyDescriptionView: View { let syncPolicy: SyncPolicy @@ -349,6 +360,7 @@ private struct SyncPolicyDescriptionView: View { } } +/// Predicts the expected behavior given the selected availability and sync policy. private struct ExpectedOutcomeView: View { let availability: PlatformAvailability let syncPolicy: SyncPolicy diff --git a/SecureStorageSample/Views/TransformingMigrationDemo.swift b/SecureStorageSample/Views/TransformingMigrationDemo.swift index a548907..ec964cc 100644 --- a/SecureStorageSample/Views/TransformingMigrationDemo.swift +++ b/SecureStorageSample/Views/TransformingMigrationDemo.swift @@ -2,6 +2,7 @@ import SwiftUI import LocalData @MainActor +/// Demonstrates transforming a legacy string into a structured ProfileName. struct TransformingMigrationDemo: View { @State private var legacyValue = "" @State private var modernValue = "" @@ -59,6 +60,7 @@ struct TransformingMigrationDemo: View { .navigationBarTitleDisplayMode(.inline) } + /// Seeds the legacy string value to simulate pre-migration data. private func saveToLegacy() { isLoading = true Task { @@ -73,6 +75,7 @@ struct TransformingMigrationDemo: View { } } + /// Loads the modern key, triggering the transforming migration. private func loadFromModern() { isLoading = true statusMessage = "Loading modern profile..." @@ -91,6 +94,7 @@ struct TransformingMigrationDemo: View { } } + /// Formats the migrated name for display in the demo UI. private func format(_ value: ProfileName) -> String { let trimmedFirst = value.firstName.trimmingCharacters(in: .whitespacesAndNewlines) let trimmedLast = value.lastName.trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/SecureStorageSample/Views/UserDefaultsDemo.swift b/SecureStorageSample/Views/UserDefaultsDemo.swift index 9dc0011..5cff22e 100644 --- a/SecureStorageSample/Views/UserDefaultsDemo.swift +++ b/SecureStorageSample/Views/UserDefaultsDemo.swift @@ -9,6 +9,7 @@ import SwiftUI import LocalData @MainActor +/// Demonstrates typed UserDefaults keys, including App Group sharing. struct UserDefaultsDemo: View { @State private var inputText = "" @State private var storedValue = "" @@ -136,6 +137,7 @@ struct UserDefaultsDemo: View { } } + /// Persists the current input value with the standard UserDefaults key. private func saveValue() { isLoading = true Task { @@ -150,6 +152,7 @@ struct UserDefaultsDemo: View { } } + /// Loads the standard UserDefaults value and reflects it in the UI. private func loadValue() { isLoading = true Task { @@ -169,6 +172,7 @@ struct UserDefaultsDemo: View { } } + /// Removes the standard UserDefaults value to reset the demo. private func removeValue() { isLoading = true Task { @@ -184,6 +188,7 @@ struct UserDefaultsDemo: View { } } + /// Saves the input value into the App Group UserDefaults container. private func saveAppGroupValue() { isLoading = true Task { @@ -198,6 +203,7 @@ struct UserDefaultsDemo: View { } } + /// Loads the App Group value to demonstrate cross-target sharing. private func loadAppGroupValue() { isLoading = true Task { @@ -217,6 +223,7 @@ struct UserDefaultsDemo: View { } } + /// Clears the App Group value to demonstrate cleanup behavior. private func removeAppGroupValue() { isLoading = true Task { diff --git a/SecureStorageSample/WatchOptimized.swift b/SecureStorageSample/WatchOptimized.swift index 8b5726d..ccbff6a 100644 --- a/SecureStorageSample/WatchOptimized.swift +++ b/SecureStorageSample/WatchOptimized.swift @@ -1,22 +1,24 @@ import Foundation -// Example shared models for Watch-optimized data - +/// Example shared models that demonstrate creating Watch-optimized payloads. +/// The goal is to minimize what needs to traverse WatchConnectivity. struct Workout: Codable { let date: Date // Add other properties as needed } +/// Preferences included in the full profile; used to derive a slimmer watch payload. struct Preferences: Codable { let isPremium: Bool let appearance: Appearance } +/// Display preferences for the demo data. enum Appearance: Codable { case light, dark } -// Shared model (iOS + watchOS) +/// Shared full-fidelity profile used on iOS, with a computed watch-friendly version. struct FullUserProfile: Codable { let id: UUID let fullName: String @@ -25,7 +27,7 @@ struct FullUserProfile: Codable { let allWorkouts: [Workout] // ← potentially huge let preferences: Preferences - // Lightweight Watch version — computed + /// Lightweight Watch version computed from the full profile. var watchVersion: WatchUserProfile { WatchUserProfile( id: id, @@ -39,6 +41,7 @@ struct FullUserProfile: Codable { } } +/// Slimmed-down payload intended for watch display only. struct WatchUserProfile: Codable, Sendable { let id: UUID let displayName: String @@ -54,7 +57,7 @@ struct WatchUserProfile: Codable, Sendable { // let watchData = try JSONEncoder().encode(profile.watchVersion) // WCSession.default.updateApplicationContext(["profile": watchData]) -// Alternative: WatchRepresentable protocol +/// Optional protocol to standardize access to a watch-friendly payload. public protocol WatchRepresentable { associatedtype WatchVersion: Codable, Sendable var watchVersion: WatchVersion { get }