react-native-nitro-storage 0.4.1 → 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.
- package/README.md +39 -0
- package/android/build.gradle +0 -12
- package/android/consumer-rules.pro +26 -4
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +7 -10
- package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +0 -4
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +172 -77
- package/cpp/bindings/HybridStorage.cpp +120 -69
- package/cpp/bindings/HybridStorage.hpp +4 -0
- package/ios/IOSStorageAdapterCpp.hpp +2 -1
- package/ios/IOSStorageAdapterCpp.mm +264 -49
- package/lib/commonjs/index.js +73 -21
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +120 -42
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/internal.js +51 -23
- package/lib/commonjs/internal.js.map +1 -1
- package/lib/module/index.js +72 -21
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +119 -42
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/internal.js +51 -23
- package/lib/module/internal.js.map +1 -1
- package/lib/typescript/index.d.ts +4 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +5 -1
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/internal.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +83 -28
- package/src/index.web.ts +135 -50
- 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
|
-
|
|
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
|
-
[
|
|
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
|
-
[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
[
|
|
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
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
|
391
|
-
if (
|
|
392
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
443
|
-
|
|
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
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
const std::vector<std::string>
|
|
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());
|