184 lines
8.9 KiB
Markdown
184 lines
8.9 KiB
Markdown
# SecureStorageSample
|
|
|
|
A sample iOS app demonstrating the LocalData package capabilities for secure, typed storage across multiple domains.
|
|
|
|
## Features
|
|
|
|
This app provides interactive demos for all LocalData storage options:
|
|
|
|
| Screen | Demo | Storage Domain |
|
|
|--------|------|----------------|
|
|
| **UserDefaults** | Save/load/remove values | UserDefaults |
|
|
| **Keychain** | Secure credentials with biometrics | Keychain |
|
|
| **File Storage** | User profiles with Codable models | File System |
|
|
| **Encrypted Storage** | Encrypted logs (AES or ChaCha20) | Encrypted File System |
|
|
| **Platform Sync Lab** | Platform availability & sync policies | Multiple |
|
|
|
|
The project also includes a watchOS companion app target for watch-specific demos.
|
|
The watch app displays the synced user profile and the syncable setting from the Platform Sync Lab.
|
|
On iPhone launch and when the watch becomes available, the app re-sends any syncable keys so the watch updates without manual re-entry.
|
|
The watch app also requests a sync on launch when the iPhone is reachable.
|
|
|
|
## Watch Sync Handshake
|
|
|
|
This sample uses a launch-order-safe handshake so either app can start first:
|
|
|
|
1. **Watch app launches** → sends a `request_sync` message (or queues it if the iPhone is unreachable).
|
|
2. **iOS app receives the request** → replies with a snapshot of current syncable keys and updates `applicationContext`.
|
|
3. **Watch app applies the snapshot** → UI updates immediately.
|
|
|
|
This avoids requiring users to remember which app to open first.
|
|
|
|
### Where the Logic Lives
|
|
|
|
- iOS WCSession + handshake: `SecureStorageSample/SecureStorageSample/Services/WatchConnectivityService.swift`
|
|
- Bootstrap on launch: `SecureStorageSample/SecureStorageSample/SecureStorageSampleApp.swift`
|
|
- Sync policy UI lab: `SecureStorageSample/SecureStorageSample/Views/PlatformSyncDemo.swift`
|
|
- Watch WCSession + request: `SecureStorageSample/SecureStorageSample Watch App/Services/WatchConnectivityService.swift`
|
|
- Watch payload handlers: `SecureStorageSample/SecureStorageSample Watch App/Services/Handlers/`
|
|
|
|
## Requirements
|
|
|
|
- iOS 17.0+
|
|
- watchOS 10.0+ (companion app target)
|
|
- Xcode 15+
|
|
|
|
## Getting Started
|
|
|
|
1. Open `SecureStorageSample.xcodeproj`
|
|
2. Select an iOS simulator or device
|
|
3. Build and run (⌘R)
|
|
4. To use App Group demos, enable the App Group entitlement for each target that should share data. The identifier is derived from the bundle ID via SharedKit constants.
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
SharedPackage/
|
|
├── Package.swift
|
|
└── Sources/
|
|
└── SharedKit/
|
|
├── Constants/
|
|
│ ├── StorageKeyNames.swift
|
|
│ └── StorageServiceIdentifiers.swift
|
|
└── Models/
|
|
└── UserProfile.swift
|
|
SecureStorageSample/
|
|
├── ContentView.swift # List-based navigation
|
|
├── Models/
|
|
│ ├── Credential.swift
|
|
│ └── SampleLocationData.swift
|
|
├── StorageKeys/
|
|
│ ├── UserDefaults/
|
|
│ ├── Keychain/
|
|
│ ├── FileSystem/
|
|
│ ├── EncryptedFileSystem/
|
|
│ ├── AppGroup/
|
|
│ └── Platform/
|
|
├── WatchOptimized.swift # Watch data models
|
|
├── Services/
|
|
│ ├── AppStorageCatalog.swift
|
|
│ ├── ExternalKeyMaterialProvider.swift
|
|
│ └── WatchConnectivityService.swift
|
|
└── Views/
|
|
├── UserDefaultsDemo.swift
|
|
├── KeychainDemo.swift
|
|
├── FileSystemDemo.swift
|
|
├── EncryptedStorageDemo.swift
|
|
└── PlatformSyncDemo.swift
|
|
SecureStorageSample Watch App/
|
|
├── SecureStorageSampleApp.swift
|
|
├── ContentView.swift
|
|
├── Protocols/
|
|
│ └── WatchDataHandling.swift
|
|
├── State/
|
|
│ └── WatchProfileStore.swift
|
|
└── Services/
|
|
├── WatchConnectivityService.swift
|
|
└── Handlers/
|
|
└── UserProfileWatchHandler.swift
|
|
```
|
|
|
|
## Storage Design Philosophy
|
|
|
|
This app intentionally uses a **Type-Safe Storage Design**. Unlike standard iOS development which uses string keys (e.g., `UserDefaults.standard.string(forKey: "user_name")`), this library requires you to define `StorageKey` values.
|
|
|
|
### Why types instead of strings?
|
|
1. **Safety**: The compiler prevents typos. You can't accidentally load from `"user_name"` and save to `"username"`.
|
|
2. **Codable Support**: Keys define their own value types. You can store complex `Codable` structs or classes just as easily as strings, and the library handles the JSON/Plist serialization automatically.
|
|
3. **Visibility**: All data your app stores is discoverable in the `StorageKeys/` folder. It serves as a manifest of your app's persistence layer.
|
|
4. **Migration**: You can move a piece of data from `UserDefaults` to `EncryptedFileSystem` just by changing the `domain` in the Key definition. No UI code needs to change.
|
|
|
|
## Storage Key Examples
|
|
|
|
The app demonstrates various storage configurations:
|
|
|
|
### UserDefaults
|
|
- Simple string storage with automatic sync
|
|
- App Group UserDefaults support for shared preferences
|
|
|
|
### Keychain
|
|
- 7 accessibility options (whenUnlocked, afterFirstUnlock, etc.)
|
|
- 6 access control options (biometry, passcode, etc.)
|
|
|
|
### File System
|
|
- **Documents**: Permanent storage, backed up to iCloud. Use for critical user data.
|
|
- **Caches**: Purgeable storage (iOS may delete when low on space), not backed up. Use for temporary data.
|
|
- JSON and PropertyList serializers supported.
|
|
|
|
### App Group Storage
|
|
- Shared UserDefaults via App Group identifier
|
|
- Shared files in the App Group container
|
|
- Requires App Group entitlements in all participating targets
|
|
|
|
### Encrypted Storage
|
|
- AES-256-GCM or ChaCha20-Poly1305 encryption
|
|
- PBKDF2 or HKDF key derivation
|
|
- PBKDF2 iteration count must remain consistent or existing data will not decrypt
|
|
- Complete file protection
|
|
- External key material example via `KeyMaterialProviding`
|
|
- Global encryption configuration (Keychain service/account) in app `init`
|
|
|
|
### Platform & Sync
|
|
- Platform availability (phoneOnly, watchOnly, all)
|
|
- Sync policies (never, manual, automaticSmall)
|
|
- Global sync configuration (max file size) in app `init`
|
|
|
|
### Data Migration
|
|
- **Fallback**: Automatically moves data from `legacyMigrationSource` to `modernMigrationDestination` on first access using protocol-based migration.
|
|
- **Transforming**: Converts a legacy full-name string into a structured `ProfileName`.
|
|
- **Aggregating**: Combines legacy notification + theme settings into `UnifiedSettings`.
|
|
- **Conditional**: Migrates app mode only when the version rule is met.
|
|
- **Manual Sweep**: Explicitly triggers a "drain" of legacy keys to the Keychain using `StorageRouter.shared.forceMigration(for:)`.
|
|
- **Startup Sweep**: Automatically cleanses all registered legacy keys at app launch via `registerCatalog(..., migrateImmediately: true)`.
|
|
|
|
## Global Configuration
|
|
|
|
The app demonstrates how to configure the `LocalData` library globally during startup in `SecureStorageSampleApp.swift`:
|
|
|
|
- **Encryption**: Customized Keychain service (`SecureStorageSample`) and account (`AppMasterKey`) names to isolate the library's master encryption key from other apps.
|
|
- **Sync**: Set a custom `maxAutoSyncSize` of 50KB to control which data is automatically synchronized to the Apple Watch, overriding the library's 100KB default.
|
|
- **File Storage**: Scoping all library files into a `SecureStorage` sub-directory. This ensures that the library's data (whether in the main sandbox or a shared App Group container) is kept neat and isolated within its own folder, rather than cluttering the root directories.
|
|
- **Storage Defaults**: Pre-configuring the default Keychain service and App Group identifier. This allows common keys in the app to omit these identifiers, reducing boilerplate and making the code more maintainable.
|
|
|
|
This approach centralizes infrastructure settings and avoids hardcoding environment-specific values within individual storage keys.
|
|
|
|
## Dependencies
|
|
|
|
- [LocalData](../localPackages/LocalData) - Local package for typed secure storage
|
|
- SharedKit - Local package for shared iOS/watch models and constants
|
|
|
|
## Notes
|
|
|
|
- Storage keys are now split into one file per key and grouped by domain; platform-focused keys live in `StorageKeys/Platform` with comments calling out availability/sync focus.
|
|
- Keys are declared as `StorageKey<Value>` static properties via constrained extensions (e.g., `extension StorageKey where Value == String`).
|
|
- The shared model/constants live in `SharedPackage` (`SharedKit`) to keep the watch/iOS data contract centralized.
|
|
- Keychain service IDs and App Group identifiers are centralized in `SharedKit/Constants/StorageServiceIdentifiers.swift` to avoid hardcoded strings in keys.
|
|
- The watch app uses a handler-based WatchConnectivity layer so new payload types can be added in `Services/Handlers` without bloating the main service.
|
|
- A `StorageKeyCatalog` sample is included to generate a security audit report of all storage keys.
|
|
- Each `StorageKey` includes a `description` used in audit reports.
|
|
- The catalog is registered at app startup to enforce key registration and catch duplicates.
|
|
|
|
## License
|
|
|
|
This sample is provided for demonstration purposes.
|