react-native-nitro-storage 0.4.0 → 0.4.3

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.
Files changed (41) hide show
  1. package/README.md +90 -0
  2. package/android/build.gradle +0 -12
  3. package/android/consumer-rules.pro +26 -4
  4. package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +7 -10
  5. package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +0 -4
  6. package/android/src/main/cpp/cpp-adapter.cpp +3 -1
  7. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +172 -77
  8. package/cpp/bindings/HybridStorage.cpp +120 -69
  9. package/cpp/bindings/HybridStorage.hpp +4 -0
  10. package/ios/IOSStorageAdapterCpp.hpp +2 -1
  11. package/ios/IOSStorageAdapterCpp.mm +264 -49
  12. package/lib/commonjs/index.js +128 -20
  13. package/lib/commonjs/index.js.map +1 -1
  14. package/lib/commonjs/index.web.js +169 -41
  15. package/lib/commonjs/index.web.js.map +1 -1
  16. package/lib/commonjs/indexeddb-backend.js +130 -0
  17. package/lib/commonjs/indexeddb-backend.js.map +1 -0
  18. package/lib/commonjs/internal.js +51 -23
  19. package/lib/commonjs/internal.js.map +1 -1
  20. package/lib/module/index.js +121 -20
  21. package/lib/module/index.js.map +1 -1
  22. package/lib/module/index.web.js +162 -41
  23. package/lib/module/index.web.js.map +1 -1
  24. package/lib/module/indexeddb-backend.js +126 -0
  25. package/lib/module/indexeddb-backend.js.map +1 -0
  26. package/lib/module/internal.js +51 -23
  27. package/lib/module/internal.js.map +1 -1
  28. package/lib/typescript/index.d.ts +6 -0
  29. package/lib/typescript/index.d.ts.map +1 -1
  30. package/lib/typescript/index.web.d.ts +7 -1
  31. package/lib/typescript/index.web.d.ts.map +1 -1
  32. package/lib/typescript/indexeddb-backend.d.ts +29 -0
  33. package/lib/typescript/indexeddb-backend.d.ts.map +1 -0
  34. package/lib/typescript/internal.d.ts.map +1 -1
  35. package/nitrogen/generated/android/NitroStorageOnLoad.cpp +22 -17
  36. package/nitrogen/generated/android/NitroStorageOnLoad.hpp +13 -4
  37. package/package.json +7 -3
  38. package/src/index.ts +137 -27
  39. package/src/index.web.ts +182 -49
  40. package/src/indexeddb-backend.ts +143 -0
  41. package/src/internal.ts +51 -23
@@ -2,8 +2,6 @@
2
2
  #import <Foundation/Foundation.h>
3
3
  #import <Security/Security.h>
4
4
  #import <LocalAuthentication/LocalAuthentication.h>
5
- #include <algorithm>
6
- #include <unordered_set>
7
5
 
