react-native-nitro-storage 0.10.4 → 0.12.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 +86 -14
- package/README.md +149 -41
- package/SECURITY.md +16 -6
- package/android/build.gradle +0 -1
- package/android/consumer-rules.pro +0 -3
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +1 -4
- package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +3 -3
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +107 -36
- package/android/src/main/java/com/nitrostorage/DiskSqliteStore.kt +32 -13
- package/app.plugin.js +51 -51
- package/cpp/bindings/HybridStorage.cpp +25 -6
- package/cpp/core/NativeStorageAdapter.hpp +14 -1
- package/cpp/core/SqliteDiskStore.cpp +105 -20
- package/cpp/core/SqliteDiskStore.hpp +11 -0
- package/docs/api-reference.md +74 -28
- package/docs/benchmarks.md +4 -12
- package/docs/mmkv-migration.md +3 -1
- package/docs/native-libraries.md +10 -4
- package/docs/secure-storage.md +26 -12
- package/docs/web-backends.md +6 -2
- package/indexeddb-backend/package.json +6 -0
- package/ios/IOSStorageAdapterCpp.hpp +4 -0
- package/ios/IOSStorageAdapterCpp.mm +96 -18
- package/lib/commonjs/capabilities.js +3 -8
- package/lib/commonjs/capabilities.js.map +1 -1
- package/lib/commonjs/index.js +23 -8
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +63 -20
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/indexeddb-backend.js +21 -37
- package/lib/commonjs/indexeddb-backend.js.map +1 -1
- package/lib/commonjs/internal.js +6 -0
- package/lib/commonjs/internal.js.map +1 -1
- package/lib/commonjs/storage-core.js +35 -11
- package/lib/commonjs/storage-core.js.map +1 -1
- package/lib/commonjs/storage-runtime.js +1 -1
- package/lib/commonjs/storage-runtime.js.map +1 -1
- package/lib/commonjs/testing.js +68 -23
- package/lib/commonjs/testing.js.map +1 -1
- package/lib/commonjs/web-backend-contract.js +6 -2
- package/lib/commonjs/web-backend-contract.js.map +1 -1
- package/lib/module/capabilities.js +3 -8
- package/lib/module/capabilities.js.map +1 -1
- package/lib/module/index.js +14 -5
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +63 -13
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/indexeddb-backend.js +21 -37
- package/lib/module/indexeddb-backend.js.map +1 -1
- package/lib/module/internal.js +5 -0
- package/lib/module/internal.js.map +1 -1
- package/lib/module/storage-core.js +36 -12
- package/lib/module/storage-core.js.map +1 -1
- package/lib/module/storage-runtime.js +1 -1
- package/lib/module/storage-runtime.js.map +1 -1
- package/lib/module/testing.js +59 -16
- package/lib/module/testing.js.map +1 -1
- package/lib/module/web-backend-contract.js +5 -2
- package/lib/module/web-backend-contract.js.map +1 -1
- package/lib/typescript/capabilities.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +8 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +7 -1
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
- package/lib/typescript/internal.d.ts +1 -0
- package/lib/typescript/internal.d.ts.map +1 -1
- package/lib/typescript/storage-core.d.ts +1 -1
- package/lib/typescript/storage-core.d.ts.map +1 -1
- package/lib/typescript/storage-runtime.d.ts +1 -1
- package/lib/typescript/storage-runtime.d.ts.map +1 -1
- package/lib/typescript/testing.d.ts +15 -2
- package/lib/typescript/testing.d.ts.map +1 -1
- package/lib/typescript/web-backend-contract.d.ts +1 -0
- package/lib/typescript/web-backend-contract.d.ts.map +1 -1
- package/package.json +14 -5
- package/react-native-nitro-storage.podspec +4 -2
- package/src/capabilities.ts +4 -9
- package/src/index.ts +19 -5
- package/src/index.web.ts +91 -20
- package/src/indexeddb-backend.ts +20 -42
- package/src/internal.ts +8 -0
- package/src/storage-core.ts +52 -19
- package/src/storage-runtime.ts +5 -1
- package/src/testing.ts +91 -18
- package/src/web-backend-contract.ts +6 -2
- package/testing/package.json +8 -0
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
package com.nitrostorage
|
|
4
4
|
|
|
5
5
|
import android.content.Context
|
|
6
|
+
import android.database.sqlite.SQLiteDatabaseCorruptException
|
|
7
|
+
import android.database.sqlite.SQLiteException
|
|
8
|
+
import android.database.sqlite.SQLiteFullException
|
|
6
9
|
import android.os.Build
|
|
7
10
|
import android.security.keystore.KeyGenParameterSpec
|
|
8
11
|
import android.security.keystore.KeyPermanentlyInvalidatedException
|
|
@@ -33,7 +36,9 @@ private fun Throwable.storageErrorCode(): String? {
|
|
|
33
36
|
return taggedCode
|
|
34
37
|
}
|
|
35
38
|
return when {
|
|
36
|
-
hasCause(
|
|
39
|
+
hasCause(SQLiteFullException::class.java) -> "storage_full"
|
|
40
|
+
hasCause(SQLiteDatabaseCorruptException::class.java) ||
|
|
41
|
+
hasCause(AEADBadTagException::class.java) -> "storage_corruption"
|
|
37
42
|
hasCause(UserNotAuthenticatedException::class.java) ||
|
|
38
43
|
hasCause(KeyStoreException::class.java) -> "authentication_required"
|
|
39
44
|
hasCause(KeyPermanentlyInvalidatedException::class.java) ||
|
|
@@ -61,24 +66,50 @@ private fun Throwable.wrapStorageException(
|
|
|
61
66
|
class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
62
67
|
private val sharedPreferences: SharedPreferences =
|
|
63
68
|
context.getSharedPreferences("NitroStorage", Context.MODE_PRIVATE)
|
|
64
|
-
private val diskStore: DiskSqliteStore
|
|
69
|
+
private val diskStore: DiskSqliteStore by lazy {
|
|
70
|
+
DiskSqliteStore(context, sharedPreferences)
|
|
71
|
+
}
|
|
65
72
|
|
|
66
73
|
private val masterKeyAlias = "${context.packageName}.nitro_storage.master_key"
|
|
67
74
|
|
|
68
|
-
private val masterKey: MasterKey
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
private val masterKey: MasterKey by lazy {
|
|
76
|
+
try {
|
|
77
|
+
createDefaultMasterKey()
|
|
78
|
+
} catch (e: Exception) {
|
|
79
|
+
throw permanentSecureStoreError(
|
|
80
|
+
"NitroStorage: Cannot create the Secure encryption key.",
|
|
81
|
+
e,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
74
84
|
}
|
|
75
85
|
|
|
76
|
-
private val encryptedPreferences: SharedPreferences
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
86
|
+
private val encryptedPreferences: SharedPreferences by lazy {
|
|
87
|
+
try {
|
|
88
|
+
initializeEncryptedPreferences(
|
|
89
|
+
"NitroStorageSecure",
|
|
90
|
+
masterKey,
|
|
91
|
+
masterKeyAlias,
|
|
92
|
+
::createDefaultMasterKey,
|
|
93
|
+
)
|
|
94
|
+
} catch (e: Exception) {
|
|
95
|
+
throw permanentSecureStoreError(
|
|
96
|
+
"NitroStorage: Cannot open Secure storage.",
|
|
97
|
+
e,
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private fun permanentSecureStoreError(message: String, cause: Exception): RuntimeException {
|
|
103
|
+
if (cause.message?.contains("[nitro-error:storage_corruption]") == true &&
|
|
104
|
+
cause is RuntimeException
|
|
105
|
+
) {
|
|
106
|
+
return cause
|
|
107
|
+
}
|
|
108
|
+
return RuntimeException(
|
|
109
|
+
"[nitro-error:storage_corruption] $message ${cause.message ?: ""}".trimEnd(),
|
|
110
|
+
cause,
|
|
111
|
+
)
|
|
112
|
+
}
|
|
82
113
|
|
|
83
114
|
private val biometricMasterKeyAlias = "${context.packageName}.nitro_storage.biometric_key"
|
|
84
115
|
private val biometricOrPasscodeMasterKeyAlias =
|
|
@@ -273,25 +304,55 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
273
304
|
|
|
274
305
|
private fun existingBiometricPreferences(): List<SharedPreferences> {
|
|
275
306
|
val stores = mutableListOf<SharedPreferences>()
|
|
307
|
+
if (
|
|
308
|
+
biometricOrPasscodeInitialized ||
|
|
309
|
+
preferencesFileExists("NitroStorageBiometricOrPasscode")
|
|
310
|
+
) {
|
|
311
|
+
stores.add(biometricOrPasscodePreferences)
|
|
312
|
+
}
|
|
313
|
+
if (
|
|
314
|
+
legacyBiometricInitialized ||
|
|
315
|
+
preferencesFileExists("NitroStorageBiometric")
|
|
316
|
+
) {
|
|
317
|
+
stores.add(legacyBiometricPreferences)
|
|
318
|
+
}
|
|
276
319
|
if (
|
|
277
320
|
biometricOnlyInitialized ||
|
|
278
321
|
preferencesFileExists("NitroStorageBiometricOnly")
|
|
279
322
|
) {
|
|
280
323
|
stores.add(biometricOnlyPreferences)
|
|
281
324
|
}
|
|
325
|
+
return stores
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private fun <T : Any> firstInExistingBiometricPreferences(
|
|
329
|
+
lookup: (SharedPreferences) -> T?,
|
|
330
|
+
): T? {
|
|
282
331
|
if (
|
|
283
332
|
biometricOrPasscodeInitialized ||
|
|
284
333
|
preferencesFileExists("NitroStorageBiometricOrPasscode")
|
|
285
334
|
) {
|
|
286
|
-
|
|
335
|
+
lookup(biometricOrPasscodePreferences)?.let { return it }
|
|
287
336
|
}
|
|
288
337
|
if (
|
|
289
338
|
legacyBiometricInitialized ||
|
|
290
339
|
preferencesFileExists("NitroStorageBiometric")
|
|
291
340
|
) {
|
|
292
|
-
|
|
341
|
+
lookup(legacyBiometricPreferences)?.let { return it }
|
|
293
342
|
}
|
|
294
|
-
|
|
343
|
+
if (
|
|
344
|
+
biometricOnlyInitialized ||
|
|
345
|
+
preferencesFileExists("NitroStorageBiometricOnly")
|
|
346
|
+
) {
|
|
347
|
+
lookup(biometricOnlyPreferences)?.let { return it }
|
|
348
|
+
}
|
|
349
|
+
return null
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private fun biometricPreferencesContain(key: String): Boolean {
|
|
353
|
+
return firstInExistingBiometricPreferences { preferences ->
|
|
354
|
+
if (preferences.contains(key)) true else null
|
|
355
|
+
} ?: false
|
|
295
356
|
}
|
|
296
357
|
|
|
297
358
|
private fun biometricPreferencesForLevel(level: Int): SharedPreferences {
|
|
@@ -518,59 +579,73 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
518
579
|
|
|
519
580
|
// --- Disk ---
|
|
520
581
|
|
|
582
|
+
private inline fun <T> diskOperation(
|
|
583
|
+
operation: String,
|
|
584
|
+
block: DiskSqliteStore.() -> T,
|
|
585
|
+
): T {
|
|
586
|
+
val instance = getInstanceOrThrow()
|
|
587
|
+
try {
|
|
588
|
+
return instance.diskStore.block()
|
|
589
|
+
} catch (e: SQLiteException) {
|
|
590
|
+
throw e.wrapStorageException(
|
|
591
|
+
"NitroStorage: Disk SQLite $operation failed: ${e.message}",
|
|
592
|
+
)
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
521
596
|
@JvmStatic
|
|
522
597
|
fun setDisk(key: String, value: String) {
|
|
523
|
-
|
|
598
|
+
diskOperation("set") { set(key, value) }
|
|
524
599
|
}
|
|
525
600
|
|
|
526
601
|
@JvmStatic
|
|
527
602
|
fun setDiskBatch(keys: Array<String>, values: Array<String>) {
|
|
528
|
-
|
|
603
|
+
diskOperation("set") { setBatch(keys, values) }
|
|
529
604
|
}
|
|
530
605
|
|
|
531
606
|
@JvmStatic
|
|
532
607
|
fun getDisk(key: String): String? {
|
|
533
|
-
return
|
|
608
|
+
return diskOperation("get") { get(key) }
|
|
534
609
|
}
|
|
535
610
|
|
|
536
611
|
@JvmStatic
|
|
537
612
|
fun getDiskBatch(keys: Array<String>): Array<String?> {
|
|
538
|
-
return
|
|
613
|
+
return diskOperation("get") { getBatch(keys) }
|
|
539
614
|
}
|
|
540
615
|
|
|
541
616
|
@JvmStatic
|
|
542
617
|
fun deleteDisk(key: String) {
|
|
543
|
-
|
|
618
|
+
diskOperation("remove") { remove(key) }
|
|
544
619
|
}
|
|
545
620
|
|
|
546
621
|
@JvmStatic
|
|
547
622
|
fun deleteDiskBatch(keys: Array<String>) {
|
|
548
|
-
|
|
623
|
+
diskOperation("remove") { removeBatch(keys) }
|
|
549
624
|
}
|
|
550
625
|
|
|
551
626
|
@JvmStatic
|
|
552
627
|
fun hasDisk(key: String): Boolean {
|
|
553
|
-
return
|
|
628
|
+
return diskOperation("has") { has(key) }
|
|
554
629
|
}
|
|
555
630
|
|
|
556
631
|
@JvmStatic
|
|
557
632
|
fun getAllKeysDisk(): Array<String> {
|
|
558
|
-
return
|
|
633
|
+
return diskOperation("getAllKeys") { getAllKeys() }
|
|
559
634
|
}
|
|
560
635
|
|
|
561
636
|
@JvmStatic
|
|
562
637
|
fun getKeysByPrefixDisk(prefix: String): Array<String> {
|
|
563
|
-
return
|
|
638
|
+
return diskOperation("getKeysByPrefix") { getKeysByPrefix(prefix) }
|
|
564
639
|
}
|
|
565
640
|
|
|
566
641
|
@JvmStatic
|
|
567
642
|
fun sizeDisk(): Int {
|
|
568
|
-
return
|
|
643
|
+
return diskOperation("size") { size() }
|
|
569
644
|
}
|
|
570
645
|
|
|
571
646
|
@JvmStatic
|
|
572
647
|
fun clearDisk() {
|
|
573
|
-
|
|
648
|
+
diskOperation("clear") { clear() }
|
|
574
649
|
}
|
|
575
650
|
|
|
576
651
|
// --- Secure (async apply by default, sync commit when requested) ---
|
|
@@ -663,7 +738,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
663
738
|
if (inst.encryptedPreferences.contains(key)) {
|
|
664
739
|
return true
|
|
665
740
|
}
|
|
666
|
-
return inst.
|
|
741
|
+
return inst.biometricPreferencesContain(key)
|
|
667
742
|
}
|
|
668
743
|
|
|
669
744
|
@JvmStatic
|
|
@@ -776,13 +851,9 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
776
851
|
fun getSecureBiometric(key: String): String? {
|
|
777
852
|
val inst = getInstanceOrThrow()
|
|
778
853
|
return try {
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
if (value != null) {
|
|
782
|
-
return value
|
|
783
|
-
}
|
|
854
|
+
inst.firstInExistingBiometricPreferences { preferences ->
|
|
855
|
+
inst.getSecureSafe(preferences, key)
|
|
784
856
|
}
|
|
785
|
-
null
|
|
786
857
|
} catch (e: Exception) {
|
|
787
858
|
throw e.wrapStorageException(
|
|
788
859
|
"NitroStorage: Failed to read biometric storage: ${e.message}",
|
|
@@ -809,7 +880,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
809
880
|
fun hasSecureBiometric(key: String): Boolean {
|
|
810
881
|
val inst = getInstanceOrThrow()
|
|
811
882
|
return try {
|
|
812
|
-
inst.
|
|
883
|
+
inst.biometricPreferencesContain(key)
|
|
813
884
|
} catch (e: Exception) {
|
|
814
885
|
throw e.wrapStorageException(
|
|
815
886
|
"NitroStorage: Failed to inspect biometric storage: ${e.message}",
|
|
@@ -9,21 +9,25 @@ internal class DiskSqliteStore(
|
|
|
9
9
|
context: Context,
|
|
10
10
|
private val legacyPreferences: SharedPreferences,
|
|
11
11
|
) {
|
|
12
|
-
private val db: SQLiteDatabase
|
|
12
|
+
private val db: SQLiteDatabase =
|
|
13
|
+
SQLiteDatabase.openOrCreateDatabase(File(context.filesDir, DATABASE_NAME), null)
|
|
13
14
|
|
|
14
15
|
init {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
16
|
+
try {
|
|
17
|
+
// PRAGMA journal_mode returns a row; Android forbids result-bearing SQL on execSQL.
|
|
18
|
+
db.rawQuery("PRAGMA journal_mode=WAL", null).close()
|
|
19
|
+
db.execSQL("PRAGMA synchronous=NORMAL")
|
|
20
|
+
db.execSQL(
|
|
21
|
+
"CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY NOT NULL, value TEXT NOT NULL)",
|
|
22
|
+
)
|
|
23
|
+
db.execSQL(
|
|
24
|
+
"CREATE TABLE IF NOT EXISTS meta (k TEXT PRIMARY KEY NOT NULL, v TEXT NOT NULL)",
|
|
25
|
+
)
|
|
26
|
+
migrateLegacyPreferences()
|
|
27
|
+
} catch (e: Exception) {
|
|
28
|
+
db.close()
|
|
29
|
+
throw e
|
|
30
|
+
}
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
@Synchronized
|
|
@@ -44,6 +48,7 @@ internal class DiskSqliteStore(
|
|
|
44
48
|
@Synchronized
|
|
45
49
|
fun remove(key: String) {
|
|
46
50
|
db.execSQL("DELETE FROM kv WHERE key = ?", arrayOf(key))
|
|
51
|
+
removeLegacyKeys(listOf(key))
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
@Synchronized
|
|
@@ -86,6 +91,7 @@ internal class DiskSqliteStore(
|
|
|
86
91
|
} finally {
|
|
87
92
|
db.endTransaction()
|
|
88
93
|
}
|
|
94
|
+
removeLegacyKeys(keys.asIterable())
|
|
89
95
|
}
|
|
90
96
|
|
|
91
97
|
@Synchronized
|
|
@@ -135,6 +141,9 @@ internal class DiskSqliteStore(
|
|
|
135
141
|
@Synchronized
|
|
136
142
|
fun clear() {
|
|
137
143
|
db.execSQL("DELETE FROM kv")
|
|
144
|
+
if (legacyPreferences.all.isNotEmpty()) {
|
|
145
|
+
legacyPreferences.edit().clear().apply()
|
|
146
|
+
}
|
|
138
147
|
}
|
|
139
148
|
|
|
140
149
|
private fun migrateLegacyPreferences() {
|
|
@@ -167,6 +176,16 @@ internal class DiskSqliteStore(
|
|
|
167
176
|
}
|
|
168
177
|
}
|
|
169
178
|
|
|
179
|
+
private fun removeLegacyKeys(keys: Iterable<String>) {
|
|
180
|
+
val present = keys.filter { legacyPreferences.contains(it) }
|
|
181
|
+
if (present.isEmpty()) {
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
val editor = legacyPreferences.edit()
|
|
185
|
+
present.forEach { editor.remove(it) }
|
|
186
|
+
editor.apply()
|
|
187
|
+
}
|
|
188
|
+
|
|
170
189
|
private companion object {
|
|
171
190
|
const val DATABASE_NAME = "nitro-storage-disk.sqlite"
|
|
172
191
|
const val PREFS_MIGRATION_KEY = "prefs_v1"
|
package/app.plugin.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
1
3
|
const {
|
|
2
4
|
withInfoPlist,
|
|
3
5
|
withAndroidManifest,
|
|
4
6
|
withDangerousMod,
|
|
5
7
|
createRunOncePlugin,
|
|
6
|
-
} = require("
|
|
7
|
-
const fs = require("fs");
|
|
8
|
-
const path = require("path");
|
|
8
|
+
} = require("expo/config-plugins");
|
|
9
9
|
const pkg = require("./package.json");
|
|
10
10
|
|
|
11
11
|
const DATA_EXTRACTION_RULES_RESOURCE =
|
|
@@ -84,63 +84,61 @@ function writeAndroidBackupFiles(projectRoot) {
|
|
|
84
84
|
);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
"Allow $(PRODUCT_NAME) to use Face ID for secure authentication";
|
|
90
|
-
const {
|
|
91
|
-
faceIDPermission,
|
|
92
|
-
addBiometricPermissions = false,
|
|
93
|
-
configureAndroidBackup = true,
|
|
94
|
-
} = props;
|
|
95
|
-
|
|
96
|
-
config = withInfoPlist(config, (config) => {
|
|
97
|
-
if (
|
|
98
|
-
typeof faceIDPermission === "string" &&
|
|
99
|
-
faceIDPermission.trim() !== ""
|
|
100
|
-
) {
|
|
101
|
-
config.modResults.NSFaceIDUsageDescription = faceIDPermission;
|
|
102
|
-
} else if (!config.modResults.NSFaceIDUsageDescription) {
|
|
103
|
-
config.modResults.NSFaceIDUsageDescription = defaultFaceIDPermission;
|
|
104
|
-
}
|
|
105
|
-
return config;
|
|
106
|
-
});
|
|
87
|
+
const DEFAULT_FACE_ID_PERMISSION =
|
|
88
|
+
"Allow $(PRODUCT_NAME) to use Face ID for secure authentication";
|
|
107
89
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
90
|
+
const BIOMETRIC_PERMISSIONS = [
|
|
91
|
+
"android.permission.USE_BIOMETRIC",
|
|
92
|
+
"android.permission.USE_FINGERPRINT",
|
|
93
|
+
];
|
|
112
94
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
95
|
+
function applyInfoPlist(infoPlist, props = {}) {
|
|
96
|
+
const { faceIDPermission } = props;
|
|
97
|
+
if (typeof faceIDPermission === "string" && faceIDPermission.trim() !== "") {
|
|
98
|
+
infoPlist.NSFaceIDUsageDescription = faceIDPermission;
|
|
99
|
+
} else if (!infoPlist.NSFaceIDUsageDescription) {
|
|
100
|
+
infoPlist.NSFaceIDUsageDescription = DEFAULT_FACE_ID_PERMISSION;
|
|
101
|
+
}
|
|
102
|
+
return infoPlist;
|
|
103
|
+
}
|
|
116
104
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
105
|
+
function applyAndroidManifest(androidManifest, props = {}) {
|
|
106
|
+
const { addBiometricPermissions = false, configureAndroidBackup = true } =
|
|
107
|
+
props;
|
|
120
108
|
|
|
121
|
-
|
|
109
|
+
if (configureAndroidBackup) {
|
|
110
|
+
ensureBackupAttributes(androidManifest);
|
|
111
|
+
}
|
|
122
112
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const fingerprintPermission = {
|
|
127
|
-
$: { "android:name": "android.permission.USE_FINGERPRINT" },
|
|
128
|
-
};
|
|
113
|
+
if (!addBiometricPermissions) {
|
|
114
|
+
return androidManifest;
|
|
115
|
+
}
|
|
129
116
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const hasFingerprint = permissions.some(
|
|
134
|
-
(p) => p.$?.["android:name"] === "android.permission.USE_FINGERPRINT",
|
|
135
|
-
);
|
|
117
|
+
if (!androidManifest.manifest["uses-permission"]) {
|
|
118
|
+
androidManifest.manifest["uses-permission"] = [];
|
|
119
|
+
}
|
|
136
120
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (!
|
|
141
|
-
permissions.push(
|
|
121
|
+
const permissions = androidManifest.manifest["uses-permission"];
|
|
122
|
+
for (const name of BIOMETRIC_PERMISSIONS) {
|
|
123
|
+
const present = permissions.some((p) => p.$?.["android:name"] === name);
|
|
124
|
+
if (!present) {
|
|
125
|
+
permissions.push({ $: { "android:name": name } });
|
|
142
126
|
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return androidManifest;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const withNitroStorage = (config, props = {}) => {
|
|
133
|
+
const { configureAndroidBackup = true } = props;
|
|
134
|
+
|
|
135
|
+
config = withInfoPlist(config, (config) => {
|
|
136
|
+
applyInfoPlist(config.modResults, props);
|
|
137
|
+
return config;
|
|
138
|
+
});
|
|
143
139
|
|
|
140
|
+
config = withAndroidManifest(config, (config) => {
|
|
141
|
+
applyAndroidManifest(config.modResults, props);
|
|
144
142
|
return config;
|
|
145
143
|
});
|
|
146
144
|
|
|
@@ -164,6 +162,8 @@ module.exports = createRunOncePlugin(
|
|
|
164
162
|
);
|
|
165
163
|
module.exports.withNitroStorage = withNitroStorage;
|
|
166
164
|
module.exports._internal = {
|
|
165
|
+
applyInfoPlist,
|
|
166
|
+
applyAndroidManifest,
|
|
167
167
|
dataExtractionRulesXml,
|
|
168
168
|
fullBackupContentXml,
|
|
169
169
|
ensureBackupAttributes,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#include "HybridStorage.hpp"
|
|
2
|
+
#include <algorithm>
|
|
2
3
|
#include <cmath>
|
|
3
4
|
#include <stdexcept>
|
|
4
5
|
|
|
@@ -32,8 +33,8 @@ HybridStorage::HybridStorage()
|
|
|
32
33
|
#if __APPLE__
|
|
33
34
|
nativeAdapter_ = std::make_shared<::NitroStorage::IOSStorageAdapterCpp>();
|
|
34
35
|
#elif __ANDROID__
|
|
35
|
-
|
|
36
|
-
nativeAdapter_ = std::make_shared<::NitroStorage::AndroidStorageAdapterCpp>(
|
|
36
|
+
::NitroStorage::AndroidStorageAdapterJava::ensureInitialized();
|
|
37
|
+
nativeAdapter_ = std::make_shared<::NitroStorage::AndroidStorageAdapterCpp>();
|
|
37
38
|
#endif
|
|
38
39
|
#endif
|
|
39
40
|
}
|
|
@@ -338,8 +339,17 @@ void HybridStorage::setBatch(const std::vector<std::string>& keys, const std::ve
|
|
|
338
339
|
break;
|
|
339
340
|
case Scope::Secure:
|
|
340
341
|
ensureAdapter();
|
|
341
|
-
|
|
342
|
-
|
|
342
|
+
try {
|
|
343
|
+
runAdapterOperation(
|
|
344
|
+
[&] { nativeAdapter_->setSecureBatch(keys, values); }, "Secure setBatch");
|
|
345
|
+
} catch (const ::NitroStorage::PartialBatchError& error) {
|
|
346
|
+
const auto listeners = copyListenersForScope(static_cast<int>(s));
|
|
347
|
+
const auto applied = std::min(error.appliedCount(), keys.size());
|
|
348
|
+
for (size_t i = 0; i < applied; ++i) {
|
|
349
|
+
notifyListeners(listeners, keys[i], values[i]);
|
|
350
|
+
}
|
|
351
|
+
throw;
|
|
352
|
+
}
|
|
343
353
|
break;
|
|
344
354
|
}
|
|
345
355
|
|
|
@@ -402,8 +412,17 @@ void HybridStorage::removeBatch(const std::vector<std::string>& keys, double sco
|
|
|
402
412
|
break;
|
|
403
413
|
case Scope::Secure:
|
|
404
414
|
ensureAdapter();
|
|
405
|
-
|
|
406
|
-
|
|
415
|
+
try {
|
|
416
|
+
runAdapterOperation(
|
|
417
|
+
[&] { nativeAdapter_->deleteSecureBatch(keys); }, "Secure removeBatch");
|
|
418
|
+
} catch (const ::NitroStorage::PartialBatchError& error) {
|
|
419
|
+
const auto listeners = copyListenersForScope(static_cast<int>(s));
|
|
420
|
+
const auto applied = std::min(error.appliedCount(), keys.size());
|
|
421
|
+
for (size_t i = 0; i < applied; ++i) {
|
|
422
|
+
notifyListeners(listeners, keys[i], std::nullopt);
|
|
423
|
+
}
|
|
424
|
+
throw;
|
|
425
|
+
}
|
|
407
426
|
break;
|
|
408
427
|
}
|
|
409
428
|
|
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
#pragma once
|
|
2
2
|
|
|
3
|
-
#include <
|
|
3
|
+
#include <cstddef>
|
|
4
4
|
#include <optional>
|
|
5
|
+
#include <stdexcept>
|
|
6
|
+
#include <string>
|
|
5
7
|
#include <vector>
|
|
6
8
|
|
|
7
9
|
namespace NitroStorage {
|
|
8
10
|
|
|
11
|
+
class PartialBatchError : public std::runtime_error {
|
|
12
|
+
public:
|
|
13
|
+
PartialBatchError(size_t appliedCount, const std::string& message)
|
|
14
|
+
: std::runtime_error(message), appliedCount_(appliedCount) {}
|
|
15
|
+
|
|
16
|
+
size_t appliedCount() const noexcept { return appliedCount_; }
|
|
17
|
+
|
|
18
|
+
private:
|
|
19
|
+
size_t appliedCount_;
|
|
20
|
+
};
|
|
21
|
+
|
|
9
22
|
class NativeStorageAdapter {
|
|
10
23
|
public:
|
|
11
24
|
virtual ~NativeStorageAdapter() = default;
|