Compare commits

...

10 Commits

50 changed files with 432 additions and 165 deletions

View File

@ -1,4 +1,4 @@
# SecureStorgageSample
# SecureStorageSample
A sample iOS app demonstrating the LocalData package capabilities for secure, typed storage across multiple domains.
@ -24,10 +24,10 @@ The project also includes a watchOS companion app target for watch-specific demo
## Getting Started
1. Open `SecureStorgageSample.xcodeproj`
1. Open `SecureStorageSample.xcodeproj`
2. Select an iOS simulator or device
3. Build and run (⌘R)
4. To use App Group demos, set your App Group identifier in `SecureStorgageSample/SecureStorgageSample/Models/AppGroupConfiguration.swift` and enable the entitlement for each target that should share data.
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
@ -37,10 +37,11 @@ SharedPackage/
└── Sources/
└── SharedKit/
├── Constants/
│ └── StorageKeyNames.swift
│ ├── StorageKeyNames.swift
│ └── StorageServiceIdentifiers.swift
└── Models/
└── UserProfile.swift
SecureStorgageSample/
SecureStorageSample/
├── ContentView.swift # Tabbed navigation
├── Models/
│ ├── Credential.swift
@ -89,9 +90,9 @@ The app demonstrates various storage configurations:
- 6 access control options (biometry, passcode, etc.)
### File System
- Documents directory (persisted, backed up)
- Caches directory (can be purged)
- JSON and PropertyList serializers
- **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
@ -103,10 +104,23 @@ The app demonstrates various storage configurations:
- PBKDF2 or HKDF key derivation
- 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`
## 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
@ -117,6 +131,7 @@ The app demonstrates various storage configurations:
- 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.
- 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.

View File

@ -0,0 +1,58 @@
# SecureStorageSample Watch App
A watchOS companion app demonstrating data synchronization with the iOS app using WatchConnectivity.
## Overview
This watch app receives `UserProfile` data synced from the paired iPhone via `WCSession.updateApplicationContext`. It does **not** use LocalData directly for storage—instead, it displays synced data in memory.
## Architecture
```
SecureStorageSample Watch App/
├── ContentView.swift # Displays synced profile data
├── SecureStorageSampleApp.swift
├── Protocols/
│ └── WatchDataHandling.swift # Protocol for payload handlers
├── State/
│ └── WatchProfileStore.swift # Holds synced profile in memory
└── Services/
├── WatchConnectivityService.swift
└── Handlers/
└── UserProfileWatchHandler.swift
```
## Data Flow
1. **iOS app** calls `SyncHelper` when storing data with `syncPolicy: .automaticSmall` or `.manual`
2. `SyncHelper` sends data via `WCSession.updateApplicationContext`
3. **Watch app** receives context in `WatchConnectivityService`
4. The service dispatches each payload key to its registered `WatchDataHandling` handler
5. `UserProfileWatchHandler` decodes the profile and updates `WatchProfileStore`
## Adding New Sync Payloads
1. Create a new handler conforming to `WatchDataHandling`:
```swift
struct MyDataWatchHandler: WatchDataHandling {
let key = "myData"
func handle(data: Data) {
// Decode and update state
}
}
```
2. Register it in `WatchConnectivityService.registerDefaultHandlers()`
## Limitations
- **No persistent storage on watch**: Data is held in memory only
- **One-way sync**: Watch receives data from iPhone; it does not send data back
- **Simulator limitations**: WatchConnectivity may not work reliably in the simulator
## Requirements
- watchOS 10.0+
- Paired with iOS app via WatchConnectivity

View File

@ -8,10 +8,14 @@ final class UserProfileWatchHandler: WatchDataHandling {
private let store: WatchProfileStore
private let decoder = JSONDecoder()
init(store: WatchProfileStore = .shared) {
init(store: WatchProfileStore) {
self.store = store
}
convenience init() {
self.init(store: .shared)
}
func handle(data: Data) {
do {
let profile = try decoder.decode(UserProfile.self, from: data)

View File

@ -20,14 +20,14 @@
containerPortal = EA179CF92F1722BB00B1D54A /* Project object */;
proxyType = 1;
remoteGlobalIDString = EA179D002F1722BB00B1D54A;
remoteInfo = SecureStorgageSample;
remoteInfo = SecureStorageSample;
};
EA179D192F1722BC00B1D54A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = EA179CF92F1722BB00B1D54A /* Project object */;
proxyType = 1;
remoteGlobalIDString = EA179D002F1722BB00B1D54A;
remoteInfo = SecureStorgageSample;
remoteInfo = SecureStorageSample;
};
EA65D6F22F17DD6800C48466 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
@ -67,28 +67,28 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
EA179D012F1722BB00B1D54A /* SecureStorgageSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SecureStorgageSample.app; sourceTree = BUILT_PRODUCTS_DIR; };
EA179D0E2F1722BC00B1D54A /* SecureStorgageSampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SecureStorgageSampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
EA179D182F1722BC00B1D54A /* SecureStorgageSampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SecureStorgageSampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
EA179D012F1722BB00B1D54A /* SecureStorageSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SecureStorageSample.app; sourceTree = BUILT_PRODUCTS_DIR; };
EA179D0E2F1722BC00B1D54A /* SecureStorageSampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SecureStorageSampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
EA179D182F1722BC00B1D54A /* SecureStorageSampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SecureStorageSampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
EA65D6E52F17DD6700C48466 /* SecureStorageSample Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "SecureStorageSample Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
EA65D6F12F17DD6800C48466 /* SecureStorageSample Watch AppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SecureStorageSample Watch AppTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
EA65D6FB2F17DD6800C48466 /* SecureStorageSample Watch AppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SecureStorageSample Watch AppUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
EA179D032F1722BB00B1D54A /* SecureStorgageSample */ = {
EA179D032F1722BB00B1D54A /* SecureStorageSample */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = SecureStorgageSample;
path = SecureStorageSample;
sourceTree = "<group>";
};
EA179D112F1722BC00B1D54A /* SecureStorgageSampleTests */ = {
EA179D112F1722BC00B1D54A /* SecureStorageSampleTests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = SecureStorgageSampleTests;
path = SecureStorageSampleTests;
sourceTree = "<group>";
};
EA179D1B2F1722BC00B1D54A /* SecureStorgageSampleUITests */ = {
EA179D1B2F1722BC00B1D54A /* SecureStorageSampleUITests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = SecureStorgageSampleUITests;
path = SecureStorageSampleUITests;
sourceTree = "<group>";
};
EA65D6E62F17DD6700C48466 /* SecureStorageSample Watch App */ = {
@ -161,9 +161,9 @@
EA179CF82F1722BB00B1D54A = {
isa = PBXGroup;
children = (
EA179D032F1722BB00B1D54A /* SecureStorgageSample */,
EA179D112F1722BC00B1D54A /* SecureStorgageSampleTests */,
EA179D1B2F1722BC00B1D54A /* SecureStorgageSampleUITests */,
EA179D032F1722BB00B1D54A /* SecureStorageSample */,
EA179D112F1722BC00B1D54A /* SecureStorageSampleTests */,
EA179D1B2F1722BC00B1D54A /* SecureStorageSampleUITests */,
EA65D6E62F17DD6700C48466 /* SecureStorageSample Watch App */,
EA65D6F42F17DD6800C48466 /* SecureStorageSample Watch AppTests */,
EA65D6FE2F17DD6800C48466 /* SecureStorageSample Watch AppUITests */,
@ -175,9 +175,9 @@
EA179D022F1722BB00B1D54A /* Products */ = {
isa = PBXGroup;
children = (
EA179D012F1722BB00B1D54A /* SecureStorgageSample.app */,
EA179D0E2F1722BC00B1D54A /* SecureStorgageSampleTests.xctest */,
EA179D182F1722BC00B1D54A /* SecureStorgageSampleUITests.xctest */,
EA179D012F1722BB00B1D54A /* SecureStorageSample.app */,
EA179D0E2F1722BC00B1D54A /* SecureStorageSampleTests.xctest */,
EA179D182F1722BC00B1D54A /* SecureStorageSampleUITests.xctest */,
EA65D6E52F17DD6700C48466 /* SecureStorageSample Watch App.app */,
EA65D6F12F17DD6800C48466 /* SecureStorageSample Watch AppTests.xctest */,
EA65D6FB2F17DD6800C48466 /* SecureStorageSample Watch AppUITests.xctest */,
@ -195,9 +195,9 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
EA179D002F1722BB00B1D54A /* SecureStorgageSample */ = {
EA179D002F1722BB00B1D54A /* SecureStorageSample */ = {
isa = PBXNativeTarget;
buildConfigurationList = EA179D222F1722BC00B1D54A /* Build configuration list for PBXNativeTarget "SecureStorgageSample" */;
buildConfigurationList = EA179D222F1722BC00B1D54A /* Build configuration list for PBXNativeTarget "SecureStorageSample" */;
buildPhases = (
EA179CFD2F1722BB00B1D54A /* Sources */,
EA179CFE2F1722BB00B1D54A /* Frameworks */,
@ -210,20 +210,20 @@
EA65D70F2F17DDEB00C48466 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
EA179D032F1722BB00B1D54A /* SecureStorgageSample */,
EA179D032F1722BB00B1D54A /* SecureStorageSample */,
);
name = SecureStorgageSample;
name = SecureStorageSample;
packageProductDependencies = (
EA179D552F17379800B1D54A /* LocalData */,
EA65D7312F17DDEB00C48466 /* SharedKit */,
);
productName = SecureStorgageSample;
productReference = EA179D012F1722BB00B1D54A /* SecureStorgageSample.app */;
productName = SecureStorageSample;
productReference = EA179D012F1722BB00B1D54A /* SecureStorageSample.app */;
productType = "com.apple.product-type.application";
};
EA179D0D2F1722BC00B1D54A /* SecureStorgageSampleTests */ = {
EA179D0D2F1722BC00B1D54A /* SecureStorageSampleTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = EA179D252F1722BC00B1D54A /* Build configuration list for PBXNativeTarget "SecureStorgageSampleTests" */;
buildConfigurationList = EA179D252F1722BC00B1D54A /* Build configuration list for PBXNativeTarget "SecureStorageSampleTests" */;
buildPhases = (
EA179D0A2F1722BC00B1D54A /* Sources */,
EA179D0B2F1722BC00B1D54A /* Frameworks */,
@ -235,19 +235,19 @@
EA179D102F1722BC00B1D54A /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
EA179D112F1722BC00B1D54A /* SecureStorgageSampleTests */,
EA179D112F1722BC00B1D54A /* SecureStorageSampleTests */,
);
name = SecureStorgageSampleTests;
name = SecureStorageSampleTests;
packageProductDependencies = (
EA65D7312F17DDEB00C48466 /* SharedKit */,
);
productName = SecureStorgageSampleTests;
productReference = EA179D0E2F1722BC00B1D54A /* SecureStorgageSampleTests.xctest */;
productName = SecureStorageSampleTests;
productReference = EA179D0E2F1722BC00B1D54A /* SecureStorageSampleTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
EA179D172F1722BC00B1D54A /* SecureStorgageSampleUITests */ = {
EA179D172F1722BC00B1D54A /* SecureStorageSampleUITests */ = {
isa = PBXNativeTarget;
buildConfigurationList = EA179D282F1722BC00B1D54A /* Build configuration list for PBXNativeTarget "SecureStorgageSampleUITests" */;
buildConfigurationList = EA179D282F1722BC00B1D54A /* Build configuration list for PBXNativeTarget "SecureStorageSampleUITests" */;
buildPhases = (
EA179D142F1722BC00B1D54A /* Sources */,
EA179D152F1722BC00B1D54A /* Frameworks */,
@ -259,13 +259,13 @@
EA179D1A2F1722BC00B1D54A /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
EA179D1B2F1722BC00B1D54A /* SecureStorgageSampleUITests */,
EA179D1B2F1722BC00B1D54A /* SecureStorageSampleUITests */,
);
name = SecureStorgageSampleUITests;
name = SecureStorageSampleUITests;
packageProductDependencies = (
);
productName = SecureStorgageSampleUITests;
productReference = EA179D182F1722BC00B1D54A /* SecureStorgageSampleUITests.xctest */;
productName = SecureStorageSampleUITests;
productReference = EA179D182F1722BC00B1D54A /* SecureStorageSampleUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
EA65D6E42F17DD6700C48466 /* SecureStorageSample Watch App */ = {
@ -371,7 +371,7 @@
};
};
};
buildConfigurationList = EA179CFC2F1722BB00B1D54A /* Build configuration list for PBXProject "SecureStorgageSample" */;
buildConfigurationList = EA179CFC2F1722BB00B1D54A /* Build configuration list for PBXProject "SecureStorageSample" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
@ -389,9 +389,9 @@
projectDirPath = "";
projectRoot = "";
targets = (
EA179D002F1722BB00B1D54A /* SecureStorgageSample */,
EA179D0D2F1722BC00B1D54A /* SecureStorgageSampleTests */,
EA179D172F1722BC00B1D54A /* SecureStorgageSampleUITests */,
EA179D002F1722BB00B1D54A /* SecureStorageSample */,
EA179D0D2F1722BC00B1D54A /* SecureStorageSampleTests */,
EA179D172F1722BC00B1D54A /* SecureStorageSampleUITests */,
EA65D6E42F17DD6700C48466 /* SecureStorageSample Watch App */,
EA65D6F02F17DD6800C48466 /* SecureStorageSample Watch AppTests */,
EA65D6FA2F17DD6800C48466 /* SecureStorageSample Watch AppUITests */,
@ -492,12 +492,12 @@
/* Begin PBXTargetDependency section */
EA179D102F1722BC00B1D54A /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = EA179D002F1722BB00B1D54A /* SecureStorgageSample */;
target = EA179D002F1722BB00B1D54A /* SecureStorageSample */;
targetProxy = EA179D0F2F1722BC00B1D54A /* PBXContainerItemProxy */;
};
EA179D1A2F1722BC00B1D54A /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = EA179D002F1722BB00B1D54A /* SecureStorgageSample */;
target = EA179D002F1722BB00B1D54A /* SecureStorageSample */;
targetProxy = EA179D192F1722BC00B1D54A /* PBXContainerItemProxy */;
};
EA65D6F32F17DD6800C48466 /* PBXTargetDependency */ = {
@ -642,7 +642,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = SecureStorgageSample/SecureStorgageSample.entitlements;
CODE_SIGN_ENTITLEMENTS = SecureStorageSample/SecureStorageSample.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
@ -659,7 +659,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.SecureStorgageSample;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.SecureStorageSample;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
@ -676,7 +676,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = SecureStorgageSample/SecureStorgageSample.entitlements;
CODE_SIGN_ENTITLEMENTS = SecureStorageSample/SecureStorageSample.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
@ -693,7 +693,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.SecureStorgageSample;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.SecureStorageSample;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
@ -714,7 +714,7 @@
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.SecureStorgageSampleTests;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.SecureStorageSampleTests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
@ -722,7 +722,7 @@
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SecureStorgageSample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SecureStorgageSample";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SecureStorageSample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SecureStorageSample";
};
name = Debug;
};
@ -735,7 +735,7 @@
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.SecureStorgageSampleTests;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.SecureStorageSampleTests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
@ -743,7 +743,7 @@
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SecureStorgageSample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SecureStorgageSample";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SecureStorageSample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SecureStorageSample";
};
name = Release;
};
@ -754,7 +754,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.SecureStorgageSampleUITests;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.SecureStorageSampleUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
@ -762,7 +762,7 @@
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = SecureStorgageSample;
TEST_TARGET_NAME = SecureStorageSample;
};
name = Debug;
};
@ -773,7 +773,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.SecureStorgageSampleUITests;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.SecureStorageSampleUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
@ -781,7 +781,7 @@
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = SecureStorgageSample;
TEST_TARGET_NAME = SecureStorageSample;
};
name = Release;
};
@ -797,13 +797,13 @@
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = SecureStorageSample;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.mbrucedogs.SecureStorgageSample;
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.mbrucedogs.SecureStorageSample;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.SecureStorgageSample.watchkitapp;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.SecureStorageSample.watchkitapp;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos;
SKIP_INSTALL = YES;
@ -830,13 +830,13 @@
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = SecureStorageSample;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.mbrucedogs.SecureStorgageSample;
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.mbrucedogs.SecureStorageSample;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.SecureStorgageSample.watchkitapp;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.SecureStorageSample.watchkitapp;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos;
SKIP_INSTALL = YES;
@ -944,7 +944,7 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
EA179CFC2F1722BB00B1D54A /* Build configuration list for PBXProject "SecureStorgageSample" */ = {
EA179CFC2F1722BB00B1D54A /* Build configuration list for PBXProject "SecureStorageSample" */ = {
isa = XCConfigurationList;
buildConfigurations = (
EA179D202F1722BC00B1D54A /* Debug */,
@ -953,7 +953,7 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
EA179D222F1722BC00B1D54A /* Build configuration list for PBXNativeTarget "SecureStorgageSample" */ = {
EA179D222F1722BC00B1D54A /* Build configuration list for PBXNativeTarget "SecureStorageSample" */ = {
isa = XCConfigurationList;
buildConfigurations = (
EA179D232F1722BC00B1D54A /* Debug */,
@ -962,7 +962,7 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
EA179D252F1722BC00B1D54A /* Build configuration list for PBXNativeTarget "SecureStorgageSampleTests" */ = {
EA179D252F1722BC00B1D54A /* Build configuration list for PBXNativeTarget "SecureStorageSampleTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
EA179D262F1722BC00B1D54A /* Debug */,
@ -971,7 +971,7 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
EA179D282F1722BC00B1D54A /* Build configuration list for PBXNativeTarget "SecureStorgageSampleUITests" */ = {
EA179D282F1722BC00B1D54A /* Build configuration list for PBXNativeTarget "SecureStorageSampleUITests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
EA179D292F1722BC00B1D54A /* Debug */,

View File

@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2600"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EA65D6E42F17DD6700C48466"
BuildableName = "SecureStorageSample Watch App.app"
BlueprintName = "SecureStorageSample Watch App"
ReferencedContainer = "container:SecureStorageSample.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EA179D002F1722BB00B1D54A"
BuildableName = "SecureStorageSample.app"
BlueprintName = "SecureStorageSample"
ReferencedContainer = "container:SecureStorageSample.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EA65D6F02F17DD6800C48466"
BuildableName = "SecureStorageSample Watch AppTests.xctest"
BlueprintName = "SecureStorageSample Watch AppTests"
ReferencedContainer = "container:SecureStorageSample.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EA65D6FA2F17DD6800C48466"
BuildableName = "SecureStorageSample Watch AppUITests.xctest"
BlueprintName = "SecureStorageSample Watch AppUITests"
ReferencedContainer = "container:SecureStorageSample.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EA65D6E42F17DD6700C48466"
BuildableName = "SecureStorageSample Watch App.app"
BlueprintName = "SecureStorageSample Watch App"
ReferencedContainer = "container:SecureStorageSample.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EA65D6E42F17DD6700C48466"
BuildableName = "SecureStorageSample Watch App.app"
BlueprintName = "SecureStorageSample Watch App"
ReferencedContainer = "container:SecureStorageSample.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2600"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EA179D002F1722BB00B1D54A"
BuildableName = "SecureStorageSample.app"
BlueprintName = "SecureStorageSample"
ReferencedContainer = "container:SecureStorageSample.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EA179D002F1722BB00B1D54A"
BuildableName = "SecureStorageSample.app"
BlueprintName = "SecureStorageSample"
ReferencedContainer = "container:SecureStorageSample.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EA179D002F1722BB00B1D54A"
BuildableName = "SecureStorageSample.app"
BlueprintName = "SecureStorageSample"
ReferencedContainer = "container:SecureStorageSample.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -2,6 +2,6 @@
<Workspace
version = "1.0">
<FileRef
location = "container:SecureStorgageSample.xcodeproj">
location = "container:SecureStorageSample.xcodeproj">
</FileRef>
</Workspace>

View File

@ -1,6 +1,6 @@
//
// ContentView.swift
// SecureStorgageSample
// SecureStorageSample
//
// Main navigation view with tabbed interface for all LocalData demos.
//

View File

@ -0,0 +1,69 @@
//
// SecureStorageSampleApp.swift
// SecureStorageSample
//
// Created by Matt Bruce on 1/13/26.
//
import SwiftUI
import LocalData
@main
struct SecureStorageSampleApp: App {
init() {
_ = WatchConnectivityService.shared
Task {
// 1. Global Encryption Configuration
// We isolate our library's master key in the Keychain by providing a specific service
// and account name. This prevents conflicts with other apps using the same library.
// We also set PBKDF2 iterations for consistent security across the app.
let config = EncryptionConfiguration(
masterKeyService: "SecureStorageSample",
masterKeyAccount: "AppMasterKey",
pbkdf2Iterations: 20_000
)
await StorageRouter.shared.updateEncryptionConfiguration(config)
// 2. Global Sync Configuration
// Overriding the default 100KB limit for automatic Watch sync to 50KB.
// This demonstrates how to tune performance for low-bandwidth scenarios.
let syncConfig = SyncConfiguration(maxAutoSyncSize: 50_000)
await StorageRouter.shared.updateSyncConfiguration(syncConfig)
// 3. Global File Storage Configuration
// We scope all our library files into a "SecureStorage" sub-directory
// underneath the standard Documents/Caches folders.
let fileConfig = FileStorageConfiguration(subDirectory: "SecureStorage")
await StorageRouter.shared.updateFileStorageConfiguration(fileConfig)
// 4. Global Storage Defaults
// Setting default identifiers for Keychain and App Groups.
// This allows keys to be defined more concisely without repeating these IDs.
let storageConfig = StorageConfiguration(
defaultKeychainService: StorageServiceIdentifiers.bundleIdentifier,
defaultAppGroupIdentifier: StorageServiceIdentifiers.appGroupIdentifier
)
await StorageRouter.shared.updateStorageConfiguration(storageConfig)
do {
try await StorageRouter.shared.registerCatalog(AppStorageCatalog.self)
} catch {
assertionFailure("Storage catalog registration failed: \(error)")
}
await StorageRouter.shared.registerKeyMaterialProvider(
ExternalKeyMaterialProvider(),
for: SampleKeyMaterialSources.external
)
}
#if DEBUG
let report = StorageAuditReport.renderText(for: AppStorageCatalog.self)
print(report)
#endif
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

View File

@ -1,6 +1,6 @@
import Foundation
import LocalData
import SharedKit
extension StorageKeys {
/// Stores a shared setting in App Group UserDefaults.
/// - Domain: App Group UserDefaults
@ -10,7 +10,7 @@ extension StorageKeys {
typealias Value = String
let name = "app_group_setting"
let domain: StorageDomain = .appGroupUserDefaults(identifier: AppGroupConfiguration.identifier)
let domain: StorageDomain = .appGroupUserDefaults(identifier: StorageServiceIdentifiers.appGroupIdentifier)
let security: SecurityPolicy = .none
let serializer: Serializer<String> = .json
let owner = "SampleApp"

View File

@ -24,7 +24,7 @@ extension StorageKeys {
}
var domain: StorageDomain {
.appGroupFileSystem(identifier: AppGroupConfiguration.identifier, directory: directory)
.appGroupFileSystem(identifier: StorageServiceIdentifiers.appGroupIdentifier, directory: directory)
}
}
}

View File

@ -1,5 +1,6 @@
import Foundation
import LocalData
import SharedKit
extension StorageKeys {
/// Stores user preferences in App Group UserDefaults.
@ -10,7 +11,7 @@ extension StorageKeys {
typealias Value = [String: AnyCodable]
let name = "user_preferences"
let domain: StorageDomain = .appGroupUserDefaults(identifier: AppGroupConfiguration.identifier)
let domain: StorageDomain = .appGroupUserDefaults(identifier: StorageServiceIdentifiers.appGroupIdentifier)
let security: SecurityPolicy = .none
let serializer: Serializer<[String: AnyCodable]> = .json
let owner = "SampleApp"

View File

@ -1,6 +1,6 @@
//
// EncryptedStorageDemo.swift
// SecureStorgageSample
// SecureStorageSample
//
// Demonstrates encrypted file storage with LocalData package.
//

View File

@ -1,6 +1,6 @@
//
// FileSystemDemo.swift
// SecureStorgageSample
// SecureStorageSample
//
// Demonstrates file system storage with LocalData package.
//

View File

@ -1,6 +1,6 @@
//
// KeychainDemo.swift
// SecureStorgageSample
// SecureStorageSample
//
// Demonstrates Keychain storage with LocalData package.
//

View File

@ -1,6 +1,6 @@
//
// PlatformSyncDemo.swift
// SecureStorgageSample
// SecureStorageSample
//
// Demonstrates platform availability and sync policies with LocalData package.
//

View File

@ -1,6 +1,6 @@
//
// UserDefaultsDemo.swift
// SecureStorgageSample
// SecureStorageSample
//
// Demonstrates UserDefaults storage with LocalData package.
//

View File

@ -1,14 +1,14 @@
//
// SecureStorgageSampleTests.swift
// SecureStorgageSampleTests
// SecureStorageSampleTests.swift
// SecureStorageSampleTests
//
// Created by Matt Bruce on 1/13/26.
//
import Testing
@testable import SecureStorgageSample
@testable import SecureStorageSample
struct SecureStorgageSampleTests {
struct SecureStorageSampleTests {
@Test func example() async throws {
// Write your test here and use APIs like `#expect(...)` to check expected conditions.

