@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.
Files changed (69) hide show
  1. package/README.md +123 -2
  2. package/WalletNative.podspec +74 -0
  3. package/android/CMakeLists.txt +99 -0
  4. package/android/build.gradle +177 -0
  5. package/android/consumer-rules.pro +4 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/cpp-adapter.cpp +29 -0
  8. package/android/src/main/java/com/margelo/nitro/qntxwallet/AppLifecycle.kt +35 -0
  9. package/android/src/main/java/com/margelo/nitro/qntxwallet/KernelPbkdf2.java +59 -0
  10. package/android/src/main/java/com/margelo/nitro/qntxwallet/Keystore.kt +280 -0
  11. package/android/src/main/java/com/margelo/nitro/qntxwallet/Passkey.kt +147 -0
  12. package/android/src/main/java/com/margelo/nitro/qntxwallet/VaultDirectory.kt +28 -0
  13. package/android/src/main/java/com/margelo/nitro/qntxwallet/WalletNativePackage.java +30 -0
  14. package/cpp/host/HybridKernelHost.cpp +162 -0
  15. package/cpp/host/HybridKernelHost.hpp +49 -0
  16. package/cpp/host/KernelFile.cpp +120 -0
  17. package/cpp/host/KernelFile.hpp +24 -0
  18. package/cpp/host/Keystore.hpp +70 -0
  19. package/cpp/host/Lifecycle.cpp +77 -0
  20. package/cpp/host/Lifecycle.hpp +71 -0
  21. package/cpp/host/Passkey.hpp +39 -0
  22. package/cpp/host/VaultDirectory.hpp +16 -0
  23. package/cpp/platform/KeystoreAndroid.cpp +175 -0
  24. package/cpp/platform/KeystoreIos.mm +300 -0
  25. package/cpp/platform/LifecycleAndroid.cpp +55 -0
  26. package/cpp/platform/LifecycleIos.mm +36 -0
  27. package/cpp/platform/PasskeyAndroid.cpp +118 -0
  28. package/cpp/platform/PasskeyIos.mm +20 -0
  29. package/cpp/platform/VaultDirectory.mm +58 -0
  30. package/cpp/platform/VaultDirectoryAndroid.cpp +35 -0
  31. package/cpp/runtime/KernelBackends.cpp +297 -0
  32. package/cpp/runtime/KernelBackends.hpp +20 -0
  33. package/cpp/runtime/KernelBuffer.cpp +12 -0
  34. package/cpp/runtime/KernelBuffer.hpp +96 -0
  35. package/cpp/runtime/KernelGlobals.cpp +535 -0
  36. package/cpp/runtime/KernelGlobals.hpp +107 -0
  37. package/cpp/runtime/KernelJs.cpp +155 -0
  38. package/cpp/runtime/KernelJs.hpp +81 -0
  39. package/cpp/runtime/KernelKeystore.cpp +224 -0
  40. package/cpp/runtime/KernelKeystore.hpp +28 -0
  41. package/cpp/runtime/KernelPasskey.cpp +137 -0
  42. package/cpp/runtime/KernelPasskey.hpp +23 -0
  43. package/cpp/runtime/KernelRuntime.cpp +496 -0
  44. package/cpp/runtime/KernelRuntime.hpp +94 -0
  45. package/cpp/runtime/platform/KernelPbkdf2.cpp +198 -0
  46. package/cpp/runtime/platform/KernelPbkdf2.hpp +38 -0
  47. package/dist/index.d.mts +44 -0
  48. package/dist/index.mjs +170 -0
  49. package/dist/index.mjs.map +1 -0
  50. package/kernel/generated/kernel-entry.js +42967 -0
  51. package/kernel/generated/kernel.manifest.json +80 -0
  52. package/nitrogen/generated/.gitattributes +1 -0
  53. package/nitrogen/generated/android/WalletNative+autolinking.cmake +81 -0
  54. package/nitrogen/generated/android/WalletNative+autolinking.gradle +27 -0
  55. package/nitrogen/generated/android/WalletNativeOnLoad.cpp +49 -0
  56. package/nitrogen/generated/android/WalletNativeOnLoad.hpp +34 -0
  57. package/nitrogen/generated/android/kotlin/com/margelo/nitro/qntxwallet/WalletNativeOnLoad.kt +35 -0
  58. package/nitrogen/generated/ios/WalletNative+autolinking.rb +62 -0
  59. package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Bridge.cpp +17 -0
  60. package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Bridge.hpp +27 -0
  61. package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Umbrella.hpp +38 -0
  62. package/nitrogen/generated/ios/WalletNativeAutolinking.mm +35 -0
  63. package/nitrogen/generated/ios/WalletNativeAutolinking.swift +16 -0
  64. package/nitrogen/generated/shared/c++/HybridKernelHostSpec.cpp +25 -0
  65. package/nitrogen/generated/shared/c++/HybridKernelHostSpec.hpp +69 -0
  66. package/nitrogen/generated/shared/c++/KernelExitReason.hpp +84 -0
  67. package/package.json +61 -4
  68. package/react-native.config.js +14 -0
  69. 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