8
6
  namespace NitroStorage {
9
7
 
@@ -16,13 +14,22 @@ static NSUserDefaults* NitroDiskDefaults() {
16
14
  return defaults ?: [NSUserDefaults standardUserDefaults];
17
15
  }
18
16
 
17
+ // Prevents the Keychain from showing auth UI. On iOS 14+ kSecUseAuthenticationUIFail is
18
+ // deprecated; the correct replacement is an LAContext with interactionNotAllowed = YES.
19
+ static void disableKeychainInteraction(NSMutableDictionary* query) {
20
+ LAContext* ctx = [[LAContext alloc] init];
21
+ ctx.interactionNotAllowed = YES;
22
+ query[(__bridge id)kSecUseAuthenticationContext] = ctx;
23
+ }
24
+
19
25
  static CFStringRef accessControlAttr(int level) {
20
26
  switch (level) {
21
27
  case 1: return kSecAttrAccessibleAfterFirstUnlock;
22
28
  case 2: return kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly;
23
29
  case 3: return kSecAttrAccessibleWhenUnlockedThisDeviceOnly;
24
30
  case 4: return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
25
- default: return kSecAttrAccessibleWhenUnlocked;
31
+ case 0: return kSecAttrAccessibleWhenUnlocked;
32
+ default: return kSecAttrAccessibleAfterFirstUnlock;
26
33
  }
27
34
  }
28
35
 
@@ -36,7 +43,10 @@ void IOSStorageAdapterCpp::setDisk(const std::string& key, const std::string& va
36
43
  NSString* nsValue = [NSString stringWithUTF8String:value.c_str()];
37
44
  NSUserDefaults* defaults = NitroDiskDefaults();
38
45
  [defaults setObject:nsValue forKey:nsKey];
39
- [[NSUserDefaults standardUserDefaults] removeObjectForKey:nsKey];
46
+ NSUserDefaults* standard = [NSUserDefaults standardUserDefaults];
47
+ if ([standard objectForKey:nsKey] != nil) {
48
+ [standard removeObjectForKey:nsKey];
49
+ }
40
50
  }
41
51
 
42
52
  std::optional<std::string> IOSStorageAdapterCpp::getDisk(const std::string& key) {
@@ -61,16 +71,22 @@ std::optional<std::string> IOSStorageAdapterCpp::getDisk(const std::string& key)
61
71
  void IOSStorageAdapterCpp::deleteDisk(const std::string& key) {
62
72
  NSString* nsKey = [NSString stringWithUTF8String:key.c_str()];
63
73
  [NitroDiskDefaults() removeObjectForKey:nsKey];
64
- [[NSUserDefaults standardUserDefaults] removeObjectForKey:nsKey];
74
+ NSUserDefaults* standard = [NSUserDefaults standardUserDefaults];
75
+ if ([standard objectForKey:nsKey] != nil) {
76
+ [standard removeObjectForKey:nsKey];
77
+ }
65
78
  }
66
79
 
67
80
  bool IOSStorageAdapterCpp::hasDisk(const std::string& key) {
68
81
  NSString* nsKey = [NSString stringWithUTF8String:key.c_str()];
69
- return [NitroDiskDefaults() objectForKey:nsKey] != nil;
82
+ if ([NitroDiskDefaults() objectForKey:nsKey] != nil) return true;
83
+ // Check legacy standardUserDefaults for un-migrated keys
84
+ return [[NSUserDefaults standardUserDefaults] stringForKey:nsKey] != nil;
70
85
  }
71
86
 
72
87
  std::vector<std::string> IOSStorageAdapterCpp::getAllKeysDisk() {
73
- NSDictionary<NSString*, id>* entries = [NitroDiskDefaults() dictionaryRepresentation];
88
+ NSUserDefaults* defaults = NitroDiskDefaults();
89
+ NSDictionary<NSString*, id>* entries = [defaults persistentDomainForName:kDiskSuiteName] ?: @{};
74
90
  std::vector<std::string> keys;
75
91
  keys.reserve(entries.count);
76
92
  for (NSString* key in entries) {
@@ -92,7 +108,8 @@ std::vector<std::string> IOSStorageAdapterCpp::getKeysByPrefixDisk(const std::st
92
108
  }
93
109
 
94
110
  size_t IOSStorageAdapterCpp::sizeDisk() {
95
- return [NitroDiskDefaults() dictionaryRepresentation].count;
111
+ NSDictionary<NSString*, id>* entries = [NitroDiskDefaults() persistentDomainForName:kDiskSuiteName] ?: @{};
112
+ return entries.count;
96
113
  }
97
114
 
98
115
  void IOSStorageAdapterCpp::setDiskBatch(
@@ -100,11 +117,18 @@ void IOSStorageAdapterCpp::setDiskBatch(
100
117
  const std::vector<std::string>& values
101
118
  ) {
102
119
  NSUserDefaults* defaults = NitroDiskDefaults();
120
+ NSUserDefaults* standard = [NSUserDefaults standardUserDefaults];
121
+ NSMutableArray* legacyKeysToRemove = [NSMutableArray array];
103
122
  for (size_t i = 0; i < keys.size() && i < values.size(); ++i) {
104
123
  NSString* nsKey = [NSString stringWithUTF8String:keys[i].c_str()];
105
124
  NSString* nsValue = [NSString stringWithUTF8String:values[i].c_str()];
106
125
  [defaults setObject:nsValue forKey:nsKey];
107
- [[NSUserDefaults standardUserDefaults] removeObjectForKey:nsKey];
126
+ if ([standard objectForKey:nsKey] != nil) {
127
+ [legacyKeysToRemove addObject:nsKey];
128
+ }
129
+ }
130
+ for (NSString* key in legacyKeysToRemove) {
131
+ [standard removeObjectForKey:key];
108
132
  }
109
133
  }
110
134
 
@@ -127,7 +151,7 @@ void IOSStorageAdapterCpp::deleteDiskBatch(const std::vector<std::string>& keys)
127
151
 
128
152
  void IOSStorageAdapterCpp::clearDisk() {
129
153
  NSUserDefaults* defaults = NitroDiskDefaults();
130
- NSDictionary<NSString*, id>* entries = [defaults dictionaryRepresentation];
154
+ NSDictionary<NSString*, id>* entries = [defaults persistentDomainForName:kDiskSuiteName] ?: @{};
131
155
  for (NSString* key in entries) {
132
156
  [defaults removeObjectForKey:key];
133
157
  }
@@ -162,15 +186,31 @@ static NSMutableDictionary* allAccountsQuery(NSString* service, NSString* access
162
186
 
163
187
  static std::vector<std::string> keychainAccountsForService(NSString* service, NSString* accessGroup) {
164
188
  NSMutableDictionary* query = allAccountsQuery(service, accessGroup);
189
+ disableKeychainInteraction(query);
165
190
  CFTypeRef result = NULL;
166
191
  std::vector<std::string> keys;
167
- if (SecItemCopyMatching((__bridge CFDictionaryRef)query, &result) == errSecSuccess && result) {
168
- NSArray* items = (__bridge_transfer NSArray*)result;
169
- keys.reserve(items.count);
170
- for (NSDictionary* item in items) {
171
- NSString* account = item[(__bridge id)kSecAttrAccount];
172
- if (account) {
173
- keys.push_back(std::string([account UTF8String]));
192
+ OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
193
+ if (status == errSecInteractionNotAllowed) {
194
+ throw std::runtime_error(
195
+ "NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
196
+ "The item is not accessible until the device is unlocked."
197
+ );
198
+ }
199
+ if (status == errSecSuccess && result) {
200
+ id items = (__bridge_transfer id)result;
201
+ NSArray* itemArray = nil;
202
+ if ([items isKindOfClass:[NSArray class]]) {
203
+ itemArray = (NSArray*)items;
204
+ } else if ([items isKindOfClass:[NSDictionary class]]) {
205
+ itemArray = @[(NSDictionary*)items];
206
+ }
207
+ if (itemArray) {
208
+ keys.reserve(itemArray.count);
209
+ for (NSDictionary* item in itemArray) {
210
+ NSString* account = item[(__bridge id)kSecAttrAccount];
211
+ if (account) {
212
+ keys.push_back(std::string([account UTF8String]));
213
+ }
174
214
  }
175
215
  }
176
216
  }
@@ -180,7 +220,14 @@ static std::vector<std::string> keychainAccountsForService(NSString* service, NS
180
220
  void IOSStorageAdapterCpp::setSecure(const std::string& key, const std::string& value) {
181
221
  NSString* nsKey = [NSString stringWithUTF8String:key.c_str()];
182
222
  NSData* data = [[NSString stringWithUTF8String:value.c_str()] dataUsingEncoding:NSUTF8StringEncoding];
183
- NSString* group = keychainAccessGroup_.empty() ? nil : [NSString stringWithUTF8String:keychainAccessGroup_.c_str()];
223
+ std::string groupStr;
224
+ int accessControlLevel;
225
+ {
226
+ std::lock_guard<std::mutex> lock(accessGroupMutex_);
227
+ groupStr = keychainAccessGroup_;
228
+ accessControlLevel = accessControlLevel_;
229
+ }
230
+ NSString* group = groupStr.empty() ? nil : [NSString stringWithUTF8String:groupStr.c_str()];
184
231
  NSMutableDictionary* query = baseKeychainQuery(nsKey, kKeychainService, group);
185
232
 
186
233
  NSDictionary* updateAttributes = @{
@@ -196,9 +243,15 @@ void IOSStorageAdapterCpp::setSecure(const std::string& key, const std::string&
196
243
 
197
244
  if (status == errSecItemNotFound) {
198
245
  query[(__bridge id)kSecValueData] = data;
199
- query[(__bridge id)kSecAttrAccessible] = (__bridge id)accessControlAttr(accessControlLevel_);
246
+ query[(__bridge id)kSecAttrAccessible] = (__bridge id)accessControlAttr(accessControlLevel);
200
247
  const OSStatus addStatus = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
201
248
  if (addStatus != errSecSuccess) {
249
+ if (addStatus == errSecInteractionNotAllowed) {
250
+ throw std::runtime_error(
251
+ "NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
252
+ "The item is not accessible until the device is unlocked."
253
+ );
254
+ }
202
255
  throw std::runtime_error(
203
256
  "NitroStorage: Secure set failed with status " + std::to_string(addStatus)
204
257
  );
@@ -207,6 +260,12 @@ void IOSStorageAdapterCpp::setSecure(const std::string& key, const std::string&
207
260
  return;
208
261
  }
209
262
 
263
+ if (status == errSecInteractionNotAllowed) {
264
+ throw std::runtime_error(
265
+ "NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
266
+ "The item is not accessible until the device is unlocked."
267
+ );
268
+ }
210
269
  throw std::runtime_error(
211
270
  "NitroStorage: Secure set failed with status " + std::to_string(status)
212
271
  );
@@ -214,39 +273,81 @@ void IOSStorageAdapterCpp::setSecure(const std::string& key, const std::string&
214
273
 
215
274
  std::optional<std::string> IOSStorageAdapterCpp::getSecure(const std::string& key) {
216
275
  NSString* nsKey = [NSString stringWithUTF8String:key.c_str()];
217
- NSString* group = keychainAccessGroup_.empty() ? nil : [NSString stringWithUTF8String:keychainAccessGroup_.c_str()];
276
+ std::string groupStr;
277
+ {
278
+ std::lock_guard<std::mutex> lock(accessGroupMutex_);
279
+ groupStr = keychainAccessGroup_;
280
+ }
281
+ NSString* group = groupStr.empty() ? nil : [NSString stringWithUTF8String:groupStr.c_str()];
218
282
  NSMutableDictionary* query = baseKeychainQuery(nsKey, kKeychainService, group);
219
283
  query[(__bridge id)kSecReturnData] = @YES;
220
284
  query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
285
+ disableKeychainInteraction(query);
221
286
 
222
287
  CFTypeRef result = NULL;
223
- if (SecItemCopyMatching((__bridge CFDictionaryRef)query, &result) == errSecSuccess && result) {
288
+ OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
289
+ if (status == errSecSuccess && result) {
224
290
  NSData* data = (__bridge_transfer NSData*)result;
225
291
  NSString* str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
226
292
  if (str) return std::string([str UTF8String]);
227
293
  }
294
+ if (status == errSecInteractionNotAllowed) {
295
+ throw std::runtime_error(
296
+ "NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
297
+ "The item is not accessible until the device is unlocked."
298
+ );
299
+ }
228
300
  return std::nullopt;
229
301
  }
230
302
 
231
303
  void IOSStorageAdapterCpp::deleteSecure(const std::string& key) {
232
304
  NSString* nsKey = [NSString stringWithUTF8String:key.c_str()];
233
- NSString* group = keychainAccessGroup_.empty() ? nil : [NSString stringWithUTF8String:keychainAccessGroup_.c_str()];
305
+ std::string groupStr;
306
+ {
307
+ std::lock_guard<std::mutex> lock(accessGroupMutex_);
308
+ groupStr = keychainAccessGroup_;
309
+ }
310
+ NSString* group = groupStr.empty() ? nil : [NSString stringWithUTF8String:groupStr.c_str()];
311
+
234
312
  NSMutableDictionary* secureQuery = baseKeychainQuery(nsKey, kKeychainService, group);
235
- SecItemDelete((__bridge CFDictionaryRef)secureQuery);
313
+ OSStatus secureStatus = SecItemDelete((__bridge CFDictionaryRef)secureQuery);
314
+ if (secureStatus == errSecInteractionNotAllowed) {
315
+ throw std::runtime_error(
316
+ "NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
317
+ "The item is not accessible until the device is unlocked."
318
+ );
319
+ }
320
+
236
321
  NSMutableDictionary* biometricQuery = baseKeychainQuery(nsKey, kBiometricKeychainService, group);
237
- SecItemDelete((__bridge CFDictionaryRef)biometricQuery);
322
+ OSStatus biometricStatus = SecItemDelete((__bridge CFDictionaryRef)biometricQuery);
323
+ if (biometricStatus == errSecInteractionNotAllowed) {
324
+ throw std::runtime_error(
325
+ "NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
326
+ "The item is not accessible until the device is unlocked."
327
+ );
328
+ }
329
+
330
+ // errSecItemNotFound means the item was already gone — that's fine (idempotent).
331
+ // Only update the cache if the delete actually ran (success or item-not-found).
238
332
  markSecureKeyRemoved(key);
239
333
  markBiometricKeyRemoved(key);
240
334
  }
241
335
 
242
336
  bool IOSStorageAdapterCpp::hasSecure(const std::string& key) {
243
337
  NSString* nsKey = [NSString stringWithUTF8String:key.c_str()];
244
- NSString* group = keychainAccessGroup_.empty() ? nil : [NSString stringWithUTF8String:keychainAccessGroup_.c_str()];
338
+ std::string groupStr;
339
+ {
340
+ std::lock_guard<std::mutex> lock(accessGroupMutex_);
341
+ groupStr = keychainAccessGroup_;
342
+ }
343
+ NSString* group = groupStr.empty() ? nil : [NSString stringWithUTF8String:groupStr.c_str()];
245
344
  NSMutableDictionary* secureQuery = baseKeychainQuery(nsKey, kKeychainService, group);
345
+ disableKeychainInteraction(secureQuery);
246
346
  if (SecItemCopyMatching((__bridge CFDictionaryRef)secureQuery, NULL) == errSecSuccess) {
247
347
  return true;
248
348
  }
249
349
  NSMutableDictionary* biometricQuery = baseKeychainQuery(nsKey, kBiometricKeychainService, group);
350
+ disableKeychainInteraction(biometricQuery);
250
351
  return SecItemCopyMatching((__bridge CFDictionaryRef)biometricQuery, NULL) == errSecSuccess;
251
352
  }
252
353
 
@@ -310,7 +411,12 @@ void IOSStorageAdapterCpp::deleteSecureBatch(const std::vector<std::string>& key
310
411
  }
311
412
 
312
413
  void IOSStorageAdapterCpp::clearSecure() {
313
- NSString* group = keychainAccessGroup_.empty() ? nil : [NSString stringWithUTF8String:keychainAccessGroup_.c_str()];
414
+ std::string groupStr;
415
+ {
416
+ std::lock_guard<std::mutex> lock(accessGroupMutex_);
417
+ groupStr = keychainAccessGroup_;
418
+ }
419
+ NSString* group = groupStr.empty() ? nil : [NSString stringWithUTF8String:groupStr.c_str()];
314
420
  NSMutableDictionary* secureQuery = [@{
315
421
  (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
316
422
  (__bridge id)kSecAttrService: kKeychainService
@@ -318,7 +424,14 @@ void IOSStorageAdapterCpp::clearSecure() {
318
424
  if (group && group.length > 0) {
319
425
  secureQuery[(__bridge id)kSecAttrAccessGroup] = group;
320
426
  }
321
- SecItemDelete((__bridge CFDictionaryRef)secureQuery);
427
+ OSStatus secStatus = SecItemDelete((__bridge CFDictionaryRef)secureQuery);
428
+ if (secStatus != errSecSuccess && secStatus != errSecItemNotFound) {
429
+ if (secStatus == errSecInteractionNotAllowed) {
430
+ throw std::runtime_error("NitroStorage: Cannot clear secure storage: keychain is locked (errSecInteractionNotAllowed)");
431
+ }
432
+ throw std::runtime_error(
433
+ std::string("NitroStorage: clearSecure failed with status ") + std::to_string(secStatus));
434
+ }
322
435
 
323
436
  NSMutableDictionary* biometricQuery = [@{
324
437
  (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
@@ -327,13 +440,21 @@ void IOSStorageAdapterCpp::clearSecure() {
327
440
  if (group && group.length > 0) {
328
441
  biometricQuery[(__bridge id)kSecAttrAccessGroup] = group;
329
442
  }
330
- SecItemDelete((__bridge CFDictionaryRef)biometricQuery);
331
- clearSecureKeyCache();
443
+ OSStatus bioStatus = SecItemDelete((__bridge CFDictionaryRef)biometricQuery);
444
+ if (bioStatus != errSecSuccess && bioStatus != errSecItemNotFound) {
445
+ if (bioStatus == errSecInteractionNotAllowed) {
446
+ throw std::runtime_error("NitroStorage: Cannot clear biometric storage: keychain is locked (errSecInteractionNotAllowed)");
447
+ }
448
+ throw std::runtime_error(
449
+ std::string("NitroStorage: clearSecureBiometric failed with status ") + std::to_string(bioStatus));
450
+ }
451
+ clearSecureKeyCache(); // Only clears cache AFTER confirmed deletion
332
452
  }
333
453
 
334
454
  // --- Configuration ---
335
455
 
336
456
  void IOSStorageAdapterCpp::setSecureAccessControl(int level) {
457
+ std::lock_guard<std::mutex> lock(accessGroupMutex_);
337
458
  accessControlLevel_ = level;
338
459
  }
339
460
 
@@ -342,8 +463,12 @@ void IOSStorageAdapterCpp::setSecureWritesAsync(bool /*enabled*/) {
342
463
  }
343
464
 
344
465
  void IOSStorageAdapterCpp::setKeychainAccessGroup(const std::string& group) {
466
+ std::lock_guard<std::mutex> lock1(accessGroupMutex_);
467
+ std::lock_guard<std::mutex> lock2(secureKeysMutex_);
345
468
  keychainAccessGroup_ = group;
346
- clearSecureKeyCache();
469
+ secureKeysCache_.clear();
470
+ biometricKeysCache_.clear();
471
+ secureKeyCacheHydrated_ = false;
347
472
  }
348
473
 
349
474
  // --- Biometric (separate Keychain service with biometric ACL) ---
@@ -353,14 +478,41 @@ void IOSStorageAdapterCpp::setSecureBiometric(const std::string& key, const std:
353
478
  }
354
479
 
355
480
  void IOSStorageAdapterCpp::setSecureBiometricWithLevel(const std::string& key, const std::string& value, int level) {
481
+ NSString* nsKey = [NSString stringWithUTF8String:key.c_str()];
482
+ NSData* data = [[NSString stringWithUTF8String:value.c_str()] dataUsingEncoding:NSUTF8StringEncoding];
483
+ std::string groupStr;
484
+ {
485
+ std::lock_guard<std::mutex> lock(accessGroupMutex_);
486
+ groupStr = keychainAccessGroup_;
487
+ }
488
+ NSString* group = groupStr.empty() ? nil : [NSString stringWithUTF8String:groupStr.c_str()];
489
+
356
490
  if (level == 0) {
491
+ // Delete any existing biometric keychain entry for this key
492
+ NSMutableDictionary* deleteQuery = baseKeychainQuery(nsKey, kBiometricKeychainService, group);
493
+ SecItemDelete((__bridge CFDictionaryRef)deleteQuery);
494
+ markBiometricKeyRemoved(key);
495
+
357
496
  setSecure(key, value);
358
497
  return;
359
498
  }
360
499
 
361
- NSString* nsKey = [NSString stringWithUTF8String:key.c_str()];
362
- NSData* data = [[NSString stringWithUTF8String:value.c_str()] dataUsingEncoding:NSUTF8StringEncoding];
363
- NSString* group = keychainAccessGroup_.empty() ? nil : [NSString stringWithUTF8String:keychainAccessGroup_.c_str()];
500
+ // Capture backup before delete — must not prompt for auth
501
+ std::optional<std::string> backup = std::nullopt;
502
+ {
503
+ NSMutableDictionary* backupQuery = baseKeychainQuery(nsKey, kBiometricKeychainService, group);
504
+ backupQuery[(__bridge id)kSecReturnData] = @YES;
505
+ backupQuery[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
506
+ disableKeychainInteraction(backupQuery);
507
+ CFTypeRef backupResult = NULL;
508
+ if (SecItemCopyMatching((__bridge CFDictionaryRef)backupQuery, &backupResult) == errSecSuccess && backupResult) {
509
+ NSData* bData = (__bridge_transfer NSData*)backupResult;
510
+ NSString* str = [[NSString alloc] initWithData:bData encoding:NSUTF8StringEncoding];
511
+ if (str) backup = std::string([str UTF8String]);
512
+ } else if (backupResult) {
513
+ CFRelease(backupResult);
514
+ }
515
+ }
364
516
 
365
517
  // Delete existing item first (access control can't be updated in place)
366
518
  NSMutableDictionary* deleteQuery = baseKeychainQuery(nsKey, kBiometricKeychainService, group);
@@ -380,6 +532,7 @@ void IOSStorageAdapterCpp::setSecureBiometricWithLevel(const std::string& key, c
380
532
 
381
533
  if (error || !access) {
382
534
  if (access) CFRelease(access);
535
+ if (error) CFRelease(error);
383
536
  throw std::runtime_error("NitroStorage: Failed to create biometric access control");
384
537
  }
385
538
 
@@ -387,16 +540,40 @@ void IOSStorageAdapterCpp::setSecureBiometricWithLevel(const std::string& key, c
387
540
  attrs[(__bridge id)kSecValueData] = data;
388
541
  attrs[(__bridge id)kSecAttrAccessControl] = (__bridge_transfer id)access;
389
542
 
390
- OSStatus status = SecItemAdd((__bridge CFDictionaryRef)attrs, NULL);
391
- if (status != errSecSuccess) {
392
- throw std::runtime_error("NitroStorage: Biometric set failed with status " + std::to_string(status));
543
+ OSStatus addStatus = SecItemAdd((__bridge CFDictionaryRef)attrs, NULL);
544
+ if (addStatus != errSecSuccess) {
545
+ if (backup.has_value()) {
546
+ try {
547
+ setSecure(key, *backup);
548
+ } catch (const std::exception& restoreEx) {
549
+ throw std::runtime_error(
550
+ std::string("NitroStorage: Biometric set failed with status ") +
551
+ std::to_string(addStatus) +
552
+ " and previous value restoration also failed: " + restoreEx.what());
553
+ }
554
+ }
555
+ if (addStatus == errSecInteractionNotAllowed) {
556
+ throw std::runtime_error(
557
+ "NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
558
+ "The item is not accessible until the device is unlocked."
559
+ );
560
+ }
561
+ throw std::runtime_error(
562
+ std::string("NitroStorage: Biometric set failed with status ") +
563
+ std::to_string(addStatus) +
564
+ (backup.has_value() ? " (previous value restored to non-biometric keychain)" : " (no previous value)"));
393
565
  }
394
566
  markBiometricKeySet(key);
395
567
  }
396
568
 
397
569
  std::optional<std::string> IOSStorageAdapterCpp::getSecureBiometric(const std::string& key) {
398
570
  NSString* nsKey = [NSString stringWithUTF8String:key.c_str()];
399
- NSString* group = keychainAccessGroup_.empty() ? nil : [NSString stringWithUTF8String:keychainAccessGroup_.c_str()];
571
+ std::string groupStr;
572
+ {
573
+ std::lock_guard<std::mutex> lock(accessGroupMutex_);
574
+ groupStr = keychainAccessGroup_;
575
+ }
576
+ NSString* group = groupStr.empty() ? nil : [NSString stringWithUTF8String:groupStr.c_str()];
400
577
  NSMutableDictionary* query = baseKeychainQuery(nsKey, kBiometricKeychainService, group);
401
578
  query[(__bridge id)kSecReturnData] = @YES;
402
579
  query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
@@ -408,6 +585,12 @@ std::optional<std::string> IOSStorageAdapterCpp::getSecureBiometric(const std::s
408
585
  NSString* str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
409
586
  if (str) return std::string([str UTF8String]);
410
587
  }
588
+ if (status == errSecInteractionNotAllowed) {
589
+ throw std::runtime_error(
590
+ "NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
591
+ "The item is not accessible until the device is unlocked."
592
+ );
593
+ }
411
594
  if (status == errSecUserCanceled || status == errSecAuthFailed) {
412
595
  throw std::runtime_error("NitroStorage: Biometric authentication failed");
413
596
  }
@@ -416,7 +599,12 @@ std::optional<std::string> IOSStorageAdapterCpp::getSecureBiometric(const std::s
416
599
 
417
600
  void IOSStorageAdapterCpp::deleteSecureBiometric(const std::string& key) {
418
601
  NSString* nsKey = [NSString stringWithUTF8String:key.c_str()];
419
- NSString* group = keychainAccessGroup_.empty() ? nil : [NSString stringWithUTF8String:keychainAccessGroup_.c_str()];
602
+ std::string groupStr;
603
+ {
604
+ std::lock_guard<std::mutex> lock(accessGroupMutex_);
605
+ groupStr = keychainAccessGroup_;
606
+ }
607
+ NSString* group = groupStr.empty() ? nil : [NSString stringWithUTF8String:groupStr.c_str()];
420
608
  NSMutableDictionary* query = baseKeychainQuery(nsKey, kBiometricKeychainService, group);
421
609
  SecItemDelete((__bridge CFDictionaryRef)query);
422
610
  markBiometricKeyRemoved(key);
@@ -424,13 +612,24 @@ void IOSStorageAdapterCpp::deleteSecureBiometric(const std::string& key) {
424
612
 
425
613
  bool IOSStorageAdapterCpp::hasSecureBiometric(const std::string& key) {
426
614
  NSString* nsKey = [NSString stringWithUTF8String:key.c_str()];
427
- NSString* group = keychainAccessGroup_.empty() ? nil : [NSString stringWithUTF8String:keychainAccessGroup_.c_str()];
615
+ std::string groupStr;
616
+ {
617
+ std::lock_guard<std::mutex> lock(accessGroupMutex_);
618
+ groupStr = keychainAccessGroup_;
619
+ }
620
+ NSString* group = groupStr.empty() ? nil : [NSString stringWithUTF8String:groupStr.c_str()];
428
621
  NSMutableDictionary* query = baseKeychainQuery(nsKey, kBiometricKeychainService, group);
622
+ disableKeychainInteraction(query);
429
623
  return SecItemCopyMatching((__bridge CFDictionaryRef)query, NULL) == errSecSuccess;
430
624
  }
431
625
 
432
626
  void IOSStorageAdapterCpp::clearSecureBiometric() {
433
- NSString* group = keychainAccessGroup_.empty() ? nil : [NSString stringWithUTF8String:keychainAccessGroup_.c_str()];
627
+ std::string groupStr;
628
+ {
629
+ std::lock_guard<std::mutex> lock(accessGroupMutex_);
630
+ groupStr = keychainAccessGroup_;
631
+ }
632
+ NSString* group = groupStr.empty() ? nil : [NSString stringWithUTF8String:groupStr.c_str()];
434
633
  NSMutableDictionary* query = [@{
435
634
  (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
436
635
  (__bridge id)kSecAttrService: kBiometricKeychainService
@@ -438,24 +637,40 @@ void IOSStorageAdapterCpp::clearSecureBiometric() {
438
637
  if (group && group.length > 0) {
439
638
  query[(__bridge id)kSecAttrAccessGroup] = group;
440
639
  }
441
- SecItemDelete((__bridge CFDictionaryRef)query);
442
- std::lock_guard<std::mutex> lock(secureKeysMutex_);
443
- biometricKeysCache_.clear();
640
+ OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
641
+ if (status != errSecSuccess && status != errSecItemNotFound) {
642
+ if (status == errSecInteractionNotAllowed) {
643
+ throw std::runtime_error("NitroStorage: Cannot clear biometric storage: keychain is locked (errSecInteractionNotAllowed)");
644
+ }
645
+ throw std::runtime_error(
646
+ std::string("NitroStorage: clearSecureBiometric failed with status ") + std::to_string(status));
647
+ }
648
+ {
649
+ std::lock_guard<std::mutex> lock(secureKeysMutex_);
650
+ biometricKeysCache_.clear();
651
+ }
444
652
  }
445
653
 
446
654
  void IOSStorageAdapterCpp::ensureSecureKeyCacheHydrated() {
447
655
  {
448
656
  std::lock_guard<std::mutex> lock(secureKeysMutex_);
449
- if (secureKeyCacheHydrated_) {
450
- return;
451
- }
657
+ if (secureKeyCacheHydrated_) return;
658
+ }
659
+
660
+ std::string groupStr;
661
+ {
662
+ std::lock_guard<std::mutex> lock(accessGroupMutex_);
663
+ groupStr = keychainAccessGroup_;
452
664
  }
665
+ NSString* nsGroup = groupStr.empty() ? nil : [NSString stringWithUTF8String:groupStr.c_str()];
453
666
 
454
- NSString* group = keychainAccessGroup_.empty() ? nil : [NSString stringWithUTF8String:keychainAccessGroup_.c_str()];
455
- const std::vector<std::string> secureKeys = keychainAccountsForService(kKeychainService, group);
456
- const std::vector<std::string> biometricKeys = keychainAccountsForService(kBiometricKeychainService, group);
667
+ // These can throw errSecInteractionNotAllowed let the exception propagate
668
+ // so the cache is NOT marked hydrated (will be retried on next access)
669
+ const std::vector<std::string> secureKeys = keychainAccountsForService(kKeychainService, nsGroup);
670
+ const std::vector<std::string> biometricKeys = keychainAccountsForService(kBiometricKeychainService, nsGroup);
457
671
 
458
672
  std::lock_guard<std::mutex> lock(secureKeysMutex_);
673
+ if (secureKeyCacheHydrated_) return;
459
674
  secureKeysCache_.clear();
460
675
  biometricKeysCache_.clear();
461
676
  secureKeysCache_.insert(secureKeys.begin(), secureKeys.end());