react-native-nitro-storage 0.4.4 → 0.4.5
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 +107 -7
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +61 -10
- package/ios/IOSStorageAdapterCpp.mm +44 -14
- package/lib/commonjs/index.js +221 -5
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +444 -202
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/indexeddb-backend.js +129 -7
- package/lib/commonjs/indexeddb-backend.js.map +1 -1
- package/lib/commonjs/storage-runtime.js +41 -0
- package/lib/commonjs/storage-runtime.js.map +1 -0
- package/lib/commonjs/web-storage-backend.js +90 -0
- package/lib/commonjs/web-storage-backend.js.map +1 -0
- package/lib/module/index.js +213 -5
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +436 -202
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/indexeddb-backend.js +129 -7
- package/lib/module/indexeddb-backend.js.map +1 -1
- package/lib/module/storage-runtime.js +36 -0
- package/lib/module/storage-runtime.js.map +1 -0
- package/lib/module/web-storage-backend.js +86 -0
- package/lib/module/web-storage-backend.js.map +1 -0
- package/lib/typescript/index.d.ts +11 -7
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +12 -8
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/indexeddb-backend.d.ts +6 -2
- package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
- package/lib/typescript/storage-runtime.d.ts +16 -0
- package/lib/typescript/storage-runtime.d.ts.map +1 -0
- package/lib/typescript/web-storage-backend.d.ts +30 -0
- package/lib/typescript/web-storage-backend.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +264 -20
- package/src/index.web.ts +597 -245
- package/src/indexeddb-backend.ts +147 -6
- package/src/storage-runtime.ts +94 -0
- package/src/web-storage-backend.ts +129 -0
package/README.md
CHANGED
|
@@ -40,7 +40,11 @@ Every feature in this package is documented with at least one runnable example i
|
|
|
40
40
|
- Prefix utilities (`getKeysByPrefix`, `getByPrefix`) — see Prefix Queries and Namespace Inspection
|
|
41
41
|
- Versioned item API (`getWithVersion`, `setIfVersion`) — see Optimistic Versioned Writes
|
|
42
42
|
- Metrics API (`setMetricsObserver`, `getMetricsSnapshot`, `resetMetrics`) — see Storage Metrics Instrumentation
|
|
43
|
+
- Runtime capability introspection (`storage.getCapabilities()`) — see Global utility examples
|
|
44
|
+
- Structured storage error codes (`getStorageErrorCode`, `isKeychainLockedError`) — see Error Classification
|
|
45
|
+
- Web disk backend override (`setWebDiskStorageBackend`, `getWebDiskStorageBackend`) — see Custom Web Disk and Secure Backends
|
|
43
46
|
- Web secure backend override (`setWebSecureStorageBackend`, `getWebSecureStorageBackend`) — see Custom Web Secure Backend
|
|
47
|
+
- Web backend durability (`flushWebStorageBackends`) — see Custom Web Disk and Secure Backends
|
|
44
48
|
- IndexedDB backend factory (`createIndexedDBBackend`) — see IndexedDB Backend for Web
|
|
45
49
|
- Bulk import (`storage.import`) — see Bulk Data Import
|
|
46
50
|
- Batch APIs (`getBatch`, `setBatch`, `removeBatch`) — see Batch Operations and Bulk Bootstrap with Batch APIs
|
|
@@ -196,6 +200,7 @@ function createStorageItem<T = undefined>(
|
|
|
196
200
|
| `expiration` | `{ ttlMs: number }` | — | Time-to-live in milliseconds |
|
|
197
201
|
| `onExpired` | `(key: string) => void` | — | Callback fired when a TTL value expires on read |
|
|
198
202
|
| `readCache` | `boolean` | `false` | Cache deserialized values in JS (avoids repeated native reads) |
|
|
203
|
+
| `coalesceDiskWrites` | `boolean` | `false` | Batch same-tick Disk writes per key until `flushDiskWrites()` |
|
|
199
204
|
| `coalesceSecureWrites` | `boolean` | `false` | Batch same-tick Secure writes per key |
|
|
200
205
|
| `namespace` | `string` | — | Prefix key as `namespace:key` for isolation |
|
|
201
206
|
| `biometric` | `boolean` | `false` | Require biometric auth (Secure scope only) |
|
|
@@ -278,7 +283,10 @@ import { storage, StorageScope } from "react-native-nitro-storage";
|
|
|
278
283
|
| `storage.getByPrefix(prefix, scope)` | Get raw key-value pairs for keys matching a prefix |
|
|
279
284
|
| `storage.getAll(scope)` | Get all key-value pairs as `Record<string, string>` |
|
|
280
285
|
| `storage.size(scope)` | Number of stored keys |
|
|
286
|
+
| `storage.getCapabilities()` | Read runtime backend metadata and buffering support |
|
|
281
287
|
| `storage.setAccessControl(level)` | Set default secure access control for subsequent secure writes (native only) |
|
|
288
|
+
| `storage.setDiskWritesAsync(enabled)` | Buffer raw Disk writes in JS until flushed (all platforms) |
|
|
289
|
+
| `storage.flushDiskWrites()` | Force flush queued Disk writes from raw APIs / coalesced items |
|
|
282
290
|
| `storage.setSecureWritesAsync(enabled)` | Toggle async secure writes on Android (`false` by default) |
|
|
283
291
|
| `storage.flushSecureWrites()` | Force flush of queued secure writes when coalescing is enabled |
|
|
284
292
|
| `storage.setKeychainAccessGroup(group)` | Set keychain access group for app sharing (native only) |
|
|
@@ -290,6 +298,14 @@ import { storage, StorageScope } from "react-native-nitro-storage";
|
|
|
290
298
|
| `storage.getMetricsSnapshot()` | Get aggregate counters/latency stats keyed by operation |
|
|
291
299
|
| `storage.resetMetrics()` | Reset in-memory metrics counters |
|
|
292
300
|
|
|
301
|
+
| Web helper | Description |
|
|
302
|
+
| -------------------------------------- | -------------------------------------------------------------------- |
|
|
303
|
+
| `setWebDiskStorageBackend(backend?)` | Override the web Disk backend (web only) |
|
|
304
|
+
| `getWebDiskStorageBackend()` | Read the active web Disk backend (web only) |
|
|
305
|
+
| `setWebSecureStorageBackend(backend?)` | Override the web Secure backend (web only) |
|
|
306
|
+
| `getWebSecureStorageBackend()` | Read the active web Secure backend (web only) |
|
|
307
|
+
| `flushWebStorageBackends()` | Await optional backend durability hooks for Disk + Secure (web only) |
|
|
308
|
+
|
|
293
309
|
> `storage.getAll(StorageScope.Secure)` returns regular secure entries. Biometric-protected values are not included in this snapshot API.
|
|
294
310
|
|
|
295
311
|
#### Global utility examples
|
|
@@ -307,6 +323,7 @@ storage.getKeysByPrefix("user-42:", StorageScope.Disk);
|
|
|
307
323
|
storage.getByPrefix("user-42:", StorageScope.Disk);
|
|
308
324
|
storage.getAll(StorageScope.Disk);
|
|
309
325
|
storage.size(StorageScope.Disk);
|
|
326
|
+
storage.getCapabilities();
|
|
310
327
|
|
|
311
328
|
storage.clearNamespace("user-42", StorageScope.Disk);
|
|
312
329
|
storage.clearBiometric();
|
|
@@ -318,6 +335,32 @@ storage.clear(StorageScope.Memory);
|
|
|
318
335
|
storage.clearAll();
|
|
319
336
|
```
|
|
320
337
|
|
|
338
|
+
#### Disk write buffering
|
|
339
|
+
|
|
340
|
+
Disk writes can now be buffered in JS, similar to secure write coalescing, which is useful when you are doing bursty persistence and want an explicit durability boundary.
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
import {
|
|
344
|
+
createStorageItem,
|
|
345
|
+
storage,
|
|
346
|
+
StorageScope,
|
|
347
|
+
} from "react-native-nitro-storage";
|
|
348
|
+
|
|
349
|
+
const bufferedDraft = createStorageItem({
|
|
350
|
+
key: "draft",
|
|
351
|
+
scope: StorageScope.Disk,
|
|
352
|
+
defaultValue: "",
|
|
353
|
+
coalesceDiskWrites: true,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
bufferedDraft.set("hello");
|
|
357
|
+
storage.setDiskWritesAsync(true);
|
|
358
|
+
storage.setString("draft:raw", "value", StorageScope.Disk);
|
|
359
|
+
|
|
360
|
+
storage.flushDiskWrites(); // commit queued Disk writes
|
|
361
|
+
storage.setDiskWritesAsync(false);
|
|
362
|
+
```
|
|
363
|
+
|
|
321
364
|
#### Android secure write mode
|
|
322
365
|
|
|
323
366
|
`storage.setSecureWritesAsync(true)` switches secure writes from synchronous `commit()` to asynchronous `apply()` on Android.
|
|
@@ -335,25 +378,63 @@ storage.setSecureWritesAsync(true);
|
|
|
335
378
|
storage.flushSecureWrites(); // deterministic durability boundary
|
|
336
379
|
```
|
|
337
380
|
|
|
338
|
-
#### Custom
|
|
381
|
+
#### Custom Web Disk and Secure Backends
|
|
382
|
+
|
|
383
|
+
By default, web Disk and Secure scopes use `localStorage`. Disk excludes Nitro's secure prefixes, and Secure stores under `__secure_` / `__bio_` prefixes.
|
|
339
384
|
|
|
340
|
-
|
|
385
|
+
You can replace either backend with a custom implementation. The minimal backend contract is:
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
type WebStorageBackend = {
|
|
389
|
+
getItem(key: string): string | null;
|
|
390
|
+
setItem(key: string, value: string): void;
|
|
391
|
+
removeItem(key: string): void;
|
|
392
|
+
clear(): void;
|
|
393
|
+
getAllKeys(): string[];
|
|
394
|
+
getMany?: (keys: string[]) => (string | null)[];
|
|
395
|
+
setMany?: (entries: ReadonlyArray<readonly [string, string]>) => void;
|
|
396
|
+
removeMany?: (keys: string[]) => void;
|
|
397
|
+
size?: () => number;
|
|
398
|
+
subscribe?: (
|
|
399
|
+
listener: (event: { key: string | null; newValue: string | null }) => void,
|
|
400
|
+
) => () => void;
|
|
401
|
+
flush?: () => Promise<void>;
|
|
402
|
+
name?: string;
|
|
403
|
+
};
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Optional hooks are used for faster batch operations, custom cross-tab sync, and explicit durability boundaries.
|
|
341
407
|
|
|
342
408
|
```ts
|
|
343
409
|
import {
|
|
410
|
+
flushWebStorageBackends,
|
|
411
|
+
getWebDiskStorageBackend,
|
|
344
412
|
getWebSecureStorageBackend,
|
|
413
|
+
setWebDiskStorageBackend,
|
|
345
414
|
setWebSecureStorageBackend,
|
|
346
415
|
} from "react-native-nitro-storage";
|
|
347
416
|
|
|
417
|
+
setWebDiskStorageBackend({
|
|
418
|
+
getItem: (key) => diskStore.get(key) ?? null,
|
|
419
|
+
setItem: (key, value) => diskStore.set(key, value),
|
|
420
|
+
removeItem: (key) => diskStore.delete(key),
|
|
421
|
+
clear: () => diskStore.clear(),
|
|
422
|
+
getAllKeys: () => Array.from(diskStore.keys()),
|
|
423
|
+
});
|
|
424
|
+
|
|
348
425
|
setWebSecureStorageBackend({
|
|
349
426
|
getItem: (key) => encryptedStore.get(key) ?? null,
|
|
350
427
|
setItem: (key, value) => encryptedStore.set(key, value),
|
|
351
428
|
removeItem: (key) => encryptedStore.delete(key),
|
|
352
429
|
clear: () => encryptedStore.clear(),
|
|
353
|
-
getAllKeys: () => encryptedStore.keys(),
|
|
430
|
+
getAllKeys: () => Array.from(encryptedStore.keys()),
|
|
354
431
|
});
|
|
355
432
|
|
|
433
|
+
await flushWebStorageBackends();
|
|
434
|
+
|
|
435
|
+
const diskBackend = getWebDiskStorageBackend();
|
|
356
436
|
const backend = getWebSecureStorageBackend();
|
|
437
|
+
console.log("custom disk backend active:", diskBackend !== undefined);
|
|
357
438
|
console.log("custom backend active:", backend !== undefined);
|
|
358
439
|
```
|
|
359
440
|
|
|
@@ -376,12 +457,24 @@ setWebSecureStorageBackend(backend);
|
|
|
376
457
|
|
|
377
458
|
- **Async init**: `createIndexedDBBackend()` opens (or creates) the IndexedDB database and hydrates an in-memory cache from all stored entries before resolving.
|
|
378
459
|
- **Synchronous reads**: all `getItem` calls are served from the in-memory cache — no async overhead after init.
|
|
379
|
-
- **
|
|
460
|
+
- **Queued writes + durability**: writes update the cache synchronously, persist in the background, and can be awaited via `await backend.flush()` or `await flushWebStorageBackends()`.
|
|
461
|
+
- **Cross-tab sync**: backend instances on the same `dbName`/`storeName` coordinate through `BroadcastChannel` so cache invalidation reaches other tabs.
|
|
380
462
|
- **Custom database/store**: optionally pass `dbName` and `storeName` to isolate databases per environment or tenant.
|
|
381
463
|
|
|
382
464
|
```ts
|
|
383
465
|
const backend = await createIndexedDBBackend("my-app-db", "secure-kv");
|
|
384
466
|
setWebSecureStorageBackend(backend);
|
|
467
|
+
await backend.flush?.();
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
You can also pass an optional third argument to receive async persistence failures:
|
|
471
|
+
|
|
472
|
+
```ts
|
|
473
|
+
const backend = await createIndexedDBBackend("my-app-db", "secure-kv", {
|
|
474
|
+
onError: (error) => {
|
|
475
|
+
console.error("indexeddb persistence failed", error);
|
|
476
|
+
},
|
|
477
|
+
});
|
|
385
478
|
```
|
|
386
479
|
|
|
387
480
|
---
|
|
@@ -530,16 +623,23 @@ These are synchronous and go directly to the native backend without any serializ
|
|
|
530
623
|
|
|
531
624
|
---
|
|
532
625
|
|
|
533
|
-
###
|
|
626
|
+
### Error Classification
|
|
534
627
|
|
|
535
|
-
|
|
628
|
+
`getStorageErrorCode(err)` returns a stable classification for common native/web storage failures. Native bridges now emit stable `[nitro-error:<code>]` tags so the classification path does not depend on platform exception wording alone.
|
|
629
|
+
`isKeychainLockedError(err)` remains the convenience helper for retry-after-unlock flows and now delegates to the structured code path.
|
|
536
630
|
|
|
537
631
|
```ts
|
|
538
|
-
import {
|
|
632
|
+
import {
|
|
633
|
+
getStorageErrorCode,
|
|
634
|
+
isKeychainLockedError,
|
|
635
|
+
} from "react-native-nitro-storage";
|
|
539
636
|
|
|
540
637
|
try {
|
|
541
638
|
secureItem.get();
|
|
542
639
|
} catch (err) {
|
|
640
|
+
const code = getStorageErrorCode(err);
|
|
641
|
+
// "keychain_locked" | "authentication_required" | ...
|
|
642
|
+
|
|
543
643
|
if (isKeychainLockedError(err)) {
|
|
544
644
|
// device is locked — retry after unlock
|
|
545
645
|
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
package com.nitrostorage
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
+
import android.security.keystore.KeyPermanentlyInvalidatedException
|
|
5
|
+
import android.security.keystore.UserNotAuthenticatedException
|
|
4
6
|
import android.content.SharedPreferences
|
|
5
7
|
import android.util.Log
|
|
6
8
|
import androidx.security.crypto.EncryptedSharedPreferences
|
|
7
9
|
import androidx.security.crypto.MasterKey
|
|
10
|
+
import java.security.InvalidKeyException
|
|
8
11
|
import java.security.KeyStore
|
|
12
|
+
import java.security.KeyStoreException
|
|
9
13
|
import javax.crypto.AEADBadTagException
|
|
10
14
|
|
|
11
15
|
private fun Throwable.hasCause(type: Class<*>): Boolean {
|
|
@@ -17,6 +21,30 @@ private fun Throwable.hasCause(type: Class<*>): Boolean {
|
|
|
17
21
|
return false
|
|
18
22
|
}
|
|
19
23
|
|
|
24
|
+
private fun Throwable.storageErrorCode(): String? {
|
|
25
|
+
return when {
|
|
26
|
+
hasCause(AEADBadTagException::class.java) -> "storage_corruption"
|
|
27
|
+
hasCause(UserNotAuthenticatedException::class.java) ||
|
|
28
|
+
hasCause(KeyStoreException::class.java) -> "authentication_required"
|
|
29
|
+
hasCause(KeyPermanentlyInvalidatedException::class.java) ||
|
|
30
|
+
hasCause(InvalidKeyException::class.java) -> "key_invalidated"
|
|
31
|
+
else -> null
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private fun Throwable.wrapStorageException(
|
|
36
|
+
defaultMessage: String,
|
|
37
|
+
defaultCode: String? = null,
|
|
38
|
+
): RuntimeException {
|
|
39
|
+
val code = storageErrorCode() ?: defaultCode
|
|
40
|
+
val message = if (code != null) {
|
|
41
|
+
"[nitro-error:$code] $defaultMessage"
|
|
42
|
+
} else {
|
|
43
|
+
defaultMessage
|
|
44
|
+
}
|
|
45
|
+
return RuntimeException(message, this)
|
|
46
|
+
}
|
|
47
|
+
|
|
20
48
|
class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
21
49
|
private val sharedPreferences: SharedPreferences =
|
|
22
50
|
context.getSharedPreferences("NitroStorage", Context.MODE_PRIVATE)
|
|
@@ -44,8 +72,11 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
44
72
|
initializeEncryptedPreferences("NitroStorageBiometric", bioKey)
|
|
45
73
|
} catch (e: Exception) {
|
|
46
74
|
Log.e("NitroStorage", "Biometric storage unavailable: ${e.message}")
|
|
47
|
-
throw
|
|
48
|
-
"
|
|
75
|
+
throw e.wrapStorageException(
|
|
76
|
+
"NitroStorage: Biometric storage is not available on this device. " +
|
|
77
|
+
"Ensure biometric hardware is present and credentials are enrolled.",
|
|
78
|
+
defaultCode = "biometric_unavailable",
|
|
79
|
+
)
|
|
49
80
|
}
|
|
50
81
|
}
|
|
51
82
|
|
|
@@ -83,14 +114,17 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
83
114
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
|
84
115
|
)
|
|
85
116
|
} catch (retryEx: Exception) {
|
|
86
|
-
throw
|
|
117
|
+
throw retryEx.wrapStorageException(
|
|
118
|
+
"NitroStorage: Unrecoverable storage corruption in $name",
|
|
119
|
+
defaultCode = "storage_corruption",
|
|
120
|
+
)
|
|
87
121
|
}
|
|
88
122
|
}
|
|
89
123
|
else -> {
|
|
90
124
|
// Don't wipe on non-corruption failures (e.g., locked keystore)
|
|
91
|
-
throw
|
|
125
|
+
throw e.wrapStorageException(
|
|
92
126
|
"NitroStorage: Failed to initialize $name (${e::class.simpleName}). " +
|
|
93
|
-
|
|
127
|
+
"This may be a temporary keystore issue. If it persists, clear app data.",
|
|
94
128
|
)
|
|
95
129
|
}
|
|
96
130
|
}
|
|
@@ -136,7 +170,9 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
136
170
|
editor.commit()
|
|
137
171
|
}
|
|
138
172
|
} catch (e: Exception) {
|
|
139
|
-
throw
|
|
173
|
+
throw e.wrapStorageException(
|
|
174
|
+
"NitroStorage: Failed to write to secure storage: ${e.message}",
|
|
175
|
+
)
|
|
140
176
|
}
|
|
141
177
|
}
|
|
142
178
|
|
|
@@ -303,14 +339,26 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
303
339
|
@JvmStatic
|
|
304
340
|
fun getSecure(key: String): String? {
|
|
305
341
|
val inst = getInstanceOrThrow()
|
|
306
|
-
return
|
|
342
|
+
return try {
|
|
343
|
+
inst.getSecureSafe(inst.encryptedPreferences, key)
|
|
344
|
+
} catch (e: Exception) {
|
|
345
|
+
throw e.wrapStorageException(
|
|
346
|
+
"NitroStorage: Failed to read secure storage: ${e.message}",
|
|
347
|
+
)
|
|
348
|
+
}
|
|
307
349
|
}
|
|
308
350
|
|
|
309
351
|
@JvmStatic
|
|
310
352
|
fun getSecureBatch(keys: Array<String>): Array<String?> {
|
|
311
353
|
val inst = getInstanceOrThrow()
|
|
312
|
-
return
|
|
313
|
-
|
|
354
|
+
return try {
|
|
355
|
+
Array(keys.size) { index ->
|
|
356
|
+
inst.getSecureSafe(inst.encryptedPreferences, keys[index])
|
|
357
|
+
}
|
|
358
|
+
} catch (e: Exception) {
|
|
359
|
+
throw e.wrapStorageException(
|
|
360
|
+
"NitroStorage: Failed to read secure storage batch: ${e.message}",
|
|
361
|
+
)
|
|
314
362
|
}
|
|
315
363
|
}
|
|
316
364
|
|
|
@@ -406,7 +454,10 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
406
454
|
inst.applySecureEditor(editor)
|
|
407
455
|
inst.invalidateSecureKeysCache()
|
|
408
456
|
} catch (e: Exception) {
|
|
409
|
-
throw
|
|
457
|
+
throw e.wrapStorageException(
|
|
458
|
+
"NitroStorage: Biometric storage unavailable on this device",
|
|
459
|
+
defaultCode = "biometric_unavailable",
|
|
460
|
+
)
|
|
410
461
|
}
|
|
411
462
|
}
|
|
412
463
|
|
|
@@ -9,6 +9,12 @@ static NSString* const kKeychainService = @"com.nitrostorage.keychain";
|
|
|
9
9
|
static NSString* const kBiometricKeychainService = @"com.nitrostorage.biometric";
|
|
10
10
|
static NSString* const kDiskSuiteName = @"com.nitrostorage.disk";
|
|
11
11
|
|
|
12
|
+
static std::runtime_error taggedStorageError(const char* code, const std::string& message) {
|
|
13
|
+
return std::runtime_error(
|
|
14
|
+
std::string("[nitro-error:") + code + "] " + message
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
12
18
|
static NSUserDefaults* NitroDiskDefaults() {
|
|
13
19
|
static NSUserDefaults* defaults = [[NSUserDefaults alloc] initWithSuiteName:kDiskSuiteName];
|
|
14
20
|
return defaults ?: [NSUserDefaults standardUserDefaults];
|
|
@@ -191,7 +197,8 @@ static std::vector<std::string> keychainAccountsForService(NSString* service, NS
|
|
|
191
197
|
std::vector<std::string> keys;
|
|
192
198
|
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
|
|
193
199
|
if (status == errSecInteractionNotAllowed) {
|
|
194
|
-
throw
|
|
200
|
+
throw taggedStorageError(
|
|
201
|
+
"keychain_locked",
|
|
195
202
|
"NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
|
|
196
203
|
"The item is not accessible until the device is unlocked."
|
|
197
204
|
);
|
|
@@ -247,7 +254,8 @@ void IOSStorageAdapterCpp::setSecure(const std::string& key, const std::string&
|
|
|
247
254
|
const OSStatus addStatus = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
|
|
248
255
|
if (addStatus != errSecSuccess) {
|
|
249
256
|
if (addStatus == errSecInteractionNotAllowed) {
|
|
250
|
-
throw
|
|
257
|
+
throw taggedStorageError(
|
|
258
|
+
"keychain_locked",
|
|
251
259
|
"NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
|
|
252
260
|
"The item is not accessible until the device is unlocked."
|
|
253
261
|
);
|
|
@@ -261,7 +269,8 @@ void IOSStorageAdapterCpp::setSecure(const std::string& key, const std::string&
|
|
|
261
269
|
}
|
|
262
270
|
|
|
263
271
|
if (status == errSecInteractionNotAllowed) {
|
|
264
|
-
throw
|
|
272
|
+
throw taggedStorageError(
|
|
273
|
+
"keychain_locked",
|
|
265
274
|
"NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
|
|
266
275
|
"The item is not accessible until the device is unlocked."
|
|
267
276
|
);
|
|
@@ -292,7 +301,8 @@ std::optional<std::string> IOSStorageAdapterCpp::getSecure(const std::string& ke
|
|
|
292
301
|
if (str) return std::string([str UTF8String]);
|
|
293
302
|
}
|
|
294
303
|
if (status == errSecInteractionNotAllowed) {
|
|
295
|
-
throw
|
|
304
|
+
throw taggedStorageError(
|
|
305
|
+
"keychain_locked",
|
|
296
306
|
"NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
|
|
297
307
|
"The item is not accessible until the device is unlocked."
|
|
298
308
|
);
|
|
@@ -312,7 +322,8 @@ void IOSStorageAdapterCpp::deleteSecure(const std::string& key) {
|
|
|
312
322
|
NSMutableDictionary* secureQuery = baseKeychainQuery(nsKey, kKeychainService, group);
|
|
313
323
|
OSStatus secureStatus = SecItemDelete((__bridge CFDictionaryRef)secureQuery);
|
|
314
324
|
if (secureStatus == errSecInteractionNotAllowed) {
|
|
315
|
-
throw
|
|
325
|
+
throw taggedStorageError(
|
|
326
|
+
"keychain_locked",
|
|
316
327
|
"NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
|
|
317
328
|
"The item is not accessible until the device is unlocked."
|
|
318
329
|
);
|
|
@@ -321,7 +332,8 @@ void IOSStorageAdapterCpp::deleteSecure(const std::string& key) {
|
|
|
321
332
|
NSMutableDictionary* biometricQuery = baseKeychainQuery(nsKey, kBiometricKeychainService, group);
|
|
322
333
|
OSStatus biometricStatus = SecItemDelete((__bridge CFDictionaryRef)biometricQuery);
|
|
323
334
|
if (biometricStatus == errSecInteractionNotAllowed) {
|
|
324
|
-
throw
|
|
335
|
+
throw taggedStorageError(
|
|
336
|
+
"keychain_locked",
|
|
325
337
|
"NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
|
|
326
338
|
"The item is not accessible until the device is unlocked."
|
|
327
339
|
);
|
|
@@ -427,7 +439,10 @@ void IOSStorageAdapterCpp::clearSecure() {
|
|
|
427
439
|
OSStatus secStatus = SecItemDelete((__bridge CFDictionaryRef)secureQuery);
|
|
428
440
|
if (secStatus != errSecSuccess && secStatus != errSecItemNotFound) {
|
|
429
441
|
if (secStatus == errSecInteractionNotAllowed) {
|
|
430
|
-
throw
|
|
442
|
+
throw taggedStorageError(
|
|
443
|
+
"keychain_locked",
|
|
444
|
+
"NitroStorage: Cannot clear secure storage: keychain is locked (errSecInteractionNotAllowed)"
|
|
445
|
+
);
|
|
431
446
|
}
|
|
432
447
|
throw std::runtime_error(
|
|
433
448
|
std::string("NitroStorage: clearSecure failed with status ") + std::to_string(secStatus));
|
|
@@ -443,7 +458,10 @@ void IOSStorageAdapterCpp::clearSecure() {
|
|
|
443
458
|
OSStatus bioStatus = SecItemDelete((__bridge CFDictionaryRef)biometricQuery);
|
|
444
459
|
if (bioStatus != errSecSuccess && bioStatus != errSecItemNotFound) {
|
|
445
460
|
if (bioStatus == errSecInteractionNotAllowed) {
|
|
446
|
-
throw
|
|
461
|
+
throw taggedStorageError(
|
|
462
|
+
"keychain_locked",
|
|
463
|
+
"NitroStorage: Cannot clear biometric storage: keychain is locked (errSecInteractionNotAllowed)"
|
|
464
|
+
);
|
|
447
465
|
}
|
|
448
466
|
throw std::runtime_error(
|
|
449
467
|
std::string("NitroStorage: clearSecureBiometric failed with status ") + std::to_string(bioStatus));
|
|
@@ -533,7 +551,10 @@ void IOSStorageAdapterCpp::setSecureBiometricWithLevel(const std::string& key, c
|
|
|
533
551
|
if (error || !access) {
|
|
534
552
|
if (access) CFRelease(access);
|
|
535
553
|
if (error) CFRelease(error);
|
|
536
|
-
throw
|
|
554
|
+
throw taggedStorageError(
|
|
555
|
+
"biometric_unavailable",
|
|
556
|
+
"NitroStorage: Failed to create biometric access control"
|
|
557
|
+
);
|
|
537
558
|
}
|
|
538
559
|
|
|
539
560
|
NSMutableDictionary* attrs = baseKeychainQuery(nsKey, kBiometricKeychainService, group);
|
|
@@ -546,14 +567,16 @@ void IOSStorageAdapterCpp::setSecureBiometricWithLevel(const std::string& key, c
|
|
|
546
567
|
try {
|
|
547
568
|
setSecure(key, *backup);
|
|
548
569
|
} catch (const std::exception& restoreEx) {
|
|
549
|
-
throw
|
|
570
|
+
throw taggedStorageError(
|
|
571
|
+
"biometric_unavailable",
|
|
550
572
|
std::string("NitroStorage: Biometric set failed with status ") +
|
|
551
573
|
std::to_string(addStatus) +
|
|
552
574
|
" and previous value restoration also failed: " + restoreEx.what());
|
|
553
575
|
}
|
|
554
576
|
}
|
|
555
577
|
if (addStatus == errSecInteractionNotAllowed) {
|
|
556
|
-
throw
|
|
578
|
+
throw taggedStorageError(
|
|
579
|
+
"keychain_locked",
|
|
557
580
|
"NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
|
|
558
581
|
"The item is not accessible until the device is unlocked."
|
|
559
582
|
);
|
|
@@ -586,13 +609,17 @@ std::optional<std::string> IOSStorageAdapterCpp::getSecureBiometric(const std::s
|
|
|
586
609
|
if (str) return std::string([str UTF8String]);
|
|
587
610
|
}
|
|
588
611
|
if (status == errSecInteractionNotAllowed) {
|
|
589
|
-
throw
|
|
612
|
+
throw taggedStorageError(
|
|
613
|
+
"keychain_locked",
|
|
590
614
|
"NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
|
|
591
615
|
"The item is not accessible until the device is unlocked."
|
|
592
616
|
);
|
|
593
617
|
}
|
|
594
618
|
if (status == errSecUserCanceled || status == errSecAuthFailed) {
|
|
595
|
-
throw
|
|
619
|
+
throw taggedStorageError(
|
|
620
|
+
"authentication_required",
|
|
621
|
+
"NitroStorage: Biometric authentication failed"
|
|
622
|
+
);
|
|
596
623
|
}
|
|
597
624
|
return std::nullopt;
|
|
598
625
|
}
|
|
@@ -640,7 +667,10 @@ void IOSStorageAdapterCpp::clearSecureBiometric() {
|
|
|
640
667
|
OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
|
|
641
668
|
if (status != errSecSuccess && status != errSecItemNotFound) {
|
|
642
669
|
if (status == errSecInteractionNotAllowed) {
|
|
643
|
-
throw
|
|
670
|
+
throw taggedStorageError(
|
|
671
|
+
"keychain_locked",
|
|
672
|
+
"NitroStorage: Cannot clear biometric storage: keychain is locked (errSecInteractionNotAllowed)"
|
|
673
|
+
);
|
|
644
674
|
}
|
|
645
675
|
throw std::runtime_error(
|
|
646
676
|
std::string("NitroStorage: clearSecureBiometric failed with status ") + std::to_string(status));
|