react-native-nitro-storage 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +89 -29
- package/README.md +81 -24
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +5 -2
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +240 -34
- package/cpp/bindings/HybridStorage.cpp +106 -283
- package/cpp/bindings/HybridStorage.hpp +16 -9
- package/docs/api-reference.md +75 -19
- package/docs/benchmarks.md +6 -1
- package/docs/keychain-lifecycle-testing.md +36 -0
- package/docs/recipes.md +1 -2
- package/docs/secure-storage.md +45 -9
- package/ios/IOSStorageAdapterCpp.mm +391 -175
- package/lib/commonjs/capabilities.js +3 -1
- package/lib/commonjs/capabilities.js.map +1 -1
- package/lib/commonjs/core/durability.js +110 -43
- package/lib/commonjs/core/durability.js.map +1 -1
- package/lib/commonjs/index.js +15 -5
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +219 -97
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/internal.js +3 -11
- package/lib/commonjs/internal.js.map +1 -1
- package/lib/commonjs/shared.js +62 -2
- package/lib/commonjs/shared.js.map +1 -1
- package/lib/commonjs/storage-core.js +852 -141
- package/lib/commonjs/storage-core.js.map +1 -1
- package/lib/commonjs/storage-runtime.js +5 -1
- package/lib/commonjs/storage-runtime.js.map +1 -1
- package/lib/commonjs/testing.js +13 -0
- package/lib/commonjs/testing.js.map +1 -1
- package/lib/module/capabilities.js +2 -1
- package/lib/module/capabilities.js.map +1 -1
- package/lib/module/core/durability.js +110 -43
- package/lib/module/core/durability.js.map +1 -1
- package/lib/module/index.js +12 -8
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +215 -99
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/internal.js +2 -9
- package/lib/module/internal.js.map +1 -1
- package/lib/module/shared.js +60 -2
- package/lib/module/shared.js.map +1 -1
- package/lib/module/storage-core.js +853 -142
- package/lib/module/storage-core.js.map +1 -1
- package/lib/module/storage-runtime.js +4 -1
- package/lib/module/storage-runtime.js.map +1 -1
- package/lib/module/testing.js +8 -1
- package/lib/module/testing.js.map +1 -1
- package/lib/typescript/capabilities.d.ts +2 -1
- package/lib/typescript/capabilities.d.ts.map +1 -1
- package/lib/typescript/core/durability.d.ts +7 -2
- package/lib/typescript/core/durability.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -2
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +3 -29
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/internal.d.ts +0 -2
- package/lib/typescript/internal.d.ts.map +1 -1
- package/lib/typescript/shared.d.ts +19 -0
- package/lib/typescript/shared.d.ts.map +1 -1
- package/lib/typescript/storage-core.d.ts +15 -6
- package/lib/typescript/storage-core.d.ts.map +1 -1
- package/lib/typescript/storage-runtime.d.ts +2 -1
- package/lib/typescript/storage-runtime.d.ts.map +1 -1
- package/lib/typescript/testing.d.ts +1 -1
- package/lib/typescript/testing.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/capabilities.ts +3 -1
- package/src/core/durability.ts +139 -39
- package/src/index.ts +20 -10
- package/src/index.web.ts +328 -166
- package/src/internal.ts +0 -14
- package/src/shared.ts +85 -0
- package/src/storage-core.ts +1165 -202
- package/src/storage-runtime.ts +6 -0
- package/src/testing.ts +8 -1
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
#import <Security/Security.h>
|
|
4
4
|
#import <LocalAuthentication/LocalAuthentication.h>
|
|
5
5
|
|
|
6
|
+
#include <unordered_set>
|
|
7
|
+
#include <utility>
|
|
8
|
+
#include <vector>
|
|
9
|
+
|
|
6
10
|
namespace NitroStorage {
|
|
7
11
|
|
|
8
12
|
static NSString* const kKeychainService = @"com.nitrostorage.keychain";
|
|
@@ -37,24 +41,18 @@ static NSUserDefaults* NitroDiskDefaults() {
|
|
|
37
41
|
return defaults ?: [NSUserDefaults standardUserDefaults];
|
|
38
42
|
}
|
|
39
43
|
|
|
40
|
-
// --- Legacy disk key registry ---
|
|
41
|
-
// Versions before the suite domain stored Disk values in standardUserDefaults.
|
|
42
|
-
// The registry records legacy keys observed through the storage API so that
|
|
43
|
-
// clearDisk can remove both stores and deleted legacy values cannot reappear.
|
|
44
|
-
// Only keys observed through the storage API are ever registered; the host
|
|
45
|
-
// app's own standard defaults keys are never enumerated or touched.
|
|
46
|
-
|
|
47
44
|
static NSSet<NSString*>* registeredLegacyDiskKeys() {
|
|
48
45
|
NSArray* stored = [NitroDiskDefaults() stringArrayForKey:kLegacyDiskKeysRegistryKey];
|
|
49
46
|
return stored ? [NSSet setWithArray:stored] : [NSSet set];
|
|
50
47
|
}
|
|
51
48
|
|
|
52
49
|
static void persistLegacyDiskKeys(NSSet<NSString*>* keys) {
|
|
50
|
+
NSUserDefaults* defaults = NitroDiskDefaults();
|
|
53
51
|
if (keys.count == 0) {
|
|
54
|
-
[
|
|
52
|
+
[defaults removeObjectForKey:kLegacyDiskKeysRegistryKey];
|
|
55
53
|
return;
|
|
56
54
|
}
|
|
57
|
-
[
|
|
55
|
+
[defaults setObject:[keys allObjects] forKey:kLegacyDiskKeysRegistryKey];
|
|
58
56
|
}
|
|
59
57
|
|
|
60
58
|
static void registerLegacyDiskKey(NSString* key) {
|
|
@@ -80,6 +78,123 @@ static void unregisterLegacyDiskKeys(NSArray<NSString*>* keys) {
|
|
|
80
78
|
}
|
|
81
79
|
}
|
|
82
80
|
|
|
81
|
+
// --- Legacy disk key migration ---
|
|
82
|
+
// Versions before the suite domain stored Disk values in standardUserDefaults.
|
|
83
|
+
// A conservative, retryable cutover runs at adapter initialization. A valid
|
|
84
|
+
// registry is copied into the suite domain and each source is removed only
|
|
85
|
+
// after a target readback confirms the copy. Malformed registries, fallback
|
|
86
|
+
// domains, and persistence failures remain untouched for a later retry.
|
|
87
|
+
|
|
88
|
+
static NSString* const kLegacyDiskMigrationMarkerKey =
|
|
89
|
+
@"__nitro_storage_legacy_disk_migration_v1__";
|
|
90
|
+
|
|
91
|
+
static void runLegacyDiskMigrationCutover(
|
|
92
|
+
NSUserDefaults* defaults,
|
|
93
|
+
NSUserDefaults* standard
|
|
94
|
+
) {
|
|
95
|
+
if ([defaults boolForKey:kLegacyDiskMigrationMarkerKey]) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (defaults == standard) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
id registryValue = [defaults objectForKey:kLegacyDiskKeysRegistryKey];
|
|
104
|
+
if (registryValue == nil || ![registryValue isKindOfClass:[NSArray class]]) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
NSArray* registry = (NSArray*)registryValue;
|
|
109
|
+
NSMutableArray<NSString*>* keys = [NSMutableArray arrayWithCapacity:registry.count];
|
|
110
|
+
for (id rawKey in registry) {
|
|
111
|
+
if (![rawKey isKindOfClass:[NSString class]]) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
NSString* key = (NSString*)rawKey;
|
|
115
|
+
if (key.length == 0 ||
|
|
116
|
+
[key isEqualToString:kLegacyDiskKeysRegistryKey] ||
|
|
117
|
+
[key isEqualToString:kLegacyDiskMigrationMarkerKey]) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
[keys addObject:key];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (NSString* key in keys) {
|
|
124
|
+
id legacyValue = [standard objectForKey:key];
|
|
125
|
+
if (legacyValue == nil) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (![legacyValue isKindOfClass:[NSString class]]) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
id targetValue = [defaults objectForKey:key];
|
|
133
|
+
if (targetValue == nil) {
|
|
134
|
+
[defaults setObject:legacyValue forKey:key];
|
|
135
|
+
}
|
|
136
|
+
if (![defaults synchronize] ||
|
|
137
|
+
![[defaults objectForKey:key] isEqual:legacyValue]) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
[standard removeObjectForKey:key];
|
|
142
|
+
if (![standard synchronize] || [standard objectForKey:key] != nil) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
[defaults removeObjectForKey:kLegacyDiskKeysRegistryKey];
|
|
148
|
+
if (![defaults synchronize] ||
|
|
149
|
+
[defaults objectForKey:kLegacyDiskKeysRegistryKey] != nil) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
[defaults setBool:YES forKey:kLegacyDiskMigrationMarkerKey];
|
|
154
|
+
if (![defaults synchronize] ||
|
|
155
|
+
![defaults boolForKey:kLegacyDiskMigrationMarkerKey]) {
|
|
156
|
+
[defaults removeObjectForKey:kLegacyDiskMigrationMarkerKey];
|
|
157
|
+
[defaults synchronize];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#if defined(NITRO_STORAGE_TESTING)
|
|
162
|
+
void runLegacyDiskMigrationCutoverForTesting(NSUserDefaults* defaults) {
|
|
163
|
+
runLegacyDiskMigrationCutover(defaults, [NSUserDefaults standardUserDefaults]);
|
|
164
|
+
}
|
|
165
|
+
#endif
|
|
166
|
+
|
|
167
|
+
static NSString* migrateLegacyDiskValue(NSString* key) {
|
|
168
|
+
NSUserDefaults* defaults = NitroDiskDefaults();
|
|
169
|
+
NSString* result = [defaults stringForKey:key];
|
|
170
|
+
if (result) {
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
NSUserDefaults* standard = [NSUserDefaults standardUserDefaults];
|
|
175
|
+
NSString* legacyValue = [standard stringForKey:key];
|
|
176
|
+
if (!legacyValue) {
|
|
177
|
+
return nil;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
registerLegacyDiskKey(key);
|
|
181
|
+
if (defaults == standard) {
|
|
182
|
+
return legacyValue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
[defaults setObject:legacyValue forKey:key];
|
|
186
|
+
if (![defaults synchronize] ||
|
|
187
|
+
![[defaults stringForKey:key] isEqualToString:legacyValue]) {
|
|
188
|
+
return legacyValue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
[standard removeObjectForKey:key];
|
|
192
|
+
if ([standard synchronize] && [standard objectForKey:key] == nil) {
|
|
193
|
+
unregisterLegacyDiskKeys(@[key]);
|
|
194
|
+
}
|
|
195
|
+
return [defaults stringForKey:key] ?: legacyValue;
|
|
196
|
+
}
|
|
197
|
+
|
|
83
198
|
// Prevents the Keychain from showing auth UI. On iOS 14+ kSecUseAuthenticationUIFail is
|
|
84
199
|
// deprecated; the correct replacement is an LAContext with interactionNotAllowed = YES.
|
|
85
200
|
static void disableKeychainInteraction(NSMutableDictionary* query) {
|
|
@@ -88,6 +203,34 @@ static void disableKeychainInteraction(NSMutableDictionary* query) {
|
|
|
88
203
|
query[(__bridge id)kSecUseAuthenticationContext] = ctx;
|
|
89
204
|
}
|
|
90
205
|
|
|
206
|
+
struct BiometricKeychainSnapshot {
|
|
207
|
+
bool present{false};
|
|
208
|
+
std::string value;
|
|
209
|
+
SecAccessControlRef accessControl{nullptr};
|
|
210
|
+
|
|
211
|
+
BiometricKeychainSnapshot() = default;
|
|
212
|
+
BiometricKeychainSnapshot(const BiometricKeychainSnapshot&) = delete;
|
|
213
|
+
BiometricKeychainSnapshot& operator=(const BiometricKeychainSnapshot&) = delete;
|
|
214
|
+
BiometricKeychainSnapshot(BiometricKeychainSnapshot&& other) noexcept
|
|
215
|
+
: present(other.present),
|
|
216
|
+
value(std::move(other.value)),
|
|
217
|
+
accessControl(other.accessControl) {
|
|
218
|
+
other.accessControl = nullptr;
|
|
219
|
+
}
|
|
220
|
+
BiometricKeychainSnapshot& operator=(BiometricKeychainSnapshot&& other) noexcept {
|
|
221
|
+
if (this == &other) return *this;
|
|
222
|
+
if (accessControl) CFRelease(accessControl);
|
|
223
|
+
present = other.present;
|
|
224
|
+
value = std::move(other.value);
|
|
225
|
+
accessControl = other.accessControl;
|
|
226
|
+
other.accessControl = nullptr;
|
|
227
|
+
return *this;
|
|
228
|
+
}
|
|
229
|
+
~BiometricKeychainSnapshot() {
|
|
230
|
+
if (accessControl) CFRelease(accessControl);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
91
234
|
static CFStringRef accessControlAttr(int level) {
|
|
92
235
|
switch (level) {
|
|
93
236
|
case 1: return kSecAttrAccessibleAfterFirstUnlock;
|
|
@@ -99,7 +242,12 @@ static CFStringRef accessControlAttr(int level) {
|
|
|
99
242
|
}
|
|
100
243
|
}
|
|
101
244
|
|
|
102
|
-
IOSStorageAdapterCpp::IOSStorageAdapterCpp() {
|
|
245
|
+
IOSStorageAdapterCpp::IOSStorageAdapterCpp() {
|
|
246
|
+
runLegacyDiskMigrationCutover(
|
|
247
|
+
NitroDiskDefaults(),
|
|
248
|
+
[NSUserDefaults standardUserDefaults]
|
|
249
|
+
);
|
|
250
|
+
}
|
|
103
251
|
IOSStorageAdapterCpp::~IOSStorageAdapterCpp() {}
|
|
104
252
|
|
|
105
253
|
// --- Disk ---
|
|
@@ -110,7 +258,7 @@ void IOSStorageAdapterCpp::setDisk(const std::string& key, const std::string& va
|
|
|
110
258
|
NSUserDefaults* defaults = NitroDiskDefaults();
|
|
111
259
|
[defaults setObject:nsValue forKey:nsKey];
|
|
112
260
|
NSUserDefaults* standard = [NSUserDefaults standardUserDefaults];
|
|
113
|
-
if ([standard objectForKey:nsKey] != nil) {
|
|
261
|
+
if (defaults != standard && [standard objectForKey:nsKey] != nil) {
|
|
114
262
|
[standard removeObjectForKey:nsKey];
|
|
115
263
|
unregisterLegacyDiskKeys(@[nsKey]);
|
|
116
264
|
}
|
|
@@ -118,29 +266,17 @@ void IOSStorageAdapterCpp::setDisk(const std::string& key, const std::string& va
|
|
|
118
266
|
|
|
119
267
|
std::optional<std::string> IOSStorageAdapterCpp::getDisk(const std::string& key) {
|
|
120
268
|
NSString* nsKey = [NSString stringWithUTF8String:key.c_str()];
|
|
121
|
-
|
|
122
|
-
NSString* result = [defaults stringForKey:nsKey];
|
|
123
|
-
|
|
124
|
-
if (!result) {
|
|
125
|
-
NSUserDefaults* legacyDefaults = [NSUserDefaults standardUserDefaults];
|
|
126
|
-
NSString* legacyValue = [legacyDefaults stringForKey:nsKey];
|
|
127
|
-
if (legacyValue) {
|
|
128
|
-
[defaults setObject:legacyValue forKey:nsKey];
|
|
129
|
-
[legacyDefaults removeObjectForKey:nsKey];
|
|
130
|
-
unregisterLegacyDiskKeys(@[nsKey]);
|
|
131
|
-
result = legacyValue;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
269
|
+
NSString* result = migrateLegacyDiskValue(nsKey);
|
|
135
270
|
if (!result) return std::nullopt;
|
|
136
271
|
return std::string([result UTF8String]);
|
|
137
272
|
}
|
|
138
273
|
|
|
139
274
|
void IOSStorageAdapterCpp::deleteDisk(const std::string& key) {
|
|
140
275
|
NSString* nsKey = [NSString stringWithUTF8String:key.c_str()];
|
|
141
|
-
|
|
276
|
+
NSUserDefaults* defaults = NitroDiskDefaults();
|
|
277
|
+
[defaults removeObjectForKey:nsKey];
|
|
142
278
|
NSUserDefaults* standard = [NSUserDefaults standardUserDefaults];
|
|
143
|
-
if ([standard objectForKey:nsKey] != nil) {
|
|
279
|
+
if (defaults != standard && [standard objectForKey:nsKey] != nil) {
|
|
144
280
|
[standard removeObjectForKey:nsKey];
|
|
145
281
|
}
|
|
146
282
|
unregisterLegacyDiskKeys(@[nsKey]);
|
|
@@ -148,8 +284,10 @@ void IOSStorageAdapterCpp::deleteDisk(const std::string& key) {
|
|
|
148
284
|
|
|
149
285
|
bool IOSStorageAdapterCpp::hasDisk(const std::string& key) {
|
|
150
286
|
NSString* nsKey = [NSString stringWithUTF8String:key.c_str()];
|
|
151
|
-
|
|
152
|
-
|
|
287
|
+
NSUserDefaults* defaults = NitroDiskDefaults();
|
|
288
|
+
if ([defaults objectForKey:nsKey] != nil) {
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
153
291
|
if ([[NSUserDefaults standardUserDefaults] stringForKey:nsKey] != nil) {
|
|
154
292
|
registerLegacyDiskKey(nsKey);
|
|
155
293
|
return true;
|
|
@@ -160,16 +298,17 @@ bool IOSStorageAdapterCpp::hasDisk(const std::string& key) {
|
|
|
160
298
|
std::vector<std::string> IOSStorageAdapterCpp::getAllKeysDisk() {
|
|
161
299
|
NSUserDefaults* defaults = NitroDiskDefaults();
|
|
162
300
|
NSDictionary<NSString*, id>* entries = [defaults persistentDomainForName:kDiskSuiteName] ?: @{};
|
|
301
|
+
NSUserDefaults* standard = [NSUserDefaults standardUserDefaults];
|
|
163
302
|
std::unordered_set<std::string> combined;
|
|
164
303
|
for (NSString* key in entries) {
|
|
165
|
-
if (![key isEqualToString:kLegacyDiskKeysRegistryKey]
|
|
304
|
+
if (![key isEqualToString:kLegacyDiskKeysRegistryKey] &&
|
|
305
|
+
![key isEqualToString:kLegacyDiskMigrationMarkerKey]) {
|
|
166
306
|
combined.insert(std::string([key UTF8String]));
|
|
167
307
|
}
|
|
168
308
|
}
|
|
169
|
-
// Only keys observed through the storage API are registered legacy keys;
|
|
170
|
-
// arbitrary host-app standard defaults keys are never enumerated.
|
|
171
309
|
for (NSString* key in [registeredLegacyDiskKeys() allObjects]) {
|
|
172
|
-
if (entries
|
|
310
|
+
if ([entries objectForKey:key] == nil &&
|
|
311
|
+
[standard stringForKey:key] != nil) {
|
|
173
312
|
combined.insert(std::string([key UTF8String]));
|
|
174
313
|
}
|
|
175
314
|
}
|
|
@@ -208,7 +347,7 @@ void IOSStorageAdapterCpp::setDiskBatch(
|
|
|
208
347
|
NSString* nsKey = [NSString stringWithUTF8String:keys[i].c_str()];
|
|
209
348
|
NSString* nsValue = [NSString stringWithUTF8String:values[i].c_str()];
|
|
210
349
|
[defaults setObject:nsValue forKey:nsKey];
|
|
211
|
-
if ([standard objectForKey:nsKey] != nil) {
|
|
350
|
+
if (defaults != standard && [standard objectForKey:nsKey] != nil) {
|
|
212
351
|
[legacyKeysToRemove addObject:nsKey];
|
|
213
352
|
}
|
|
214
353
|
}
|
|
@@ -238,22 +377,25 @@ void IOSStorageAdapterCpp::deleteDiskBatch(const std::vector<std::string>& keys)
|
|
|
238
377
|
void IOSStorageAdapterCpp::clearDisk() {
|
|
239
378
|
NSUserDefaults* defaults = NitroDiskDefaults();
|
|
240
379
|
NSDictionary<NSString*, id>* entries = [defaults persistentDomainForName:kDiskSuiteName] ?: @{};
|
|
241
|
-
// Capture the registry before clearing the suite domain, which removes it.
|
|
242
380
|
NSMutableSet* legacyKeys = [registeredLegacyDiskKeys() mutableCopy];
|
|
243
381
|
for (NSString* key in entries) {
|
|
382
|
+
if ([key isEqualToString:kLegacyDiskKeysRegistryKey] ||
|
|
383
|
+
[key isEqualToString:kLegacyDiskMigrationMarkerKey]) {
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
244
386
|
[defaults removeObjectForKey:key];
|
|
245
387
|
}
|
|
246
|
-
// Clear the standard-defaults copies of every storage-owned key so legacy
|
|
247
|
-
// values deleted here cannot reappear through the read-time migration path.
|
|
248
388
|
NSUserDefaults* standard = [NSUserDefaults standardUserDefaults];
|
|
249
|
-
|
|
250
|
-
|
|
389
|
+
if (defaults != standard) {
|
|
390
|
+
for (NSString* key in entries) {
|
|
391
|
+
if ([key isEqualToString:kLegacyDiskKeysRegistryKey] ||
|
|
392
|
+
[key isEqualToString:kLegacyDiskMigrationMarkerKey]) {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
251
395
|
[standard removeObjectForKey:key];
|
|
252
396
|
[legacyKeys removeObject:key];
|
|
253
397
|
}
|
|
254
|
-
|
|
255
|
-
for (NSString* key in legacyKeys) {
|
|
256
|
-
if ([standard objectForKey:key] != nil) {
|
|
398
|
+
for (NSString* key in legacyKeys) {
|
|
257
399
|
[standard removeObjectForKey:key];
|
|
258
400
|
}
|
|
259
401
|
}
|
|
@@ -274,6 +416,68 @@ static NSMutableDictionary* baseKeychainQuery(NSString* key, NSString* service,
|
|
|
274
416
|
return query;
|
|
275
417
|
}
|
|
276
418
|
|
|
419
|
+
static void throwIfDeleteFailed(OSStatus status, const std::string& operation) {
|
|
420
|
+
if (status == errSecSuccess || status == errSecItemNotFound) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (status == errSecInteractionNotAllowed) {
|
|
424
|
+
throw taggedStorageError(
|
|
425
|
+
"keychain_locked",
|
|
426
|
+
"NitroStorage: Keychain is locked (errSecInteractionNotAllowed). " + operation
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
throw keychainStatusError(status, operation);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
static BiometricKeychainSnapshot captureBiometricValue(
|
|
433
|
+
NSString* nsKey,
|
|
434
|
+
NSString* group
|
|
435
|
+
) {
|
|
436
|
+
BiometricKeychainSnapshot snapshot;
|
|
437
|
+
NSMutableDictionary* query = baseKeychainQuery(nsKey, kBiometricKeychainService, group);
|
|
438
|
+
query[(__bridge id)kSecReturnAttributes] = @YES;
|
|
439
|
+
query[(__bridge id)kSecReturnData] = @YES;
|
|
440
|
+
query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
|
|
441
|
+
disableKeychainInteraction(query);
|
|
442
|
+
|
|
443
|
+
CFTypeRef result = NULL;
|
|
444
|
+
const OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
|
|
445
|
+
if (status == errSecItemNotFound) {
|
|
446
|
+
return snapshot;
|
|
447
|
+
}
|
|
448
|
+
if (status == errSecInteractionNotAllowed) {
|
|
449
|
+
throw taggedStorageError(
|
|
450
|
+
"keychain_locked",
|
|
451
|
+
"NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
|
|
452
|
+
"The biometric item is not accessible until the device is unlocked."
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
if (status != errSecSuccess || !result) {
|
|
456
|
+
if (result) CFRelease(result);
|
|
457
|
+
throw keychainStatusError(status, "Biometric snapshot");
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
NSDictionary* attributes = (__bridge NSDictionary*)result;
|
|
461
|
+
NSData* data = attributes[(__bridge id)kSecValueData];
|
|
462
|
+
id accessControl = attributes[(__bridge id)kSecAttrAccessControl];
|
|
463
|
+
if (!data || !accessControl) {
|
|
464
|
+
CFRelease(result);
|
|
465
|
+
throw std::runtime_error(
|
|
466
|
+
"NitroStorage: Biometric snapshot did not include value data and access control"
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
NSString* stringValue = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
|
470
|
+
if (!stringValue) {
|
|
471
|
+
CFRelease(result);
|
|
472
|
+
throw std::runtime_error("NitroStorage: Biometric snapshot value is not UTF-8");
|
|
473
|
+
}
|
|
474
|
+
snapshot.present = true;
|
|
475
|
+
snapshot.value = std::string([stringValue UTF8String]);
|
|
476
|
+
snapshot.accessControl = (SecAccessControlRef)CFRetain((__bridge CFTypeRef)accessControl);
|
|
477
|
+
CFRelease(result);
|
|
478
|
+
return snapshot;
|
|
479
|
+
}
|
|
480
|
+
|
|
277
481
|
static NSMutableDictionary* allAccountsQuery(NSString* service, NSString* accessGroup) {
|
|
278
482
|
NSMutableDictionary* query = [@{
|
|
279
483
|
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
|
|
@@ -358,29 +562,20 @@ static std::optional<std::string> getSecureValue(NSString* nsKey, NSString* grou
|
|
|
358
562
|
"The item is not accessible until the device is unlocked."
|
|
359
563
|
);
|
|
360
564
|
}
|
|
361
|
-
|
|
565
|
+
if (status == errSecItemNotFound) {
|
|
566
|
+
return std::nullopt;
|
|
567
|
+
}
|
|
568
|
+
throw keychainStatusError(status, "Secure get");
|
|
362
569
|
}
|
|
363
570
|
|
|
364
571
|
static void deleteSecureValue(NSString* nsKey, NSString* group) {
|
|
365
572
|
NSMutableDictionary* secureQuery = baseKeychainQuery(nsKey, kKeychainService, group);
|
|
366
|
-
OSStatus secureStatus = SecItemDelete((__bridge CFDictionaryRef)secureQuery);
|
|
367
|
-
|
|
368
|
-
throw taggedStorageError(
|
|
369
|
-
"keychain_locked",
|
|
370
|
-
"NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
|
|
371
|
-
"The item is not accessible until the device is unlocked."
|
|
372
|
-
);
|
|
373
|
-
}
|
|
573
|
+
const OSStatus secureStatus = SecItemDelete((__bridge CFDictionaryRef)secureQuery);
|
|
574
|
+
throwIfDeleteFailed(secureStatus, "Secure delete");
|
|
374
575
|
|
|
375
576
|
NSMutableDictionary* biometricQuery = baseKeychainQuery(nsKey, kBiometricKeychainService, group);
|
|
376
|
-
OSStatus biometricStatus = SecItemDelete((__bridge CFDictionaryRef)biometricQuery);
|
|
377
|
-
|
|
378
|
-
throw taggedStorageError(
|
|
379
|
-
"keychain_locked",
|
|
380
|
-
"NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
|
|
381
|
-
"The item is not accessible until the device is unlocked."
|
|
382
|
-
);
|
|
383
|
-
}
|
|
577
|
+
const OSStatus biometricStatus = SecItemDelete((__bridge CFDictionaryRef)biometricQuery);
|
|
578
|
+
throwIfDeleteFailed(biometricStatus, "Biometric delete");
|
|
384
579
|
}
|
|
385
580
|
|
|
386
581
|
// Deletes only the plain (non-biometric) keychain copy. Promotion to biometric
|
|
@@ -388,13 +583,42 @@ static void deleteSecureValue(NSString* nsKey, NSString* group) {
|
|
|
388
583
|
static void deletePlainSecureValue(NSString* nsKey, NSString* group) {
|
|
389
584
|
NSMutableDictionary* secureQuery = baseKeychainQuery(nsKey, kKeychainService, group);
|
|
390
585
|
OSStatus secureStatus = SecItemDelete((__bridge CFDictionaryRef)secureQuery);
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
586
|
+
throwIfDeleteFailed(secureStatus, "Plain secure delete");
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
static void deleteBiometricValue(NSString* nsKey, NSString* group) {
|
|
590
|
+
NSMutableDictionary* biometricQuery = baseKeychainQuery(nsKey, kBiometricKeychainService, group);
|
|
591
|
+
const OSStatus status = SecItemDelete((__bridge CFDictionaryRef)biometricQuery);
|
|
592
|
+
throwIfDeleteFailed(status, "Biometric delete");
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
static void restoreBiometricValue(
|
|
596
|
+
NSString* nsKey,
|
|
597
|
+
NSString* group,
|
|
598
|
+
const BiometricKeychainSnapshot& snapshot
|
|
599
|
+
) {
|
|
600
|
+
deleteBiometricValue(nsKey, group);
|
|
601
|
+
if (!snapshot.present) {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (!snapshot.accessControl) {
|
|
605
|
+
throw std::runtime_error(
|
|
606
|
+
"NitroStorage: Previous biometric item has no access control"
|
|
396
607
|
);
|
|
397
608
|
}
|
|
609
|
+
NSMutableDictionary* query = baseKeychainQuery(nsKey, kBiometricKeychainService, group);
|
|
610
|
+
query[(__bridge id)kSecValueData] = [nsStringFromStdString(snapshot.value) dataUsingEncoding:NSUTF8StringEncoding];
|
|
611
|
+
query[(__bridge id)kSecAttrAccessControl] = (__bridge id)snapshot.accessControl;
|
|
612
|
+
const OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
|
|
613
|
+
if (status != errSecSuccess) {
|
|
614
|
+
if (status == errSecInteractionNotAllowed) {
|
|
615
|
+
throw taggedStorageError(
|
|
616
|
+
"keychain_locked",
|
|
617
|
+
"NitroStorage: Keychain is locked (errSecInteractionNotAllowed) while restoring biometric storage"
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
throw keychainStatusError(status, "Biometric restore");
|
|
621
|
+
}
|
|
398
622
|
}
|
|
399
623
|
|
|
400
624
|
static std::vector<std::string> keychainAccountsForService(NSString* service, NSString* accessGroup) {
|
|
@@ -647,137 +871,116 @@ void IOSStorageAdapterCpp::setSecureBiometric(const std::string& key, const std:
|
|
|
647
871
|
}
|
|
648
872
|
|
|
649
873
|
void IOSStorageAdapterCpp::setSecureBiometricWithLevel(const std::string& key, const std::string& value, int level) {
|
|
874
|
+
if (level < 0 || level > 2) {
|
|
875
|
+
throw std::runtime_error("NitroStorage: Invalid biometric level");
|
|
876
|
+
}
|
|
650
877
|
NSString* nsKey = [NSString stringWithUTF8String:key.c_str()];
|
|
651
|
-
NSData* data =
|
|
878
|
+
NSData* data = nsDataFromStdString(value);
|
|
652
879
|
std::string groupStr;
|
|
880
|
+
int accessControlLevel;
|
|
653
881
|
{
|
|
654
882
|
std::lock_guard<std::mutex> lock(accessGroupMutex_);
|
|
655
883
|
groupStr = keychainAccessGroup_;
|
|
884
|
+
accessControlLevel = accessControlLevel_;
|
|
656
885
|
}
|
|
657
886
|
NSString* group = groupStr.empty() ? nil : [NSString stringWithUTF8String:groupStr.c_str()];
|
|
658
887
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
SecItemDelete((__bridge CFDictionaryRef)deleteQuery);
|
|
663
|
-
markBiometricKeyRemoved(key);
|
|
888
|
+
const BiometricKeychainSnapshot previousBiometric = captureBiometricValue(nsKey, group);
|
|
889
|
+
const std::optional<std::string> previousPlain = getSecureValue(nsKey, group);
|
|
890
|
+
std::vector<std::string> rollbackErrors;
|
|
664
891
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
{
|
|
672
|
-
NSMutableDictionary* backupQuery = baseKeychainQuery(nsKey, kBiometricKeychainService, group);
|
|
673
|
-
backupQuery[(__bridge id)kSecReturnData] = @YES;
|
|
674
|
-
backupQuery[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
|
|
675
|
-
disableKeychainInteraction(backupQuery);
|
|
676
|
-
CFTypeRef backupResult = NULL;
|
|
677
|
-
if (SecItemCopyMatching((__bridge CFDictionaryRef)backupQuery, &backupResult) == errSecSuccess && backupResult) {
|
|
678
|
-
NSData* bData = (__bridge_transfer NSData*)backupResult;
|
|
679
|
-
NSString* str = [[NSString alloc] initWithData:bData encoding:NSUTF8StringEncoding];
|
|
680
|
-
if (str) backup = std::string([str UTF8String]);
|
|
681
|
-
} else if (backupResult) {
|
|
682
|
-
CFRelease(backupResult);
|
|
892
|
+
try {
|
|
893
|
+
if (level == 0) {
|
|
894
|
+
deleteBiometricValue(nsKey, group);
|
|
895
|
+
markBiometricKeyRemoved(key);
|
|
896
|
+
setSecure(key, value);
|
|
897
|
+
return;
|
|
683
898
|
}
|
|
684
|
-
}
|
|
685
899
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
flags,
|
|
699
|
-
&error
|
|
700
|
-
);
|
|
701
|
-
|
|
702
|
-
if (error || !access) {
|
|
703
|
-
if (access) CFRelease(access);
|
|
704
|
-
if (error) CFRelease(error);
|
|
705
|
-
throw taggedStorageError(
|
|
706
|
-
"biometric_unavailable",
|
|
707
|
-
"NitroStorage: Failed to create biometric access control"
|
|
900
|
+
// A biometric item's access control cannot be updated in place. Delete
|
|
901
|
+
// the old item only after its value and ACL have been captured.
|
|
902
|
+
deleteBiometricValue(nsKey, group);
|
|
903
|
+
|
|
904
|
+
CFErrorRef error = NULL;
|
|
905
|
+
const SecAccessControlCreateFlags flags =
|
|
906
|
+
level == 1 ? kSecAccessControlUserPresence : kSecAccessControlBiometryCurrentSet;
|
|
907
|
+
SecAccessControlRef access = SecAccessControlCreateWithFlags(
|
|
908
|
+
kCFAllocatorDefault,
|
|
909
|
+
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
|
|
910
|
+
flags,
|
|
911
|
+
&error
|
|
708
912
|
);
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
attrs[(__bridge id)kSecValueData] = data;
|
|
713
|
-
attrs[(__bridge id)kSecAttrAccessControl] = (__bridge_transfer id)access;
|
|
714
|
-
|
|
715
|
-
OSStatus addStatus = SecItemAdd((__bridge CFDictionaryRef)attrs, NULL);
|
|
716
|
-
if (addStatus != errSecSuccess) {
|
|
717
|
-
if (backup.has_value()) {
|
|
718
|
-
try {
|
|
719
|
-
setSecure(key, *backup);
|
|
720
|
-
} catch (const std::exception& restoreEx) {
|
|
721
|
-
throw taggedStorageError(
|
|
722
|
-
"biometric_unavailable",
|
|
723
|
-
std::string("NitroStorage: Biometric set failed with status ") +
|
|
724
|
-
std::to_string(addStatus) +
|
|
725
|
-
" and previous value restoration also failed: " + restoreEx.what());
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
if (addStatus == errSecInteractionNotAllowed) {
|
|
913
|
+
if (error || !access) {
|
|
914
|
+
if (error) CFRelease(error);
|
|
915
|
+
if (access) CFRelease(access);
|
|
729
916
|
throw taggedStorageError(
|
|
730
|
-
"
|
|
731
|
-
"NitroStorage:
|
|
732
|
-
"The item is not accessible until the device is unlocked."
|
|
917
|
+
"biometric_unavailable",
|
|
918
|
+
"NitroStorage: Failed to create biometric access control"
|
|
733
919
|
);
|
|
734
920
|
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
921
|
+
|
|
922
|
+
NSMutableDictionary* attrs = baseKeychainQuery(nsKey, kBiometricKeychainService, group);
|
|
923
|
+
attrs[(__bridge id)kSecValueData] = data;
|
|
924
|
+
attrs[(__bridge id)kSecAttrAccessControl] = (__bridge_transfer id)access;
|
|
925
|
+
const OSStatus addStatus = SecItemAdd((__bridge CFDictionaryRef)attrs, NULL);
|
|
926
|
+
if (addStatus != errSecSuccess) {
|
|
927
|
+
if (addStatus == errSecInteractionNotAllowed) {
|
|
928
|
+
throw taggedStorageError(
|
|
929
|
+
"keychain_locked",
|
|
930
|
+
"NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
|
|
931
|
+
"The biometric item is not accessible until the device is unlocked."
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
throw keychainStatusError(addStatus, "Biometric set");
|
|
740
935
|
}
|
|
741
|
-
throw taggedStorageError(
|
|
742
|
-
"biometric_unavailable",
|
|
743
|
-
std::string("NitroStorage: Biometric set failed with status ") +
|
|
744
|
-
std::to_string(addStatus) +
|
|
745
|
-
(backup.has_value() ? " (previous value restored to non-biometric keychain)" : " (no previous value)"));
|
|
746
|
-
}
|
|
747
936
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
// If removing the plain copy fails after the biometric write succeeded,
|
|
751
|
-
// roll the biometric write back so the promotion either fully succeeds or
|
|
752
|
-
// leaves the previous state intact, then rethrow the original error.
|
|
753
|
-
try {
|
|
937
|
+
// A successful promotion has exactly one representation. If this
|
|
938
|
+
// delete fails, compensation restores both prior representations.
|
|
754
939
|
deletePlainSecureValue(nsKey, group);
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
940
|
+
markBiometricKeySet(key);
|
|
941
|
+
markSecureKeyRemoved(key);
|
|
942
|
+
} catch (const std::exception& primary) {
|
|
943
|
+
try {
|
|
944
|
+
restoreBiometricValue(nsKey, group, previousBiometric);
|
|
945
|
+
if (previousBiometric.present) {
|
|
946
|
+
markBiometricKeySet(key);
|
|
947
|
+
} else {
|
|
948
|
+
markBiometricKeyRemoved(key);
|
|
949
|
+
}
|
|
950
|
+
} catch (const std::exception& rollbackError) {
|
|
951
|
+
rollbackErrors.push_back(std::string("biometric: ") + rollbackError.what());
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
try {
|
|
955
|
+
if (previousPlain.has_value()) {
|
|
956
|
+
setSecureValue(
|
|
957
|
+
nsKey,
|
|
958
|
+
nsDataFromStdString(*previousPlain),
|
|
959
|
+
group,
|
|
960
|
+
accessControlLevel
|
|
961
|
+
);
|
|
962
|
+
markSecureKeySet(key);
|
|
769
963
|
} else {
|
|
770
|
-
|
|
771
|
-
|
|
964
|
+
deletePlainSecureValue(nsKey, group);
|
|
965
|
+
markSecureKeyRemoved(key);
|
|
772
966
|
}
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
|
|
967
|
+
} catch (const std::exception& rollbackError) {
|
|
968
|
+
rollbackErrors.push_back(std::string("plain: ") + rollbackError.what());
|
|
969
|
+
}
|
|
970
|
+
clearSecureKeyCache();
|
|
971
|
+
|
|
972
|
+
if (!rollbackErrors.empty()) {
|
|
973
|
+
const std::string operation =
|
|
974
|
+
level == 0 ? "Secure demotion" : "Biometric promotion";
|
|
975
|
+
throw taggedStorageError(
|
|
976
|
+
"storage_compensation_failed",
|
|
977
|
+
"NitroStorage: " + operation +
|
|
978
|
+
" failed; rollback_error_count=" +
|
|
979
|
+
std::to_string(rollbackErrors.size())
|
|
980
|
+
);
|
|
776
981
|
}
|
|
777
982
|
throw;
|
|
778
983
|
}
|
|
779
|
-
markBiometricKeySet(key);
|
|
780
|
-
markSecureKeyRemoved(key);
|
|
781
984
|
}
|
|
782
985
|
|
|
783
986
|
std::optional<std::string> IOSStorageAdapterCpp::getSecureBiometric(const std::string& key) {
|
|
@@ -812,7 +1015,10 @@ std::optional<std::string> IOSStorageAdapterCpp::getSecureBiometric(const std::s
|
|
|
812
1015
|
"NitroStorage: Biometric authentication failed"
|
|
813
1016
|
);
|
|
814
1017
|
}
|
|
815
|
-
|
|
1018
|
+
if (status == errSecItemNotFound) {
|
|
1019
|
+
return std::nullopt;
|
|
1020
|
+
}
|
|
1021
|
+
throw keychainStatusError(status, "Biometric get");
|
|
816
1022
|
}
|
|
817
1023
|
|
|
818
1024
|
void IOSStorageAdapterCpp::deleteSecureBiometric(const std::string& key) {
|
|
@@ -824,7 +1030,8 @@ void IOSStorageAdapterCpp::deleteSecureBiometric(const std::string& key) {
|
|
|
824
1030
|
}
|
|
825
1031
|
NSString* group = groupStr.empty() ? nil : [NSString stringWithUTF8String:groupStr.c_str()];
|
|
826
1032
|
NSMutableDictionary* query = baseKeychainQuery(nsKey, kBiometricKeychainService, group);
|
|
827
|
-
SecItemDelete((__bridge CFDictionaryRef)query);
|
|
1033
|
+
const OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
|
|
1034
|
+
throwIfDeleteFailed(status, "Biometric delete");
|
|
828
1035
|
markBiometricKeyRemoved(key);
|
|
829
1036
|
}
|
|
830
1037
|
|
|
@@ -838,7 +1045,16 @@ bool IOSStorageAdapterCpp::hasSecureBiometric(const std::string& key) {
|
|
|
838
1045
|
NSString* group = groupStr.empty() ? nil : [NSString stringWithUTF8String:groupStr.c_str()];
|
|
839
1046
|
NSMutableDictionary* query = baseKeychainQuery(nsKey, kBiometricKeychainService, group);
|
|
840
1047
|
disableKeychainInteraction(query);
|
|
841
|
-
|
|
1048
|
+
const OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, NULL);
|
|
1049
|
+
if (status == errSecSuccess) return true;
|
|
1050
|
+
if (status == errSecItemNotFound) return false;
|
|
1051
|
+
if (status == errSecInteractionNotAllowed) {
|
|
1052
|
+
throw taggedStorageError(
|
|
1053
|
+
"keychain_locked",
|
|
1054
|
+
"NitroStorage: Keychain is locked (errSecInteractionNotAllowed) while inspecting biometric storage"
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
throw keychainStatusError(status, "Biometric has");
|
|
842
1058
|
}
|
|
843
1059
|
|
|
844
1060
|
void IOSStorageAdapterCpp::clearSecureBiometric() {
|