react-native-nitro-storage 0.10.3 → 0.11.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 +85 -14
- package/README.md +148 -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 +48 -33
- package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +3 -3
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +73 -23
- package/android/src/main/java/com/nitrostorage/DiskSqliteStore.kt +22 -4
- 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 +94 -17
- package/cpp/core/SqliteDiskStore.hpp +10 -0
- package/docs/api-reference.md +73 -28
- package/docs/benchmarks.md +41 -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 +149 -57
- 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 +61 -19
- 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 +61 -22
- 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 +61 -12
- 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 +62 -23
- 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 +86 -19
- package/src/indexeddb-backend.ts +20 -42
- package/src/internal.ts +8 -0
- package/src/storage-core.ts +84 -33
- package/src/storage-runtime.ts +3 -1
- package/src/testing.ts +91 -18
- package/src/web-backend-contract.ts +6 -2
- package/testing/package.json +8 -0
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#include "AndroidStorageAdapterCpp.hpp"
|
|
2
2
|
|
|
3
|
+
#include <limits>
|
|
4
|
+
#include <stdexcept>
|
|
5
|
+
|
|
3
6
|
namespace NitroStorage {
|
|
4
7
|
|
|
5
8
|
using namespace facebook::jni;
|
|
@@ -7,10 +10,25 @@ using JavaStringArray = JArrayClass<jstring>;
|
|
|
7
10
|
|
|
8
11
|
namespace {
|
|
9
12
|
|
|
13
|
+
local_ref<JString> toJavaString(const std::string& value) {
|
|
14
|
+
// fbjni's std::string overload uses c_str(), which truncates embedded NUL.
|
|
15
|
+
if (value.find('\0') == std::string::npos) return make_jstring(value);
|
|
16
|
+
if (value.size() > static_cast<size_t>(std::numeric_limits<jsize>::max())) {
|
|
17
|
+
throw std::length_error("Storage string exceeds the Java array limit");
|
|
18
|
+
}
|
|
19
|
+
const auto size = static_cast<jsize>(value.size());
|
|
20
|
+
auto bytes = JArrayByte::newArray(size);
|
|
21
|
+
bytes->setRegion(0, size, reinterpret_cast<const jbyte*>(value.data()));
|
|
22
|
+
static auto constructor = JString::javaClassStatic()->getConstructor<
|
|
23
|
+
jstring(jbyteArray, jstring)>();
|
|
24
|
+
auto charset = make_jstring("UTF-8");
|
|
25
|
+
return JString::javaClassStatic()->newObject(constructor, bytes.get(), charset.get());
|
|
26
|
+
}
|
|
27
|
+
|
|
10
28
|
local_ref<JavaStringArray> toJavaStringArray(const std::vector<std::string>& values) {
|
|
11
29
|
auto javaArray = JavaStringArray::newArray(static_cast<jsize>(values.size()));
|
|
12
30
|
for (size_t i = 0; i < values.size(); ++i) {
|
|
13
|
-
auto javaValue =
|
|
31
|
+
auto javaValue = toJavaString(values[i]);
|
|
14
32
|
javaArray->setElement(static_cast<jsize>(i), javaValue.get());
|
|
15
33
|
}
|
|
16
34
|
return javaArray;
|
|
@@ -51,35 +69,32 @@ std::vector<std::string> fromJavaStringArray(alias_ref<JavaStringArray> values)
|
|
|
51
69
|
|
|
52
70
|
} // namespace
|
|
53
71
|
|
|
54
|
-
AndroidStorageAdapterCpp::AndroidStorageAdapterCpp(
|
|
55
|
-
// Context is validated by AndroidStorageAdapter.getContext() on the Java side.
|
|
56
|
-
// The adapter calls static Java methods directly via fbjni.
|
|
57
|
-
}
|
|
72
|
+
AndroidStorageAdapterCpp::AndroidStorageAdapterCpp() = default;
|
|
58
73
|
|
|
59
74
|
AndroidStorageAdapterCpp::~AndroidStorageAdapterCpp() = default;
|
|
60
75
|
|
|
61
76
|
// --- Disk ---
|
|
62
77
|
|
|
63
78
|
void AndroidStorageAdapterCpp::setDisk(const std::string& key, const std::string& value) {
|
|
64
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(
|
|
65
|
-
method(AndroidStorageAdapterJava::javaClassStatic(), key, value);
|
|
79
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(alias_ref<JString>, alias_ref<JString>)>("setDisk");
|
|
80
|
+
method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key), toJavaString(value));
|
|
66
81
|
}
|
|
67
82
|
|
|
68
83
|
std::optional<std::string> AndroidStorageAdapterCpp::getDisk(const std::string& key) {
|
|
69
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jstring(
|
|
70
|
-
auto result = method(AndroidStorageAdapterJava::javaClassStatic(), key);
|
|
84
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jstring(alias_ref<JString>)>("getDisk");
|
|
85
|
+
auto result = method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
|
|
71
86
|
if (!result) return std::nullopt;
|
|
72
87
|
return result->toStdString();
|
|
73
88
|
}
|
|
74
89
|
|
|
75
90
|
void AndroidStorageAdapterCpp::deleteDisk(const std::string& key) {
|
|
76
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(
|
|
77
|
-
method(AndroidStorageAdapterJava::javaClassStatic(), key);
|
|
91
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(alias_ref<JString>)>("deleteDisk");
|
|
92
|
+
method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
|
|
78
93
|
}
|
|
79
94
|
|
|
80
95
|
bool AndroidStorageAdapterCpp::hasDisk(const std::string& key) {
|
|
81
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jboolean(
|
|
82
|
-
return method(AndroidStorageAdapterJava::javaClassStatic(), key);
|
|
96
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jboolean(alias_ref<JString>)>("hasDisk");
|
|
97
|
+
return method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
|
|
83
98
|
}
|
|
84
99
|
|
|
85
100
|
std::vector<std::string> AndroidStorageAdapterCpp::getAllKeysDisk() {
|
|
@@ -92,9 +107,9 @@ std::vector<std::string> AndroidStorageAdapterCpp::getAllKeysDisk() {
|
|
|
92
107
|
|
|
93
108
|
std::vector<std::string> AndroidStorageAdapterCpp::getKeysByPrefixDisk(const std::string& prefix) {
|
|
94
109
|
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<
|
|
95
|
-
local_ref<JavaStringArray>(
|
|
110
|
+
local_ref<JavaStringArray>(alias_ref<JString>)
|
|
96
111
|
>("getKeysByPrefixDisk");
|
|
97
|
-
auto keys = method(AndroidStorageAdapterJava::javaClassStatic(), prefix);
|
|
112
|
+
auto keys = method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(prefix));
|
|
98
113
|
return fromJavaStringArray(keys);
|
|
99
114
|
}
|
|
100
115
|
|
|
@@ -145,25 +160,25 @@ void AndroidStorageAdapterCpp::clearDisk() {
|
|
|
145
160
|
// --- Secure ---
|
|
146
161
|
|
|
147
162
|
void AndroidStorageAdapterCpp::setSecure(const std::string& key, const std::string& value) {
|
|
148
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(
|
|
149
|
-
method(AndroidStorageAdapterJava::javaClassStatic(), key, value);
|
|
163
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(alias_ref<JString>, alias_ref<JString>)>("setSecure");
|
|
164
|
+
method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key), toJavaString(value));
|
|
150
165
|
}
|
|
151
166
|
|
|
152
167
|
std::optional<std::string> AndroidStorageAdapterCpp::getSecure(const std::string& key) {
|
|
153
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jstring(
|
|
154
|
-
auto result = method(AndroidStorageAdapterJava::javaClassStatic(), key);
|
|
168
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jstring(alias_ref<JString>)>("getSecure");
|
|
169
|
+
auto result = method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
|
|
155
170
|
if (!result) return std::nullopt;
|
|
156
171
|
return result->toStdString();
|
|
157
172
|
}
|
|
158
173
|
|
|
159
174
|
void AndroidStorageAdapterCpp::deleteSecure(const std::string& key) {
|
|
160
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(
|
|
161
|
-
method(AndroidStorageAdapterJava::javaClassStatic(), key);
|
|
175
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(alias_ref<JString>)>("deleteSecure");
|
|
176
|
+
method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
|
|
162
177
|
}
|
|
163
178
|
|
|
164
179
|
bool AndroidStorageAdapterCpp::hasSecure(const std::string& key) {
|
|
165
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jboolean(
|
|
166
|
-
return method(AndroidStorageAdapterJava::javaClassStatic(), key);
|
|
180
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jboolean(alias_ref<JString>)>("hasSecure");
|
|
181
|
+
return method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
|
|
167
182
|
}
|
|
168
183
|
|
|
169
184
|
std::vector<std::string> AndroidStorageAdapterCpp::getAllKeysSecure() {
|
|
@@ -176,9 +191,9 @@ std::vector<std::string> AndroidStorageAdapterCpp::getAllKeysSecure() {
|
|
|
176
191
|
|
|
177
192
|
std::vector<std::string> AndroidStorageAdapterCpp::getKeysByPrefixSecure(const std::string& prefix) {
|
|
178
193
|
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<
|
|
179
|
-
local_ref<JavaStringArray>(
|
|
194
|
+
local_ref<JavaStringArray>(alias_ref<JString>)
|
|
180
195
|
>("getKeysByPrefixSecure");
|
|
181
|
-
auto keys = method(AndroidStorageAdapterJava::javaClassStatic(), prefix);
|
|
196
|
+
auto keys = method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(prefix));
|
|
182
197
|
return fromJavaStringArray(keys);
|
|
183
198
|
}
|
|
184
199
|
|
|
@@ -242,25 +257,25 @@ void AndroidStorageAdapterCpp::setSecureBiometric(const std::string& key, const
|
|
|
242
257
|
}
|
|
243
258
|
|
|
244
259
|
void AndroidStorageAdapterCpp::setSecureBiometricWithLevel(const std::string& key, const std::string& value, int level) {
|
|
245
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(
|
|
246
|
-
method(AndroidStorageAdapterJava::javaClassStatic(), key, value, level);
|
|
260
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(alias_ref<JString>, alias_ref<JString>, jint)>("setSecureBiometricWithLevel");
|
|
261
|
+
method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key), toJavaString(value), level);
|
|
247
262
|
}
|
|
248
263
|
|
|
249
264
|
std::optional<std::string> AndroidStorageAdapterCpp::getSecureBiometric(const std::string& key) {
|
|
250
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jstring(
|
|
251
|
-
auto result = method(AndroidStorageAdapterJava::javaClassStatic(), key);
|
|
265
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jstring(alias_ref<JString>)>("getSecureBiometric");
|
|
266
|
+
auto result = method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
|
|
252
267
|
if (!result) return std::nullopt;
|
|
253
268
|
return result->toStdString();
|
|
254
269
|
}
|
|
255
270
|
|
|
256
271
|
void AndroidStorageAdapterCpp::deleteSecureBiometric(const std::string& key) {
|
|
257
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(
|
|
258
|
-
method(AndroidStorageAdapterJava::javaClassStatic(), key);
|
|
272
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(alias_ref<JString>)>("deleteSecureBiometric");
|
|
273
|
+
method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
|
|
259
274
|
}
|
|
260
275
|
|
|
261
276
|
bool AndroidStorageAdapterCpp::hasSecureBiometric(const std::string& key) {
|
|
262
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jboolean(
|
|
263
|
-
return method(AndroidStorageAdapterJava::javaClassStatic(), key);
|
|
277
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jboolean(alias_ref<JString>)>("hasSecureBiometric");
|
|
278
|
+
return method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
|
|
264
279
|
}
|
|
265
280
|
|
|
266
281
|
void AndroidStorageAdapterCpp::clearSecureBiometric() {
|
|
@@ -9,16 +9,16 @@ namespace NitroStorage {
|
|
|
9
9
|
struct AndroidStorageAdapterJava : facebook::jni::JavaClass<AndroidStorageAdapterJava> {
|
|
10
10
|
static constexpr auto kJavaDescriptor = "Lcom/nitrostorage/AndroidStorageAdapter;";
|
|
11
11
|
|
|
12
|
-
static
|
|
12
|
+
static void ensureInitialized() {
|
|
13
13
|
static auto method = javaClassStatic()->getStaticMethod<facebook::jni::JObject()>("getContext", "()Landroid/content/Context;");
|
|
14
|
-
|
|
14
|
+
method(javaClassStatic());
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
class AndroidStorageAdapterCpp : public NativeStorageAdapter {
|
|
20
20
|
public:
|
|
21
|
-
|
|
21
|
+
AndroidStorageAdapterCpp();
|
|
22
22
|
~AndroidStorageAdapterCpp() override;
|
|
23
23
|
|
|
24
24
|
void setDisk(const std::string& key, const std::string& value) override;
|
|
@@ -65,20 +65,44 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
65
65
|
|
|
66
66
|
private val masterKeyAlias = "${context.packageName}.nitro_storage.master_key"
|
|
67
67
|
|
|
68
|
-
private val masterKey: MasterKey
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
private val masterKey: MasterKey by lazy {
|
|
69
|
+
try {
|
|
70
|
+
createDefaultMasterKey()
|
|
71
|
+
} catch (e: Exception) {
|
|
72
|
+
throw permanentSecureStoreError(
|
|
73
|
+
"NitroStorage: Cannot create the Secure encryption key.",
|
|
74
|
+
e,
|
|
75
|
+
)
|
|
76
|
+
}
|
|
74
77
|
}
|
|
75
78
|
|
|
76
|
-
private val encryptedPreferences: SharedPreferences
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
private val encryptedPreferences: SharedPreferences by lazy {
|
|
80
|
+
try {
|
|
81
|
+
initializeEncryptedPreferences(
|
|
82
|
+
"NitroStorageSecure",
|
|
83
|
+
masterKey,
|
|
84
|
+
masterKeyAlias,
|
|
85
|
+
::createDefaultMasterKey,
|
|
86
|
+
)
|
|
87
|
+
} catch (e: Exception) {
|
|
88
|
+
throw permanentSecureStoreError(
|
|
89
|
+
"NitroStorage: Cannot open Secure storage.",
|
|
90
|
+
e,
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private fun permanentSecureStoreError(message: String, cause: Exception): RuntimeException {
|
|
96
|
+
if (cause.message?.contains("[nitro-error:storage_corruption]") == true &&
|
|
97
|
+
cause is RuntimeException
|
|
98
|
+
) {
|
|
99
|
+
return cause
|
|
100
|
+
}
|
|
101
|
+
return RuntimeException(
|
|
102
|
+
"[nitro-error:storage_corruption] $message ${cause.message ?: ""}".trimEnd(),
|
|
103
|
+
cause,
|
|
104
|
+
)
|
|
105
|
+
}
|
|
82
106
|
|
|
83
107
|
private val biometricMasterKeyAlias = "${context.packageName}.nitro_storage.biometric_key"
|
|
84
108
|
private val biometricOrPasscodeMasterKeyAlias =
|
|
@@ -273,25 +297,55 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
273
297
|
|
|
274
298
|
private fun existingBiometricPreferences(): List<SharedPreferences> {
|
|
275
299
|
val stores = mutableListOf<SharedPreferences>()
|
|
300
|
+
if (
|
|
301
|
+
biometricOrPasscodeInitialized ||
|
|
302
|
+
preferencesFileExists("NitroStorageBiometricOrPasscode")
|
|
303
|
+
) {
|
|
304
|
+
stores.add(biometricOrPasscodePreferences)
|
|
305
|
+
}
|
|
306
|
+
if (
|
|
307
|
+
legacyBiometricInitialized ||
|
|
308
|
+
preferencesFileExists("NitroStorageBiometric")
|
|
309
|
+
) {
|
|
310
|
+
stores.add(legacyBiometricPreferences)
|
|
311
|
+
}
|
|
276
312
|
if (
|
|
277
313
|
biometricOnlyInitialized ||
|
|
278
314
|
preferencesFileExists("NitroStorageBiometricOnly")
|
|
279
315
|
) {
|
|
280
316
|
stores.add(biometricOnlyPreferences)
|
|
281
317
|
}
|
|
318
|
+
return stores
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private fun <T : Any> firstInExistingBiometricPreferences(
|
|
322
|
+
lookup: (SharedPreferences) -> T?,
|
|
323
|
+
): T? {
|
|
282
324
|
if (
|
|
283
325
|
biometricOrPasscodeInitialized ||
|
|
284
326
|
preferencesFileExists("NitroStorageBiometricOrPasscode")
|
|
285
327
|
) {
|
|
286
|
-
|
|
328
|
+
lookup(biometricOrPasscodePreferences)?.let { return it }
|
|
287
329
|
}
|
|
288
330
|
if (
|
|
289
331
|
legacyBiometricInitialized ||
|
|
290
332
|
preferencesFileExists("NitroStorageBiometric")
|
|
291
333
|
) {
|
|
292
|
-
|
|
334
|
+
lookup(legacyBiometricPreferences)?.let { return it }
|
|
293
335
|
}
|
|
294
|
-
|
|
336
|
+
if (
|
|
337
|
+
biometricOnlyInitialized ||
|
|
338
|
+
preferencesFileExists("NitroStorageBiometricOnly")
|
|
339
|
+
) {
|
|
340
|
+
lookup(biometricOnlyPreferences)?.let { return it }
|
|
341
|
+
}
|
|
342
|
+
return null
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private fun biometricPreferencesContain(key: String): Boolean {
|
|
346
|
+
return firstInExistingBiometricPreferences { preferences ->
|
|
347
|
+
if (preferences.contains(key)) true else null
|
|
348
|
+
} ?: false
|
|
295
349
|
}
|
|
296
350
|
|
|
297
351
|
private fun biometricPreferencesForLevel(level: Int): SharedPreferences {
|
|
@@ -663,7 +717,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
663
717
|
if (inst.encryptedPreferences.contains(key)) {
|
|
664
718
|
return true
|
|
665
719
|
}
|
|
666
|
-
return inst.
|
|
720
|
+
return inst.biometricPreferencesContain(key)
|
|
667
721
|
}
|
|
668
722
|
|
|
669
723
|
@JvmStatic
|
|
@@ -776,13 +830,9 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
776
830
|
fun getSecureBiometric(key: String): String? {
|
|
777
831
|
val inst = getInstanceOrThrow()
|
|
778
832
|
return try {
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
if (value != null) {
|
|
782
|
-
return value
|
|
783
|
-
}
|
|
833
|
+
inst.firstInExistingBiometricPreferences { preferences ->
|
|
834
|
+
inst.getSecureSafe(preferences, key)
|
|
784
835
|
}
|
|
785
|
-
null
|
|
786
836
|
} catch (e: Exception) {
|
|
787
837
|
throw e.wrapStorageException(
|
|
788
838
|
"NitroStorage: Failed to read biometric storage: ${e.message}",
|
|
@@ -809,7 +859,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
809
859
|
fun hasSecureBiometric(key: String): Boolean {
|
|
810
860
|
val inst = getInstanceOrThrow()
|
|
811
861
|
return try {
|
|
812
|
-
inst.
|
|
862
|
+
inst.biometricPreferencesContain(key)
|
|
813
863
|
} catch (e: Exception) {
|
|
814
864
|
throw e.wrapStorageException(
|
|
815
865
|
"NitroStorage: Failed to inspect biometric storage: ${e.message}",
|
|
@@ -44,6 +44,7 @@ internal class DiskSqliteStore(
|
|
|
44
44
|
@Synchronized
|
|
45
45
|
fun remove(key: String) {
|
|
46
46
|
db.execSQL("DELETE FROM kv WHERE key = ?", arrayOf(key))
|
|
47
|
+
removeLegacyKeys(listOf(key))
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
@Synchronized
|
|
@@ -86,6 +87,7 @@ internal class DiskSqliteStore(
|
|
|
86
87
|
} finally {
|
|
87
88
|
db.endTransaction()
|
|
88
89
|
}
|
|
90
|
+
removeLegacyKeys(keys.asIterable())
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
@Synchronized
|
|
@@ -103,9 +105,9 @@ internal class DiskSqliteStore(
|
|
|
103
105
|
fun getKeysByPrefix(prefix: String): Array<String> {
|
|
104
106
|
val pattern = buildString {
|
|
105
107
|
for (character in prefix) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
// LIKE stops at NUL; startsWith below checks the complete prefix.
|
|
109
|
+
if (character == '\u0000') break
|
|
110
|
+
if (character == '%' || character == '_' || character == '\\') append('\\')
|
|
109
111
|
append(character)
|
|
110
112
|
}
|
|
111
113
|
append('%')
|
|
@@ -116,7 +118,10 @@ internal class DiskSqliteStore(
|
|
|
116
118
|
).use { cursor ->
|
|
117
119
|
val keys = ArrayList<String>(cursor.count)
|
|
118
120
|
while (cursor.moveToNext()) {
|
|
119
|
-
|
|
121
|
+
val key = cursor.getString(0)
|
|
122
|
+
if (key.startsWith(prefix)) {
|
|
123
|
+
keys.add(key)
|
|
124
|
+
}
|
|
120
125
|
}
|
|
121
126
|
return keys.toTypedArray()
|
|
122
127
|
}
|
|
@@ -132,6 +137,9 @@ internal class DiskSqliteStore(
|
|
|
132
137
|
@Synchronized
|
|
133
138
|
fun clear() {
|
|
134
139
|
db.execSQL("DELETE FROM kv")
|
|
140
|
+
if (legacyPreferences.all.isNotEmpty()) {
|
|
141
|
+
legacyPreferences.edit().clear().apply()
|
|
142
|
+
}
|
|
135
143
|
}
|
|
136
144
|
|
|
137
145
|
private fun migrateLegacyPreferences() {
|
|
@@ -164,6 +172,16 @@ internal class DiskSqliteStore(
|
|
|
164
172
|
}
|
|
165
173
|
}
|
|
166
174
|
|
|
175
|
+
private fun removeLegacyKeys(keys: Iterable<String>) {
|
|
176
|
+
val present = keys.filter { legacyPreferences.contains(it) }
|
|
177
|
+
if (present.isEmpty()) {
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
val editor = legacyPreferences.edit()
|
|
181
|
+
present.forEach { editor.remove(it) }
|
|
182
|
+
editor.apply()
|
|
183
|
+
}
|
|
184
|
+
|
|
167
185
|
private companion object {
|
|
168
186
|
const val DATABASE_NAME = "nitro-storage-disk.sqlite"
|
|
169
187
|
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;
|