View File

@ -1,13 +1,13 @@
//
// SecureStorgageSampleUITests.swift
// SecureStorgageSampleUITests
// SecureStorageSampleUITests.swift
// SecureStorageSampleUITests
//
// Created by Matt Bruce on 1/13/26.
//
import XCTest
final class SecureStorgageSampleUITests: XCTestCase {
final class SecureStorageSampleUITests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.

View File

@ -1,13 +1,13 @@
//
// SecureStorgageSampleUITestsLaunchTests.swift
// SecureStorgageSampleUITests
// SecureStorageSampleUITestsLaunchTests.swift
// SecureStorageSampleUITests
//
// Created by Matt Bruce on 1/13/26.
//
import XCTest
final class SecureStorgageSampleUITestsLaunchTests: XCTestCase {
final class SecureStorageSampleUITestsLaunchTests: XCTestCase {
override class var runsForEachTargetApplicationUIConfiguration: Bool {
true

View File

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>SecureStorageSample Watch App.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
</dict>
<key>SecureStorageWatch Watch App.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
</dict>
<key>SecureStorgageSample.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
<key>SecureStorgageSampleWatch.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>3</integer>
</dict>
</dict>
</dict>
</plist>

View File

@ -1,8 +0,0 @@
import Foundation
import SharedKit
enum AppGroupConfiguration {
static var identifier: String {
StorageServiceIdentifiers.appGroupIdentifier
}
}

View File

@ -1,37 +0,0 @@
//
// SecureStorgageSampleApp.swift
// SecureStorgageSample
//
// Created by Matt Bruce on 1/13/26.
//
import SwiftUI
import LocalData
@main
struct SecureStorgageSampleApp: App {
init() {
_ = WatchConnectivityService.shared
do {
try StorageRouter.shared.registerCatalog(AppStorageCatalog.self)
} catch {
assertionFailure("Storage catalog registration failed: \(error)")
}
Task {
await StorageRouter.shared.registerKeyMaterialProvider(
ExternalKeyMaterialProvider(),
for: SampleKeyMaterialSources.external
)
}
#if DEBUG
let report = StorageAuditReport.renderText(for: AppStorageCatalog.self)
print(report)
#endif
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}