react-native-nitro-storage 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +89 -29
- package/README.md +81 -24
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +5 -2
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +240 -34
- package/cpp/bindings/HybridStorage.cpp +106 -283
- package/cpp/bindings/HybridStorage.hpp +16 -9
- package/docs/api-reference.md +75 -19
- package/docs/benchmarks.md +6 -1
- package/docs/keychain-lifecycle-testing.md +36 -0
- package/docs/recipes.md +1 -2
- package/docs/secure-storage.md +45 -9
- package/ios/IOSStorageAdapterCpp.mm +391 -175
- package/lib/commonjs/capabilities.js +3 -1
- package/lib/commonjs/capabilities.js.map +1 -1
- package/lib/commonjs/core/durability.js +110 -43
- package/lib/commonjs/core/durability.js.map +1 -1
- package/lib/commonjs/index.js +15 -5
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +219 -97
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/internal.js +3 -11
- package/lib/commonjs/internal.js.map +1 -1
- package/lib/commonjs/shared.js +62 -2
- package/lib/commonjs/shared.js.map +1 -1
- package/lib/commonjs/storage-core.js +852 -141
- package/lib/commonjs/storage-core.js.map +1 -1
- package/lib/commonjs/storage-runtime.js +5 -1
- package/lib/commonjs/storage-runtime.js.map +1 -1
- package/lib/commonjs/testing.js +13 -0
- package/lib/commonjs/testing.js.map +1 -1
- package/lib/module/capabilities.js +2 -1
- package/lib/module/capabilities.js.map +1 -1
- package/lib/module/core/durability.js +110 -43
- package/lib/module/core/durability.js.map +1 -1
- package/lib/module/index.js +12 -8
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +215 -99
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/internal.js +2 -9
- package/lib/module/internal.js.map +1 -1
- package/lib/module/shared.js +60 -2
- package/lib/module/shared.js.map +1 -1
- package/lib/module/storage-core.js +853 -142
- package/lib/module/storage-core.js.map +1 -1
- package/lib/module/storage-runtime.js +4 -1
- package/lib/module/storage-runtime.js.map +1 -1
- package/lib/module/testing.js +8 -1
- package/lib/module/testing.js.map +1 -1
- package/lib/typescript/capabilities.d.ts +2 -1
- package/lib/typescript/capabilities.d.ts.map +1 -1
- package/lib/typescript/core/durability.d.ts +7 -2
- package/lib/typescript/core/durability.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -2
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +3 -29
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/internal.d.ts +0 -2
- package/lib/typescript/internal.d.ts.map +1 -1
- package/lib/typescript/shared.d.ts +19 -0
- package/lib/typescript/shared.d.ts.map +1 -1
- package/lib/typescript/storage-core.d.ts +15 -6
- package/lib/typescript/storage-core.d.ts.map +1 -1
- package/lib/typescript/storage-runtime.d.ts +2 -1
- package/lib/typescript/storage-runtime.d.ts.map +1 -1
- package/lib/typescript/testing.d.ts +1 -1
- package/lib/typescript/testing.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/capabilities.ts +3 -1
- package/src/core/durability.ts +139 -39
- package/src/index.ts +20 -10
- package/src/index.web.ts +328 -166
- package/src/internal.ts +0 -14
- package/src/shared.ts +85 -0
- package/src/storage-core.ts +1165 -202
- package/src/storage-runtime.ts +6 -0
- package/src/testing.ts +8 -1
|
@@ -11,6 +11,7 @@ import android.security.keystore.UserNotAuthenticatedException
|
|
|
11
11
|
import android.content.SharedPreferences
|
|
12
12
|
import androidx.security.crypto.EncryptedSharedPreferences
|
|
13
13
|
import androidx.security.crypto.MasterKey
|
|
14
|
+
import java.io.File
|
|
14
15
|
import java.security.InvalidKeyException
|
|
15
16
|
import java.security.KeyStore
|
|
16
17
|
import java.security.KeyStoreException
|
|
@@ -26,6 +27,11 @@ private fun Throwable.hasCause(type: Class<*>): Boolean {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
private fun Throwable.storageErrorCode(): String? {
|
|
30
|
+
val taggedCode = message
|
|
31
|
+
?.let { Regex("\\[nitro-error:([a-z_]+)]").find(it)?.groupValues?.get(1) }
|
|
32
|
+
if (taggedCode == "storage_compensation_failed") {
|
|
33
|
+
return taggedCode
|
|
34
|
+
}
|
|
29
35
|
return when {
|
|
30
36
|
hasCause(AEADBadTagException::class.java) -> "storage_corruption"
|
|
31
37
|
hasCause(UserNotAuthenticatedException::class.java) ||
|
|
@@ -40,6 +46,9 @@ private fun Throwable.wrapStorageException(
|
|
|
40
46
|
defaultMessage: String,
|
|
41
47
|
defaultCode: String? = null,
|
|
42
48
|
): RuntimeException {
|
|
49
|
+
if (message?.contains("[nitro-error:") == true && this is RuntimeException) {
|
|
50
|
+
return this
|
|
51
|
+
}
|
|
43
52
|
val code = storageErrorCode() ?: defaultCode
|
|
44
53
|
val message = if (code != null) {
|
|
45
54
|
"[nitro-error:$code] $defaultMessage"
|
|
@@ -76,28 +85,43 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
76
85
|
private val biometricOnlyMasterKeyAlias =
|
|
77
86
|
"${context.packageName}.nitro_storage.biometric_only_key"
|
|
78
87
|
|
|
88
|
+
@Volatile
|
|
89
|
+
private var legacyBiometricInitialized = false
|
|
90
|
+
|
|
91
|
+
@Volatile
|
|
92
|
+
private var biometricOrPasscodeInitialized = false
|
|
93
|
+
|
|
94
|
+
@Volatile
|
|
95
|
+
private var biometricOnlyInitialized = false
|
|
96
|
+
|
|
79
97
|
private val legacyBiometricPreferences: SharedPreferences by lazy {
|
|
80
|
-
createBiometricPreferences(
|
|
98
|
+
val preferences = createBiometricPreferences(
|
|
81
99
|
"NitroStorageBiometric",
|
|
82
100
|
biometricMasterKeyAlias,
|
|
83
101
|
1,
|
|
84
102
|
)
|
|
103
|
+
legacyBiometricInitialized = true
|
|
104
|
+
preferences
|
|
85
105
|
}
|
|
86
106
|
|
|
87
107
|
private val biometricOrPasscodePreferences: SharedPreferences by lazy {
|
|
88
|
-
createBiometricPreferences(
|
|
108
|
+
val preferences = createBiometricPreferences(
|
|
89
109
|
"NitroStorageBiometricOrPasscode",
|
|
90
110
|
biometricOrPasscodeMasterKeyAlias,
|
|
91
111
|
1,
|
|
92
112
|
)
|
|
113
|
+
biometricOrPasscodeInitialized = true
|
|
114
|
+
preferences
|
|
93
115
|
}
|
|
94
116
|
|
|
95
117
|
private val biometricOnlyPreferences: SharedPreferences by lazy {
|
|
96
|
-
createBiometricPreferences(
|
|
118
|
+
val preferences = createBiometricPreferences(
|
|
97
119
|
"NitroStorageBiometricOnly",
|
|
98
120
|
biometricOnlyMasterKeyAlias,
|
|
99
121
|
2,
|
|
100
122
|
)
|
|
123
|
+
biometricOnlyInitialized = true
|
|
124
|
+
preferences
|
|
101
125
|
}
|
|
102
126
|
|
|
103
127
|
@Volatile
|
|
@@ -154,7 +178,13 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
154
178
|
): SharedPreferences {
|
|
155
179
|
return try {
|
|
156
180
|
val keyFactory = { createBiometricMasterKey(alias, level) }
|
|
157
|
-
|
|
181
|
+
val preferences = initializeEncryptedPreferences(
|
|
182
|
+
name,
|
|
183
|
+
keyFactory(),
|
|
184
|
+
alias,
|
|
185
|
+
keyFactory,
|
|
186
|
+
)
|
|
187
|
+
verifyReadablePreferences(preferences, name, alias, keyFactory)
|
|
158
188
|
} catch (e: Exception) {
|
|
159
189
|
throw e.wrapStorageException(
|
|
160
190
|
"NitroStorage: Biometric storage is not available on this device. " +
|
|
@@ -164,6 +194,31 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
164
194
|
}
|
|
165
195
|
}
|
|
166
196
|
|
|
197
|
+
private fun verifyReadablePreferences(
|
|
198
|
+
preferences: SharedPreferences,
|
|
199
|
+
name: String,
|
|
200
|
+
alias: String,
|
|
201
|
+
keyFactory: () -> MasterKey,
|
|
202
|
+
): SharedPreferences {
|
|
203
|
+
return try {
|
|
204
|
+
preferences.all
|
|
205
|
+
preferences
|
|
206
|
+
} catch (e: Exception) {
|
|
207
|
+
if (!e.hasCause(AEADBadTagException::class.java)) {
|
|
208
|
+
throw e
|
|
209
|
+
}
|
|
210
|
+
clearCorruptedStorage(name, alias)
|
|
211
|
+
try {
|
|
212
|
+
initializeEncryptedPreferences(name, keyFactory(), alias, keyFactory)
|
|
213
|
+
} catch (retryEx: Exception) {
|
|
214
|
+
throw retryEx.wrapStorageException(
|
|
215
|
+
"NitroStorage: Unrecoverable storage corruption in $name",
|
|
216
|
+
defaultCode = "storage_corruption",
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
167
222
|
private fun initializeEncryptedPreferences(
|
|
168
223
|
name: String,
|
|
169
224
|
key: MasterKey,
|
|
@@ -212,23 +267,29 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
212
267
|
}
|
|
213
268
|
|
|
214
269
|
private fun preferencesFileExists(name: String): Boolean {
|
|
215
|
-
return context.
|
|
270
|
+
return File(context.applicationInfo.dataDir, "shared_prefs/$name.xml").exists()
|
|
216
271
|
}
|
|
217
272
|
|
|
218
273
|
private fun existingBiometricPreferences(): List<SharedPreferences> {
|
|
219
274
|
val stores = mutableListOf<SharedPreferences>()
|
|
220
|
-
if (
|
|
275
|
+
if (
|
|
276
|
+
biometricOnlyInitialized ||
|
|
277
|
+
preferencesFileExists("NitroStorageBiometricOnly")
|
|
278
|
+
) {
|
|
221
279
|
stores.add(biometricOnlyPreferences)
|
|
222
280
|
}
|
|
223
|
-
if (
|
|
281
|
+
if (
|
|
282
|
+
biometricOrPasscodeInitialized ||
|
|
283
|
+
preferencesFileExists("NitroStorageBiometricOrPasscode")
|
|
284
|
+
) {
|
|
224
285
|
stores.add(biometricOrPasscodePreferences)
|
|
225
286
|
}
|
|
226
|
-
if (
|
|
287
|
+
if (
|
|
288
|
+
legacyBiometricInitialized ||
|
|
289
|
+
preferencesFileExists("NitroStorageBiometric")
|
|
290
|
+
) {
|
|
227
291
|
stores.add(legacyBiometricPreferences)
|
|
228
292
|
}
|
|
229
|
-
for (preferences in stores) {
|
|
230
|
-
preferences.all
|
|
231
|
-
}
|
|
232
293
|
return stores
|
|
233
294
|
}
|
|
234
295
|
|
|
@@ -245,17 +306,19 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
245
306
|
private fun removeBiometricKey(
|
|
246
307
|
key: String,
|
|
247
308
|
preferencesList: List<SharedPreferences> = existingBiometricPreferences(),
|
|
309
|
+
forceSynchronous: Boolean = false,
|
|
248
310
|
) {
|
|
249
311
|
for (preferences in preferencesList) {
|
|
250
|
-
applySecureEditor(preferences.edit().remove(key))
|
|
312
|
+
applySecureEditor(preferences.edit().remove(key), forceSynchronous)
|
|
251
313
|
}
|
|
252
314
|
}
|
|
253
315
|
|
|
254
316
|
private fun clearBiometricStores(
|
|
255
317
|
preferencesList: List<SharedPreferences> = existingBiometricPreferences(),
|
|
318
|
+
forceSynchronous: Boolean = false,
|
|
256
319
|
) {
|
|
257
320
|
for (preferences in preferencesList) {
|
|
258
|
-
applySecureEditor(preferences.edit().clear())
|
|
321
|
+
applySecureEditor(preferences.edit().clear(), forceSynchronous)
|
|
259
322
|
}
|
|
260
323
|
}
|
|
261
324
|
|
|
@@ -272,9 +335,12 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
272
335
|
}
|
|
273
336
|
}
|
|
274
337
|
|
|
275
|
-
private fun applySecureEditor(
|
|
338
|
+
private fun applySecureEditor(
|
|
339
|
+
editor: SharedPreferences.Editor,
|
|
340
|
+
forceSynchronous: Boolean = false,
|
|
341
|
+
) {
|
|
276
342
|
try {
|
|
277
|
-
if (secureWritesAsync) {
|
|
343
|
+
if (secureWritesAsync && !forceSynchronous) {
|
|
278
344
|
editor.apply()
|
|
279
345
|
} else if (!editor.commit()) {
|
|
280
346
|
throw IllegalStateException("SharedPreferences commit returned false")
|
|
@@ -286,6 +352,108 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
286
352
|
}
|
|
287
353
|
}
|
|
288
354
|
|
|
355
|
+
private data class BiometricEntrySnapshot(
|
|
356
|
+
val preferences: SharedPreferences,
|
|
357
|
+
val present: Boolean,
|
|
358
|
+
val value: String?,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
private data class PlainEntrySnapshot(
|
|
362
|
+
val present: Boolean,
|
|
363
|
+
val value: String?,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
private fun captureBiometricEntrySnapshots(key: String): List<BiometricEntrySnapshot> {
|
|
367
|
+
return existingBiometricPreferences().map { preferences ->
|
|
368
|
+
val present = preferences.contains(key)
|
|
369
|
+
BiometricEntrySnapshot(
|
|
370
|
+
preferences = preferences,
|
|
371
|
+
present = present,
|
|
372
|
+
value = if (present) getSecureSafe(preferences, key) else null,
|
|
373
|
+
)
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private fun capturePlainEntrySnapshot(key: String): PlainEntrySnapshot {
|
|
378
|
+
val present = encryptedPreferences.contains(key)
|
|
379
|
+
return PlainEntrySnapshot(
|
|
380
|
+
present = present,
|
|
381
|
+
value = if (present) getSecureSafe(encryptedPreferences, key) else null,
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private fun restoreBiometricEntrySnapshots(
|
|
386
|
+
key: String,
|
|
387
|
+
snapshots: List<BiometricEntrySnapshot>,
|
|
388
|
+
additionalPreferences: List<SharedPreferences>,
|
|
389
|
+
): List<Throwable> {
|
|
390
|
+
val errors = mutableListOf<Throwable>()
|
|
391
|
+
val existingPreferences = try {
|
|
392
|
+
existingBiometricPreferences()
|
|
393
|
+
} catch (error: Throwable) {
|
|
394
|
+
errors.add(error)
|
|
395
|
+
emptyList()
|
|
396
|
+
}
|
|
397
|
+
val preferencesToClear = (
|
|
398
|
+
existingPreferences +
|
|
399
|
+
snapshots.map { it.preferences } +
|
|
400
|
+
additionalPreferences
|
|
401
|
+
).distinct()
|
|
402
|
+
preferencesToClear.forEach { preferences ->
|
|
403
|
+
try {
|
|
404
|
+
applySecureEditor(preferences.edit().remove(key), forceSynchronous = true)
|
|
405
|
+
} catch (error: Throwable) {
|
|
406
|
+
errors.add(error)
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
snapshots.filter { it.present }.forEach { snapshot ->
|
|
410
|
+
try {
|
|
411
|
+
val value = snapshot.value
|
|
412
|
+
?: throw IllegalStateException("Biometric snapshot contained a null value")
|
|
413
|
+
applySecureEditor(
|
|
414
|
+
snapshot.preferences.edit().putString(key, value),
|
|
415
|
+
forceSynchronous = true,
|
|
416
|
+
)
|
|
417
|
+
} catch (error: Throwable) {
|
|
418
|
+
errors.add(error)
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return errors
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private fun restorePlainEntrySnapshot(
|
|
425
|
+
key: String,
|
|
426
|
+
snapshot: PlainEntrySnapshot,
|
|
427
|
+
): List<Throwable> {
|
|
428
|
+
return try {
|
|
429
|
+
val editor = if (snapshot.present) {
|
|
430
|
+
val value = snapshot.value
|
|
431
|
+
?: throw IllegalStateException("Plain snapshot contained a null value")
|
|
432
|
+
encryptedPreferences.edit().putString(key, value)
|
|
433
|
+
} else {
|
|
434
|
+
encryptedPreferences.edit().remove(key)
|
|
435
|
+
}
|
|
436
|
+
applySecureEditor(editor, forceSynchronous = true)
|
|
437
|
+
emptyList()
|
|
438
|
+
} catch (error: Throwable) {
|
|
439
|
+
listOf(error)
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private fun compositeBiometricMutationError(
|
|
444
|
+
operation: String,
|
|
445
|
+
primary: Throwable,
|
|
446
|
+
rollbackErrors: List<Throwable>,
|
|
447
|
+
): RuntimeException {
|
|
448
|
+
val composite = RuntimeException(
|
|
449
|
+
"[nitro-error:storage_compensation_failed] NitroStorage: $operation failed; " +
|
|
450
|
+
"rollback_error_count=${rollbackErrors.size}",
|
|
451
|
+
primary,
|
|
452
|
+
)
|
|
453
|
+
rollbackErrors.forEach(composite::addSuppressed)
|
|
454
|
+
return composite
|
|
455
|
+
}
|
|
456
|
+
|
|
289
457
|
private fun invalidateSecureKeysCache() {
|
|
290
458
|
synchronized(this) {
|
|
291
459
|
secureKeysCache = null
|
|
@@ -418,7 +586,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
418
586
|
getInstanceOrThrow().sharedPreferences.edit().clear().apply()
|
|
419
587
|
}
|
|
420
588
|
|
|
421
|
-
// --- Secure (
|
|
589
|
+
// --- Secure (async apply by default, sync commit when requested) ---
|
|
422
590
|
|
|
423
591
|
@JvmStatic
|
|
424
592
|
fun setSecure(key: String, value: String) {
|
|
@@ -550,29 +718,66 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
550
718
|
val inst = getInstanceOrThrow()
|
|
551
719
|
try {
|
|
552
720
|
synchronized(inst) {
|
|
553
|
-
val
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
)
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
721
|
+
val previousBiometric = inst.captureBiometricEntrySnapshots(key)
|
|
722
|
+
val previousPlain = inst.capturePlainEntrySnapshot(key)
|
|
723
|
+
val additionalPreferences = mutableListOf<SharedPreferences>()
|
|
724
|
+
|
|
725
|
+
try {
|
|
726
|
+
if (level == 0) {
|
|
727
|
+
inst.removeBiometricKey(
|
|
728
|
+
key,
|
|
729
|
+
previousBiometric.map { it.preferences },
|
|
730
|
+
forceSynchronous = true,
|
|
731
|
+
)
|
|
732
|
+
inst.applySecureEditor(
|
|
733
|
+
inst.encryptedPreferences.edit().putString(key, value),
|
|
734
|
+
forceSynchronous = true,
|
|
735
|
+
)
|
|
736
|
+
} else {
|
|
737
|
+
val targetPreferences = inst.biometricPreferencesForLevel(level)
|
|
738
|
+
additionalPreferences.add(targetPreferences)
|
|
739
|
+
inst.applySecureEditor(
|
|
740
|
+
targetPreferences.edit().putString(key, value),
|
|
741
|
+
forceSynchronous = true,
|
|
742
|
+
)
|
|
743
|
+
for (preferences in previousBiometric.map { it.preferences }) {
|
|
744
|
+
if (preferences !== targetPreferences) {
|
|
745
|
+
inst.applySecureEditor(
|
|
746
|
+
preferences.edit().remove(key),
|
|
747
|
+
forceSynchronous = true,
|
|
748
|
+
)
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
inst.applySecureEditor(
|
|
752
|
+
inst.encryptedPreferences.edit().remove(key),
|
|
753
|
+
forceSynchronous = true,
|
|
754
|
+
)
|
|
755
|
+
}
|
|
756
|
+
} catch (primary: Exception) {
|
|
757
|
+
val rollbackErrors = mutableListOf<Throwable>()
|
|
758
|
+
rollbackErrors.addAll(
|
|
759
|
+
inst.restoreBiometricEntrySnapshots(
|
|
760
|
+
key,
|
|
761
|
+
previousBiometric,
|
|
762
|
+
additionalPreferences,
|
|
763
|
+
),
|
|
563
764
|
)
|
|
564
|
-
|
|
565
|
-
inst.
|
|
765
|
+
rollbackErrors.addAll(
|
|
766
|
+
inst.restorePlainEntrySnapshot(key, previousPlain),
|
|
566
767
|
)
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
768
|
+
if (rollbackErrors.isNotEmpty()) {
|
|
769
|
+
throw inst.compositeBiometricMutationError(
|
|
770
|
+
if (level == 0) "Secure demotion" else "Biometric promotion",
|
|
771
|
+
primary,
|
|
772
|
+
rollbackErrors,
|
|
773
|
+
)
|
|
571
774
|
}
|
|
775
|
+
throw primary
|
|
572
776
|
}
|
|
573
777
|
inst.invalidateSecureKeysCache()
|
|
574
778
|
}
|
|
575
779
|
} catch (e: Exception) {
|
|
780
|
+
inst.invalidateSecureKeysCache()
|
|
576
781
|
throw e.wrapStorageException(
|
|
577
782
|
"NitroStorage: Biometric storage unavailable on this device",
|
|
578
783
|
defaultCode = "biometric_unavailable",
|
|
@@ -630,13 +835,14 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
630
835
|
val inst = getInstanceOrThrow()
|
|
631
836
|
try {
|
|
632
837
|
synchronized(inst) {
|
|
633
|
-
inst.clearBiometricStores()
|
|
634
|
-
inst.invalidateSecureKeysCache()
|
|
838
|
+
inst.clearBiometricStores(forceSynchronous = true)
|
|
635
839
|
}
|
|
636
840
|
} catch (e: Exception) {
|
|
637
841
|
throw e.wrapStorageException(
|
|
638
842
|
"NitroStorage: Failed to clear biometric storage: ${e.message}",
|
|
639
843
|
)
|
|
844
|
+
} finally {
|
|
845
|
+
inst.invalidateSecureKeysCache()
|
|
640
846
|
}
|
|
641
847
|
}
|
|
642
848
|
}
|