Summary: - Sources: Models, Protocols, Services - Tests: EncryptionHelperTests.swift - Docs: README - Added symbols: struct RemoteKeyProvider, func keyMaterial, struct KeyMaterialSource, protocol KeyMaterialProviding, func registerKeyMaterialProvider, struct StaticKeyMaterialProvider Stats: - 6 files changed, 104 insertions(+), 14 deletions(-)
156 lines
4.9 KiB
Markdown
156 lines
4.9 KiB
Markdown
# LocalData
|
|
|
|
LocalData provides a typed, discoverable namespace for persisted app data across UserDefaults, Keychain, and file storage with optional encryption.
|
|
|
|
## Architecture
|
|
|
|
The package uses a clean, modular architecture with isolated actors for thread safety:
|
|
|
|
```
|
|
StorageRouter (main entry point)
|
|
├── UserDefaultsHelper
|
|
├── KeychainHelper
|
|
├── FileStorageHelper
|
|
├── EncryptionHelper
|
|
└── SyncHelper
|
|
```
|
|
|
|
## What Ships in the Package
|
|
|
|
### Protocols
|
|
- **StorageKey** - Define storage configuration for each data type
|
|
- **StorageProviding** - Abstraction for storage operations
|
|
- **KeyMaterialProviding** - Supplies external key material for encryption
|
|
|
|
### Services (Actors)
|
|
- **StorageRouter** - Main entry point for all storage operations
|
|
- **KeychainHelper** - Secure keychain storage
|
|
- **EncryptionHelper** - AES-256-GCM or ChaCha20-Poly1305 with PBKDF2/HKDF
|
|
- **FileStorageHelper** - File system operations
|
|
- **UserDefaultsHelper** - UserDefaults with suite support
|
|
- **SyncHelper** - WatchConnectivity sync
|
|
|
|
### Models
|
|
- **StorageDomain** - userDefaults, keychain, fileSystem, encryptedFileSystem
|
|
- **SecurityPolicy** - none, keychain, encrypted (AES-256 or ChaCha20-Poly1305)
|
|
- **Serializer** - JSON, plist, Data, or custom
|
|
- **PlatformAvailability** - all, phoneOnly, watchOnly, phoneWithWatchSync
|
|
- **SyncPolicy** - never, manual, automaticSmall
|
|
- **KeychainAccessibility** - All 7 iOS accessibility options
|
|
- **KeychainAccessControl** - All 6 access control options (biometry, passcode, etc.)
|
|
- **FileDirectory** - documents, caches, custom URL
|
|
- **StorageError** - Comprehensive error types
|
|
- **AnyCodable** - Type-erased Codable for mixed-type payloads
|
|
|
|
## Usage
|
|
|
|
### 1. Define Keys
|
|
Extend `StorageKeys` with your own key types:
|
|
|
|
```swift
|
|
import LocalData
|
|
|
|
extension StorageKeys {
|
|
struct UserTokenKey: StorageKey {
|
|
typealias Value = String
|
|
|
|
let name = "user_token"
|
|
let domain: StorageDomain = .keychain(service: "com.myapp")
|
|
let security: SecurityPolicy = .keychain(
|
|
accessibility: .afterFirstUnlock,
|
|
accessControl: .biometryAny
|
|
)
|
|
let serializer: Serializer<String> = .json
|
|
let owner = "AuthService"
|
|
let availability: PlatformAvailability = .phoneOnly
|
|
let syncPolicy: SyncPolicy = .never
|
|
}
|
|
}
|
|
```
|
|
If you omit `security`, it defaults to `SecurityPolicy.recommended`.
|
|
|
|
### 2. Use StorageRouter
|
|
```swift
|
|
// Save
|
|
let key = StorageKeys.UserTokenKey()
|
|
try await StorageRouter.shared.set("token123", for: key)
|
|
|
|
// Retrieve
|
|
let token = try await StorageRouter.shared.get(key)
|
|
|
|
// Remove
|
|
try await StorageRouter.shared.remove(key)
|
|
```
|
|
|
|
## Storage Domains
|
|
|
|
| Domain | Use Case |
|
|
|--------|----------|
|
|
| `userDefaults` | Preferences, small settings |
|
|
| `keychain` | Credentials, tokens, sensitive data |
|
|
| `fileSystem` | Documents, cached data, large files |
|
|
| `encryptedFileSystem` | Sensitive files with encryption policies |
|
|
|
|
## Security Options
|
|
|
|
### Keychain Accessibility
|
|
- `whenUnlocked` - Only when device unlocked
|
|
- `afterFirstUnlock` - After first unlock until restart
|
|
- `whenUnlockedThisDeviceOnly` - No migration to new device
|
|
- `afterFirstUnlockThisDeviceOnly` - No migration
|
|
- `always` - Always accessible (least secure)
|
|
- `alwaysThisDeviceOnly` - Always, no migration
|
|
- `whenPasscodeSetThisDeviceOnly` - Requires passcode
|
|
|
|
### Access Control
|
|
- `userPresence` - Any authentication
|
|
- `biometryAny` - Face ID or Touch ID
|
|
- `biometryCurrentSet` - Current enrolled biometric only
|
|
- `devicePasscode` - Passcode only
|
|
- `biometryAnyOrDevicePasscode` - Biometric preferred, passcode fallback
|
|
- `biometryCurrentSetOrDevicePasscode` - Current biometric or passcode
|
|
|
|
### Encryption
|
|
- AES-256-GCM or ChaCha20-Poly1305
|
|
- PBKDF2-SHA256 or HKDF-SHA256 key derivation
|
|
- Configurable PBKDF2 iteration count
|
|
- Master key stored securely in keychain
|
|
- Default security policy: `SecurityPolicy.recommended` (ChaCha20-Poly1305 + HKDF)
|
|
- External key material providers can be registered via `EncryptionHelper`
|
|
|
|
```swift
|
|
struct RemoteKeyProvider: KeyMaterialProviding {
|
|
func keyMaterial(for keyName: String) async throws -> Data {
|
|
// Example only: fetch from service or keychain
|
|
Data(repeating: 1, count: 32)
|
|
}
|
|
}
|
|
|
|
let source = KeyMaterialSource(id: "remote.key")
|
|
await EncryptionHelper.shared.registerKeyMaterialProvider(RemoteKeyProvider(), for: source)
|
|
|
|
let policy: SecurityPolicy.EncryptionPolicy = .external(
|
|
source: source,
|
|
keyDerivation: .hkdf()
|
|
)
|
|
```
|
|
|
|
## Sync Behavior
|
|
|
|
StorageRouter can sync data to Apple Watch via WCSession when:
|
|
- `availability` is `.all` or `.phoneWithWatchSync`
|
|
- `syncPolicy` is `.manual` or `.automaticSmall` (≤100KB)
|
|
- WCSession is activated and watch is paired
|
|
|
|
The app owns WCSession activation and handling incoming updates.
|
|
|
|
## Platforms
|
|
- iOS 17+
|
|
- watchOS 10+
|
|
|
|
## Testing
|
|
- Unit tests use Swift Testing (`Testing` package)
|
|
|
|
## Sample App
|
|
See `SecureStorgageSample` for working examples of all storage domains and security options.
|