@qntx/wallet-native 0.0.0-stage → 0.4.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/README.md +123 -2
- package/WalletNative.podspec +74 -0
- package/android/CMakeLists.txt +99 -0
- package/android/build.gradle +177 -0
- package/android/consumer-rules.pro +4 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +29 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/AppLifecycle.kt +35 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/KernelPbkdf2.java +59 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/Keystore.kt +280 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/Passkey.kt +147 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/VaultDirectory.kt +28 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/WalletNativePackage.java +30 -0
- package/cpp/host/HybridKernelHost.cpp +162 -0
- package/cpp/host/HybridKernelHost.hpp +49 -0
- package/cpp/host/KernelFile.cpp +120 -0
- package/cpp/host/KernelFile.hpp +24 -0
- package/cpp/host/Keystore.hpp +70 -0
- package/cpp/host/Lifecycle.cpp +77 -0
- package/cpp/host/Lifecycle.hpp +71 -0
- package/cpp/host/Passkey.hpp +39 -0
- package/cpp/host/VaultDirectory.hpp +16 -0
- package/cpp/platform/KeystoreAndroid.cpp +175 -0
- package/cpp/platform/KeystoreIos.mm +300 -0
- package/cpp/platform/LifecycleAndroid.cpp +55 -0
- package/cpp/platform/LifecycleIos.mm +36 -0
- package/cpp/platform/PasskeyAndroid.cpp +118 -0
- package/cpp/platform/PasskeyIos.mm +20 -0
- package/cpp/platform/VaultDirectory.mm +58 -0
- package/cpp/platform/VaultDirectoryAndroid.cpp +35 -0
- package/cpp/runtime/KernelBackends.cpp +297 -0
- package/cpp/runtime/KernelBackends.hpp +20 -0
- package/cpp/runtime/KernelBuffer.cpp +12 -0
- package/cpp/runtime/KernelBuffer.hpp +96 -0
- package/cpp/runtime/KernelGlobals.cpp +535 -0
- package/cpp/runtime/KernelGlobals.hpp +107 -0
- package/cpp/runtime/KernelJs.cpp +155 -0
- package/cpp/runtime/KernelJs.hpp +81 -0
- package/cpp/runtime/KernelKeystore.cpp +224 -0
- package/cpp/runtime/KernelKeystore.hpp +28 -0
- package/cpp/runtime/KernelPasskey.cpp +137 -0
- package/cpp/runtime/KernelPasskey.hpp +23 -0
- package/cpp/runtime/KernelRuntime.cpp +496 -0
- package/cpp/runtime/KernelRuntime.hpp +94 -0
- package/cpp/runtime/platform/KernelPbkdf2.cpp +198 -0
- package/cpp/runtime/platform/KernelPbkdf2.hpp +38 -0
- package/dist/index.d.mts +44 -0
- package/dist/index.mjs +170 -0
- package/dist/index.mjs.map +1 -0
- package/kernel/generated/kernel-entry.js +42967 -0
- package/kernel/generated/kernel.manifest.json +80 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/WalletNative+autolinking.cmake +81 -0
- package/nitrogen/generated/android/WalletNative+autolinking.gradle +27 -0
- package/nitrogen/generated/android/WalletNativeOnLoad.cpp +49 -0
- package/nitrogen/generated/android/WalletNativeOnLoad.hpp +34 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/qntxwallet/WalletNativeOnLoad.kt +35 -0
- package/nitrogen/generated/ios/WalletNative+autolinking.rb +62 -0
- package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Bridge.cpp +17 -0
- package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Bridge.hpp +27 -0
- package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Umbrella.hpp +38 -0
- package/nitrogen/generated/ios/WalletNativeAutolinking.mm +35 -0
- package/nitrogen/generated/ios/WalletNativeAutolinking.swift +16 -0
- package/nitrogen/generated/shared/c++/HybridKernelHostSpec.cpp +25 -0
- package/nitrogen/generated/shared/c++/HybridKernelHostSpec.hpp +69 -0
- package/nitrogen/generated/shared/c++/KernelExitReason.hpp +84 -0
- package/package.json +61 -4
- package/react-native.config.js +14 -0
- package/scripts/compile-kernel.mjs +118 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
#include "KernelGlobals.hpp"
|
|
2
|
+
|
|
3
|
+
#include "../host/KernelFile.hpp"
|
|
4
|
+
|
|
5
|
+
#include "KernelBackends.hpp"
|
|
6
|
+
#include "KernelKeystore.hpp"
|
|
7
|
+
#include "KernelPasskey.hpp"
|
|
8
|
+
#include "KernelBuffer.hpp"
|
|
9
|
+
#include "KernelJs.hpp"
|
|
10
|
+
#include "platform/KernelPbkdf2.hpp"
|
|
11
|
+
|
|
12
|
+
#include <atomic>
|
|
13
|
+
#include <chrono>
|
|
14
|
+
#include <cmath>
|
|
15
|
+
#include <condition_variable>
|
|
16
|
+
#include <cstdlib>
|
|
17
|
+
#include <cstring>
|
|
18
|
+
#include <deque>
|
|
19
|
+
#include <mutex>
|
|
20
|
+
#include <stdexcept>
|
|
21
|
+
#include <string>
|
|
22
|
+
#include <thread>
|
|
23
|
+
|
|
24
|
+
#if defined(__APPLE__) || defined(__ANDROID__)
|
|
25
|
+
#include <pthread.h>
|
|
26
|
+
#include <unistd.h>
|
|
27
|
+
#elif defined(__linux__)
|
|
28
|
+
#include <cerrno>
|
|
29
|
+
#include <sys/random.h>
|
|
30
|
+
#include <unistd.h>
|
|
31
|
+
#endif
|
|
32
|
+
|
|
33
|
+
#if defined(__ANDROID__)
|
|
34
|
+
#include <fbjni/fbjni.h>
|
|
35
|
+
#endif
|
|
36
|
+
|
|
37
|
+
namespace qntx {
|
|
38
|
+
|
|
39
|
+
namespace {
|
|
40
|
+
|
|
41
|
+
constexpr size_t kRandomLimit = 65536;
|
|
42
|
+
constexpr double kTimerDelayMax = 2147483647.0;
|
|
43
|
+
constexpr double kPbkdf2IterationsMax = 10000000.0;
|
|
44
|
+
constexpr size_t kPbkdf2LengthMax = 1024;
|
|
45
|
+
constexpr size_t kKdfQueueLimit = 8;
|
|
46
|
+
|
|
47
|
+
void fillRandom(uint8_t* out, size_t size) {
|
|
48
|
+
#if defined(__APPLE__) || defined(__ANDROID__)
|
|
49
|
+
arc4random_buf(out, size);
|
|
50
|
+
#elif defined(__linux__)
|
|
51
|
+
size_t filled = 0;
|
|
52
|
+
while (filled < size) {
|
|
53
|
+
const ssize_t got = getrandom(out + filled, size - filled, 0);
|
|
54
|
+
if (got < 0) {
|
|
55
|
+
if (errno == EINTR) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
std::abort();
|
|
59
|
+
}
|
|
60
|
+
filled += static_cast<size_t>(got);
|
|
61
|
+
}
|
|
62
|
+
#else
|
|
63
|
+
#error "kernel runtime: no platform random source"
|
|
64
|
+
#endif
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// One process-wide worker runs every PBKDF2 job, so concurrent kernels cannot
|
|
68
|
+
// spawn unbounded derivation threads. The queue is bounded; the worker thread
|
|
69
|
+
// is intentionally leaked rather than joined (it parks on the condition
|
|
70
|
+
// variable and holds no runtime state).
|
|
71
|
+
struct KdfJob {
|
|
72
|
+
KernelPbkdf2Hash hash;
|
|
73
|
+
SecretBytes password;
|
|
74
|
+
SecretBytes salt;
|
|
75
|
+
uint32_t iterations;
|
|
76
|
+
size_t length;
|
|
77
|
+
std::shared_ptr<KernelPendingCall> call;
|
|
78
|
+
std::shared_ptr<KernelGlobalsState> state;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
struct KdfQueue {
|
|
82
|
+
std::mutex mutex;
|
|
83
|
+
std::condition_variable cv;
|
|
84
|
+
std::deque<KdfJob> jobs;
|
|
85
|
+
bool workerStarted = false;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
KdfQueue& kdfQueue() {
|
|
89
|
+
static auto* queue = new KdfQueue();
|
|
90
|
+
return *queue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
void kdfWorkerRun() {
|
|
94
|
+
auto& queue = kdfQueue();
|
|
95
|
+
for (;;) {
|
|
96
|
+
KdfJob job;
|
|
97
|
+
{
|
|
98
|
+
std::unique_lock<std::mutex> lock(queue.mutex);
|
|
99
|
+
queue.cv.wait(lock, [&] { return !queue.jobs.empty(); });
|
|
100
|
+
job = std::move(queue.jobs.front());
|
|
101
|
+
queue.jobs.pop_front();
|
|
102
|
+
}
|
|
103
|
+
if (!job.state->active->load(std::memory_order_acquire)) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
SecretBytes derived;
|
|
107
|
+
std::string error;
|
|
108
|
+
try {
|
|
109
|
+
derived = kernelPbkdf2(
|
|
110
|
+
job.hash, job.password, job.salt, job.iterations, job.length);
|
|
111
|
+
} catch (const std::exception& e) {
|
|
112
|
+
error = e.what();
|
|
113
|
+
} catch (...) {
|
|
114
|
+
error = "unknown pbkdf2 error";
|
|
115
|
+
}
|
|
116
|
+
if (!job.state->active->load(std::memory_order_acquire)) {
|
|
117
|
+
// Runtime torn down mid-derivation: drop the job; the parked call's
|
|
118
|
+
// `done` was already cleared by teardown, and SecretBytes wipe here.
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
auto state = job.state;
|
|
122
|
+
auto call = job.call;
|
|
123
|
+
// std::function needs a copyable closure: share the result; whichever
|
|
124
|
+
// side drops it last wipes it.
|
|
125
|
+
auto result = std::make_shared<SecretBytes>(std::move(derived));
|
|
126
|
+
state->enqueueTask(
|
|
127
|
+
[state, call, result, error = std::move(error)](
|
|
128
|
+
jsi::Runtime& rt) mutable {
|
|
129
|
+
auto done = std::move(call->done);
|
|
130
|
+
state->pending.erase(call);
|
|
131
|
+
if (!done.has_value()) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (!error.empty()) {
|
|
135
|
+
done->call(
|
|
136
|
+
rt,
|
|
137
|
+
jsi::Value::undefined(),
|
|
138
|
+
jsi::Value(rt, jsi::String::createFromUtf8(rt, error)));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
auto buffer =
|
|
142
|
+
arrayBufferFromBytes(rt, result->data(), result->size());
|
|
143
|
+
auto array = makeUint8Array(rt, *state->intrinsics, std::move(buffer));
|
|
144
|
+
done->call(rt, jsi::Value(std::move(array)), jsi::Value::undefined());
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
void kdfWorkerMain() {
|
|
150
|
+
#if defined(__APPLE__)
|
|
151
|
+
pthread_setname_np("qntx-kdf");
|
|
152
|
+
#elif defined(__ANDROID__)
|
|
153
|
+
pthread_setname_np(pthread_self(), "qntx-kdf");
|
|
154
|
+
#endif
|
|
155
|
+
auto body = [] { kdfWorkerRun(); };
|
|
156
|
+
#if defined(__ANDROID__)
|
|
157
|
+
// The worker calls into javax.crypto over JNI; attach it to the JVM once for
|
|
158
|
+
// its lifetime (and let the app class loader resolve KernelPbkdf2).
|
|
159
|
+
facebook::jni::ThreadScope::WithClassLoader(std::move(body));
|
|
160
|
+
#else
|
|
161
|
+
body();
|
|
162
|
+
#endif
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
bool kdfSubmit(KdfJob job) {
|
|
166
|
+
auto& queue = kdfQueue();
|
|
167
|
+
std::lock_guard<std::mutex> lock(queue.mutex);
|
|
168
|
+
if (queue.jobs.size() >= kKdfQueueLimit) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
queue.jobs.push_back(std::move(job));
|
|
172
|
+
if (!queue.workerStarted) {
|
|
173
|
+
queue.workerStarted = true;
|
|
174
|
+
std::thread(kdfWorkerMain).detach();
|
|
175
|
+
}
|
|
176
|
+
queue.cv.notify_one();
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
void installPbkdf2(
|
|
181
|
+
jsi::Runtime& rt,
|
|
182
|
+
jsi::Object& qntx,
|
|
183
|
+
const std::shared_ptr<KernelGlobalsState>& state) {
|
|
184
|
+
qntx.setProperty(
|
|
185
|
+
rt,
|
|
186
|
+
"pbkdf2",
|
|
187
|
+
hostFn(rt, "pbkdf2", 6, [state](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
|
|
188
|
+
if (count < 6) {
|
|
189
|
+
throw jsi::JSError(
|
|
190
|
+
rt, "__qntx.pbkdf2 expects (hash, password, salt, iterations, length, done)");
|
|
191
|
+
}
|
|
192
|
+
const auto& intrinsics = *state->intrinsics;
|
|
193
|
+
if (!args[0].isString()) {
|
|
194
|
+
throw jsi::JSError(
|
|
195
|
+
rt, "__qntx.pbkdf2 expects (hash, password, salt, iterations, length, done)");
|
|
196
|
+
}
|
|
197
|
+
const auto hashName = args[0].asString(rt).utf8(rt);
|
|
198
|
+
KernelPbkdf2Hash hash;
|
|
199
|
+
if (hashName == "sha256") {
|
|
200
|
+
hash = KernelPbkdf2Hash::sha256;
|
|
201
|
+
} else if (hashName == "sha512") {
|
|
202
|
+
hash = KernelPbkdf2Hash::sha512;
|
|
203
|
+
} else {
|
|
204
|
+
throw jsi::JSError(rt, "__qntx.pbkdf2 hash must be sha256 or sha512");
|
|
205
|
+
}
|
|
206
|
+
auto passwordView =
|
|
207
|
+
byteViewFromValue(rt, args[1], "__qntx.pbkdf2 password", intrinsics);
|
|
208
|
+
SecretBytes password(passwordView.data, passwordView.size);
|
|
209
|
+
auto saltView =
|
|
210
|
+
byteViewFromValue(rt, args[2], "__qntx.pbkdf2 salt", intrinsics);
|
|
211
|
+
SecretBytes salt(saltView.data, saltView.size);
|
|
212
|
+
if (!args[5].isObject() || !args[5].asObject(rt).isFunction(rt)) {
|
|
213
|
+
throw jsi::JSError(
|
|
214
|
+
rt, "__qntx.pbkdf2 expects (hash, password, salt, iterations, length, done)");
|
|
215
|
+
}
|
|
216
|
+
const auto iterations = static_cast<uint32_t>(
|
|
217
|
+
integerInRange(rt, args[3], "__qntx.pbkdf2 iterations", 1, kPbkdf2IterationsMax));
|
|
218
|
+
const auto length = static_cast<size_t>(
|
|
219
|
+
integerInRange(rt, args[4], "__qntx.pbkdf2 length", 1, kPbkdf2LengthMax));
|
|
220
|
+
auto call = std::make_shared<KernelPendingCall>();
|
|
221
|
+
call->done = args[5].asObject(rt).asFunction(rt);
|
|
222
|
+
state->pending.insert(call);
|
|
223
|
+
|
|
224
|
+
KdfJob job{
|
|
225
|
+
hash,
|
|
226
|
+
std::move(password),
|
|
227
|
+
std::move(salt),
|
|
228
|
+
iterations,
|
|
229
|
+
length,
|
|
230
|
+
call,
|
|
231
|
+
state};
|
|
232
|
+
if (!kdfSubmit(std::move(job))) {
|
|
233
|
+
state->enqueueTask([state, call](jsi::Runtime& rt) mutable {
|
|
234
|
+
auto done = std::move(call->done);
|
|
235
|
+
state->pending.erase(call);
|
|
236
|
+
if (done.has_value()) {
|
|
237
|
+
done->call(
|
|
238
|
+
rt,
|
|
239
|
+
jsi::Value::undefined(),
|
|
240
|
+
jsi::Value(rt, jsi::String::createFromAscii(rt, "kdf queue full")));
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return jsi::Value::undefined();
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Runs once before the kernel script: locks down the native TextDecoder,
|
|
249
|
+
// installs the setTimeout/clearTimeout globals Hermes' bundled rejection
|
|
250
|
+
// tracker defers through, and wires that tracker to the private reportUnhandled
|
|
251
|
+
// native. The expression evaluates to the function object; C++ invokes it with
|
|
252
|
+
// `natives`.
|
|
253
|
+
const char kKernelPrelude[] = R"JS((function (natives) {
|
|
254
|
+
var defineHidden = function (name, value) {
|
|
255
|
+
Object.defineProperty(globalThis, name, {
|
|
256
|
+
value: value,
|
|
257
|
+
writable: false,
|
|
258
|
+
configurable: false,
|
|
259
|
+
enumerable: false,
|
|
260
|
+
});
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// Hermes 260318099+ (RN 0.88) ships a native TextDecoder — verified against
|
|
264
|
+
// the host vectors: WHATWG fatal/replacement semantics, BOM strip/ignoreBOM,
|
|
265
|
+
// streaming, RangeError on unknown labels. Adopt it instead of a shell:
|
|
266
|
+
// freeze and rebind it hidden so a script cannot swap or tamper with it. The
|
|
267
|
+
// prelude runs before any script, so Object.freeze is still pristine here.
|
|
268
|
+
if (typeof TextDecoder !== "function") {
|
|
269
|
+
throw new Error("kernel prelude: TextDecoder missing");
|
|
270
|
+
}
|
|
271
|
+
Object.freeze(TextDecoder.prototype);
|
|
272
|
+
Object.freeze(TextDecoder);
|
|
273
|
+
defineHidden("TextDecoder", TextDecoder);
|
|
274
|
+
|
|
275
|
+
// Hermes' bundled rejection tracker defers onUnhandled through the literal
|
|
276
|
+
// globals setTimeout/clearTimeout resolved at call time — Promise.then and
|
|
277
|
+
// async/await use the engine queue instead and need nothing. These are the
|
|
278
|
+
// only two timer globals the runtime installs (measured: without them
|
|
279
|
+
// onUnhandled never fires).
|
|
280
|
+
var timerSeq = 0;
|
|
281
|
+
var timerCancels = {};
|
|
282
|
+
defineHidden("setTimeout", function (fn, ms) {
|
|
283
|
+
var id = ++timerSeq;
|
|
284
|
+
var cancel = natives.setTimer(ms === undefined ? 0 : ms, function () {
|
|
285
|
+
delete timerCancels[id];
|
|
286
|
+
fn();
|
|
287
|
+
});
|
|
288
|
+
timerCancels[id] = cancel;
|
|
289
|
+
return id;
|
|
290
|
+
});
|
|
291
|
+
defineHidden("clearTimeout", function (id) {
|
|
292
|
+
var cancel = timerCancels[id];
|
|
293
|
+
if (cancel !== undefined) {
|
|
294
|
+
delete timerCancels[id];
|
|
295
|
+
cancel();
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
if (
|
|
300
|
+
typeof HermesInternal === "object" &&
|
|
301
|
+
HermesInternal !== null &&
|
|
302
|
+
typeof HermesInternal.enablePromiseRejectionTracker === "function"
|
|
303
|
+
) {
|
|
304
|
+
HermesInternal.enablePromiseRejectionTracker({
|
|
305
|
+
allRejections: true,
|
|
306
|
+
onHandled: function () {},
|
|
307
|
+
onUnhandled: function (id, reason) {
|
|
308
|
+
natives.reportUnhandled(
|
|
309
|
+
id,
|
|
310
|
+
reason == null
|
|
311
|
+
? "unknown"
|
|
312
|
+
: String(reason && reason.stack ? reason.stack : reason));
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}))JS";
|
|
317
|
+
|
|
318
|
+
} // namespace
|
|
319
|
+
|
|
320
|
+
void installKernelGlobals(
|
|
321
|
+
jsi::Runtime& rt,
|
|
322
|
+
const std::shared_ptr<KernelGlobalsState>& state) {
|
|
323
|
+
auto intrinsics = captureIntrinsics(rt);
|
|
324
|
+
state->intrinsics = intrinsics;
|
|
325
|
+
|
|
326
|
+
auto global = rt.global();
|
|
327
|
+
auto qntx = jsi::Object(rt);
|
|
328
|
+
|
|
329
|
+
qntx.setProperty(
|
|
330
|
+
rt,
|
|
331
|
+
"random",
|
|
332
|
+
hostFn(rt, "random", 1, [state](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
|
|
333
|
+
if (count < 1) {
|
|
334
|
+
throw jsi::JSError(rt, "__qntx.random expects a byte length");
|
|
335
|
+
}
|
|
336
|
+
const auto n = static_cast<size_t>(
|
|
337
|
+
integerInRange(rt, args[0], "__qntx.random length", 0, kRandomLimit));
|
|
338
|
+
auto buffer = arrayBufferFromBytes(rt, nullptr, n);
|
|
339
|
+
fillRandom(buffer.data(rt), buffer.size(rt));
|
|
340
|
+
return jsi::Value(rt, makeUint8Array(rt, *state->intrinsics, std::move(buffer)));
|
|
341
|
+
}));
|
|
342
|
+
|
|
343
|
+
qntx.setProperty(
|
|
344
|
+
rt,
|
|
345
|
+
"now",
|
|
346
|
+
hostFn(rt, "now", 0, [](jsi::Runtime&, const jsi::Value&, const jsi::Value*, size_t) -> jsi::Value {
|
|
347
|
+
// Whole milliseconds like Date.now(): vault headers require integer
|
|
348
|
+
// createdAt timestamps.
|
|
349
|
+
return jsi::Value(static_cast<double>(
|
|
350
|
+
std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
351
|
+
std::chrono::system_clock::now().time_since_epoch())
|
|
352
|
+
.count()));
|
|
353
|
+
}));
|
|
354
|
+
|
|
355
|
+
qntx.setProperty(
|
|
356
|
+
rt,
|
|
357
|
+
"elapsed",
|
|
358
|
+
hostFn(rt, "elapsed", 0, [](jsi::Runtime&, const jsi::Value&, const jsi::Value*, size_t) -> jsi::Value {
|
|
359
|
+
return jsi::Value(std::chrono::duration<double, std::milli>(
|
|
360
|
+
std::chrono::steady_clock::now().time_since_epoch())
|
|
361
|
+
.count());
|
|
362
|
+
}));
|
|
363
|
+
|
|
364
|
+
const auto makeSetTimer = [&state](jsi::Runtime& rt) -> jsi::Function {
|
|
365
|
+
return hostFn(rt, "setTimer", 2, [state](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
|
|
366
|
+
if (count < 2 || !args[1].isObject() || !args[1].asObject(rt).isFunction(rt)) {
|
|
367
|
+
throw jsi::JSError(rt, "__qntx.setTimer expects (delayMs, fn)");
|
|
368
|
+
}
|
|
369
|
+
const double delayMs = finiteNumber(rt, args[0], "__qntx.setTimer delay");
|
|
370
|
+
if (delayMs < 0 || delayMs > kTimerDelayMax) {
|
|
371
|
+
throw jsi::JSError(rt, "__qntx.setTimer delay out of range");
|
|
372
|
+
}
|
|
373
|
+
const auto deadline = std::chrono::steady_clock::now() +
|
|
374
|
+
std::chrono::milliseconds(static_cast<int64_t>(std::ceil(delayMs)));
|
|
375
|
+
auto cancel = state->scheduleTimer(deadline, args[1].asObject(rt).asFunction(rt));
|
|
376
|
+
return jsi::Value(rt, hostFn(rt, "cancel", 0, [cancel = std::move(cancel)](jsi::Runtime&, const jsi::Value&, const jsi::Value*, size_t) mutable -> jsi::Value {
|
|
377
|
+
cancel();
|
|
378
|
+
return jsi::Value::undefined();
|
|
379
|
+
}));
|
|
380
|
+
});
|
|
381
|
+
};
|
|
382
|
+
qntx.setProperty(rt, "setTimer", makeSetTimer(rt));
|
|
383
|
+
|
|
384
|
+
installPbkdf2(rt, qntx, state);
|
|
385
|
+
|
|
386
|
+
// __qntx.frames — the W2b injection-table shape over the same plumbing:
|
|
387
|
+
// post, onMessage, and onIntegrityViolation (the host fires that handler
|
|
388
|
+
// when it refuses a frame on integrity grounds).
|
|
389
|
+
auto frames = jsi::Object(rt);
|
|
390
|
+
frames.setProperty(
|
|
391
|
+
rt,
|
|
392
|
+
"post",
|
|
393
|
+
hostFn(rt, "frames.post", 1, [state](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
|
|
394
|
+
if (count < 1 || !args[0].isObject() || !args[0].asObject(rt).isArrayBuffer(rt)) {
|
|
395
|
+
throw jsi::JSError(rt, "__qntx.frames.post expects an ArrayBuffer");
|
|
396
|
+
}
|
|
397
|
+
auto buffer = args[0].asObject(rt).getArrayBuffer(rt);
|
|
398
|
+
const auto* data = buffer.data(rt);
|
|
399
|
+
state->postToHost(std::vector<uint8_t>(data, data + buffer.size(rt)));
|
|
400
|
+
return jsi::Value::undefined();
|
|
401
|
+
}));
|
|
402
|
+
frames.setProperty(
|
|
403
|
+
rt,
|
|
404
|
+
"onMessage",
|
|
405
|
+
hostFn(rt, "frames.onMessage", 1, [state](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
|
|
406
|
+
if (count < 1 || !args[0].isObject() || !args[0].asObject(rt).isFunction(rt)) {
|
|
407
|
+
throw jsi::JSError(rt, "__qntx.frames.onMessage expects a function");
|
|
408
|
+
}
|
|
409
|
+
state->messageHandler = args[0].asObject(rt).asFunction(rt);
|
|
410
|
+
return jsi::Value::undefined();
|
|
411
|
+
}));
|
|
412
|
+
frames.setProperty(
|
|
413
|
+
rt,
|
|
414
|
+
"onIntegrityViolation",
|
|
415
|
+
hostFn(rt, "frames.onIntegrityViolation", 1, [state](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
|
|
416
|
+
if (count < 1 || !args[0].isObject() || !args[0].asObject(rt).isFunction(rt)) {
|
|
417
|
+
throw jsi::JSError(rt, "__qntx.frames.onIntegrityViolation expects a function");
|
|
418
|
+
}
|
|
419
|
+
state->integrityHandler = args[0].asObject(rt).asFunction(rt);
|
|
420
|
+
return jsi::Value::undefined();
|
|
421
|
+
}));
|
|
422
|
+
intrinsics->freeze.call(rt, jsi::Value(rt, frames));
|
|
423
|
+
qntx.setProperty(rt, "frames", jsi::Value(std::move(frames)));
|
|
424
|
+
|
|
425
|
+
// __qntx.file — one opaque file image: read() -> Uint8Array | undefined,
|
|
426
|
+
// write(bytes) atomically replaces. The C++ primitive owns durability
|
|
427
|
+
// (temp + fsync + rename + dir fsync); failures throw so the file store
|
|
428
|
+
// rejects instead of silently degrading.
|
|
429
|
+
auto file = jsi::Object(rt);
|
|
430
|
+
file.setProperty(
|
|
431
|
+
rt,
|
|
432
|
+
"read",
|
|
433
|
+
hostFn(rt, "file.read", 0, [state](jsi::Runtime& rt, const jsi::Value&, const jsi::Value*, size_t) -> jsi::Value {
|
|
434
|
+
const auto bytes = kernelFileRead(state->vaultDirectory);
|
|
435
|
+
if (!bytes.has_value()) {
|
|
436
|
+
return jsi::Value::undefined();
|
|
437
|
+
}
|
|
438
|
+
auto buffer = arrayBufferFromBytes(rt, bytes->data(), bytes->size());
|
|
439
|
+
return jsi::Value(rt, makeUint8Array(rt, *state->intrinsics, std::move(buffer)));
|
|
440
|
+
}));
|
|
441
|
+
file.setProperty(
|
|
442
|
+
rt,
|
|
443
|
+
"write",
|
|
444
|
+
hostFn(rt, "file.write", 1, [state](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
|
|
445
|
+
if (count < 1) {
|
|
446
|
+
throw jsi::JSError(rt, "__qntx.file.write expects an ArrayBuffer or TypedArray");
|
|
447
|
+
}
|
|
448
|
+
const auto view =
|
|
449
|
+
byteViewFromValue(rt, args[0], "__qntx.file.write", *state->intrinsics);
|
|
450
|
+
kernelFileWrite(
|
|
451
|
+
state->vaultDirectory, std::vector<uint8_t>(view.data, view.data + view.size));
|
|
452
|
+
return jsi::Value::undefined();
|
|
453
|
+
}));
|
|
454
|
+
intrinsics->freeze.call(rt, jsi::Value(rt, file));
|
|
455
|
+
qntx.setProperty(rt, "file", jsi::Value(std::move(file)));
|
|
456
|
+
|
|
457
|
+
installKernelBackends(rt, qntx, *intrinsics);
|
|
458
|
+
installKernelKeystore(rt, qntx, state);
|
|
459
|
+
installKernelPasskey(rt, qntx, state);
|
|
460
|
+
|
|
461
|
+
intrinsics->freeze.call(rt, jsi::Value(rt, qntx));
|
|
462
|
+
defineHiddenGlobal(rt, *intrinsics, "__qntx", jsi::Value(std::move(qntx)));
|
|
463
|
+
|
|
464
|
+
auto cryptoObject = jsi::Object(rt);
|
|
465
|
+
cryptoObject.setProperty(
|
|
466
|
+
rt,
|
|
467
|
+
"getRandomValues",
|
|
468
|
+
hostFn(rt, "getRandomValues", 1, [state](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
|
|
469
|
+
if (count < 1 || !args[0].isObject()) {
|
|
470
|
+
throw jsi::JSError(rt, "crypto.getRandomValues expects a TypedArray");
|
|
471
|
+
}
|
|
472
|
+
const auto& intrinsics = *state->intrinsics;
|
|
473
|
+
auto view = args[0].asObject(rt);
|
|
474
|
+
const jsi::Value tag = intrinsics.taTagGet.callWithThis(rt, view);
|
|
475
|
+
static const char* const kIntegerArrays[] = {
|
|
476
|
+
"Int8Array",
|
|
477
|
+
"Uint8Array",
|
|
478
|
+
"Uint8ClampedArray",
|
|
479
|
+
"Int16Array",
|
|
480
|
+
"Uint16Array",
|
|
481
|
+
"Int32Array",
|
|
482
|
+
"Uint32Array",
|
|
483
|
+
"BigInt64Array",
|
|
484
|
+
"BigUint64Array",
|
|
485
|
+
};
|
|
486
|
+
const std::string tagName =
|
|
487
|
+
tag.isString() ? tag.asString(rt).utf8(rt) : std::string();
|
|
488
|
+
bool integerTyped = false;
|
|
489
|
+
for (const char* allowed : kIntegerArrays) {
|
|
490
|
+
if (tagName == allowed) {
|
|
491
|
+
integerTyped = true;
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (!integerTyped) {
|
|
496
|
+
throw jsi::JSError(
|
|
497
|
+
rt, "crypto.getRandomValues accepts only integer TypedArrays");
|
|
498
|
+
}
|
|
499
|
+
auto bytes =
|
|
500
|
+
byteViewFromValue(rt, args[0], "crypto.getRandomValues", intrinsics);
|
|
501
|
+
if (bytes.size > kRandomLimit) {
|
|
502
|
+
throw jsi::JSError(
|
|
503
|
+
rt, "QuotaExceededError: crypto.getRandomValues view exceeds 65536 bytes");
|
|
504
|
+
}
|
|
505
|
+
fillRandom(bytes.data, bytes.size);
|
|
506
|
+
return jsi::Value(rt, args[0]);
|
|
507
|
+
}));
|
|
508
|
+
intrinsics->freeze.call(rt, jsi::Value(rt, cryptoObject));
|
|
509
|
+
defineHiddenGlobal(
|
|
510
|
+
rt, *intrinsics, "crypto", jsi::Value(std::move(cryptoObject)));
|
|
511
|
+
|
|
512
|
+
auto natives = jsi::Object(rt);
|
|
513
|
+
natives.setProperty(rt, "setTimer", makeSetTimer(rt));
|
|
514
|
+
natives.setProperty(
|
|
515
|
+
rt,
|
|
516
|
+
"reportUnhandled",
|
|
517
|
+
hostFn(rt, "reportUnhandled", 2, [state](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
|
|
518
|
+
std::string reason = "unhandled promise rejection";
|
|
519
|
+
if (count > 0 && args[0].isNumber()) {
|
|
520
|
+
reason += " id=" +
|
|
521
|
+
std::to_string(static_cast<long long>(args[0].getNumber()));
|
|
522
|
+
}
|
|
523
|
+
if (count > 1 && args[1].isString()) {
|
|
524
|
+
reason += ": " + args[1].asString(rt).utf8(rt);
|
|
525
|
+
}
|
|
526
|
+
state->reportFatal(std::move(reason));
|
|
527
|
+
return jsi::Value::undefined();
|
|
528
|
+
}));
|
|
529
|
+
|
|
530
|
+
auto prelude = rt.evaluateJavaScript(
|
|
531
|
+
std::make_shared<jsi::StringBuffer>(kKernelPrelude), "<kernel-prelude>");
|
|
532
|
+
prelude.asObject(rt).asFunction(rt).call(rt, jsi::Value(std::move(natives)));
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
} // namespace qntx
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <jsi/jsi.h>
|
|
4
|
+
|
|
5
|
+
#include <atomic>
|
|
6
|
+
#include <chrono>
|
|
7
|
+
#include <cstdint>
|
|
8
|
+
#include <functional>
|
|
9
|
+
#include <memory>
|
|
10
|
+
#include <optional>
|
|
11
|
+
#include <string>
|
|
12
|
+
#include <unordered_set>
|
|
13
|
+
#include <vector>
|
|
14
|
+
|
|
15
|
+
namespace qntx {
|
|
16
|
+
|
|
17
|
+
// A JSI callback parked while an async platform call (e.g. pbkdf2) runs on a
|
|
18
|
+
// background thread. `done` is only read, moved or cleared on the kernel
|
|
19
|
+
// thread — background threads never touch it.
|
|
20
|
+
struct KernelPendingCall {
|
|
21
|
+
std::optional<facebook::jsi::Function> done;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Defined in KernelJs.hpp: captured JS intrinsics (defineProperty,
|
|
25
|
+
// freeze, %TypedArray%.prototype getters, the Uint8Array constructor, …) that
|
|
26
|
+
// host functions call instead of looking properties up on possibly-tampered
|
|
27
|
+
// objects.
|
|
28
|
+
struct KernelIntrinsics;
|
|
29
|
+
|
|
30
|
+
// Shared state between the host functions installed by installKernelGlobals
|
|
31
|
+
// and the owning KernelRuntime. The jsi members must only be touched on the
|
|
32
|
+
// kernel thread; KernelRuntime clears them before destroying the runtime.
|
|
33
|
+
struct KernelGlobalsState {
|
|
34
|
+
// Directory the vault image file lives in — resolved by the host once per
|
|
35
|
+
// KernelRuntime::start (platformVaultDirectory on device builds) and passed
|
|
36
|
+
// to kernelFileRead/kernelFileWrite by the __qntx.file lambdas. Kernel
|
|
37
|
+
// thread only after start().
|
|
38
|
+
std::string vaultDirectory;
|
|
39
|
+
|
|
40
|
+
// Copies the posted bytes out of the runtime and reports them to the host.
|
|
41
|
+
std::function<void(std::vector<uint8_t>)> postToHost;
|
|
42
|
+
|
|
43
|
+
// Records a fatal kernel failure reported by JS (unhandled promise
|
|
44
|
+
// rejection). The runtime tears down after the current task drains.
|
|
45
|
+
std::function<void(std::string)> reportFatal;
|
|
46
|
+
|
|
47
|
+
// Schedules fn at deadline on the kernel task queue. Returns the
|
|
48
|
+
// cancellation closure: calling it on the kernel thread erases the entry
|
|
49
|
+
// immediately, releasing the stored jsi::Function. Both are kernel-thread
|
|
50
|
+
// only — timers never cross threads.
|
|
51
|
+
using TimerCancel = std::function<void()>;
|
|
52
|
+
std::function<TimerCancel(std::chrono::steady_clock::time_point, facebook::jsi::Function)>
|
|
53
|
+
scheduleTimer;
|
|
54
|
+
|
|
55
|
+
// Enqueues a task that runs fn(rt) on the kernel thread. Callable from any
|
|
56
|
+
// thread — background completions use it to return to the runtime.
|
|
57
|
+
std::function<void(std::function<void(facebook::jsi::Runtime&)>)> enqueueTask;
|
|
58
|
+
|
|
59
|
+
// True while the owning runtime is alive. Cleared in teardown AFTER pending
|
|
60
|
+
// callbacks are dropped, so a KDF worker that sees false may safely release
|
|
61
|
+
// its job (and the parked KernelPendingCall) off the kernel thread.
|
|
62
|
+
std::shared_ptr<std::atomic_bool> active;
|
|
63
|
+
|
|
64
|
+
// Callbacks parked for in-flight async platform calls. Kernel thread only;
|
|
65
|
+
// teardown clears every `done` before the runtime is destroyed.
|
|
66
|
+
std::unordered_set<std::shared_ptr<KernelPendingCall>> pending;
|
|
67
|
+
|
|
68
|
+
// Captured intrinsics for the host functions. Kernel thread only; teardown
|
|
69
|
+
// resets it while the runtime is still alive.
|
|
70
|
+
std::shared_ptr<KernelIntrinsics> intrinsics;
|
|
71
|
+
|
|
72
|
+
// Kernel-side handler for bytes delivered by KernelRuntime::post; installed
|
|
73
|
+
// by the script through __qntx.setMessageHandler or __qntx.frames.onMessage.
|
|
74
|
+
// Kernel thread only.
|
|
75
|
+
std::optional<facebook::jsi::Function> messageHandler;
|
|
76
|
+
|
|
77
|
+
// Kernel-side handler the host calls when it refuses an inbound frame on
|
|
78
|
+
// integrity grounds (oversize, queue full); installed through
|
|
79
|
+
// __qntx.frames.onIntegrityViolation. Kernel thread only.
|
|
80
|
+
std::optional<facebook::jsi::Function> integrityHandler;
|
|
81
|
+
|
|
82
|
+
// Dismiss in-flight system prompts (keystore, passkey); each installer adds
|
|
83
|
+
// one and teardown runs them all. Kernel thread only.
|
|
84
|
+
std::vector<std::function<void()>> cancelCeremonies;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Installs the kernel's capabilities on a fresh runtime:
|
|
88
|
+
// globalThis.__qntx frozen { random, now, elapsed, setTimer, pbkdf2, file,
|
|
89
|
+
// frames, backends, keystore, passkey }
|
|
90
|
+
// globalThis.crypto frozen { getRandomValues }
|
|
91
|
+
// globalThis.TextDecoder the native Hermes decoder (260318099+), frozen
|
|
92
|
+
// and rebound hidden
|
|
93
|
+
// globalThis.setTimeout / clearTimeout id-based, kernel-thread timers —
|
|
94
|
+
// Hermes' bundled promise rejection tracker resolves these literals at
|
|
95
|
+
// call time to defer onUnhandled; Promise.then/async use the engine queue
|
|
96
|
+
// and need no globals (measured).
|
|
97
|
+
// Each is installed via Object.defineProperty as non-writable,
|
|
98
|
+
// non-configurable and non-enumerable.
|
|
99
|
+
//
|
|
100
|
+
// Also evaluates the kernel prelude — a function expression invoked once with
|
|
101
|
+
// a private `natives` object { setTimer, reportUnhandled }. Private
|
|
102
|
+
// natives are never reachable from globalThis.
|
|
103
|
+
void installKernelGlobals(
|
|
104
|
+
facebook::jsi::Runtime& runtime,
|
|
105
|
+
const std::shared_ptr<KernelGlobalsState>& state);
|
|
106
|
+
|
|
107
|
+
} // namespace qntx
|