@qntx/wallet-native 0.0.0-stage → 0.5.2

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 (71) hide show
  1. package/LICENSE-APACHE +201 -0
  2. package/LICENSE-MIT +21 -0
  3. package/README.md +123 -2
  4. package/WalletNative.podspec +74 -0
  5. package/android/CMakeLists.txt +99 -0
  6. package/android/build.gradle +177 -0
  7. package/android/consumer-rules.pro +4 -0
  8. package/android/src/main/AndroidManifest.xml +2 -0
  9. package/android/src/main/cpp/cpp-adapter.cpp +29 -0
  10. package/android/src/main/java/com/margelo/nitro/qntxwallet/AppLifecycle.kt +35 -0
  11. package/android/src/main/java/com/margelo/nitro/qntxwallet/KernelPbkdf2.java +59 -0
  12. package/android/src/main/java/com/margelo/nitro/qntxwallet/Keystore.kt +280 -0
  13. package/android/src/main/java/com/margelo/nitro/qntxwallet/Passkey.kt +147 -0
  14. package/android/src/main/java/com/margelo/nitro/qntxwallet/VaultDirectory.kt +28 -0
  15. package/android/src/main/java/com/margelo/nitro/qntxwallet/WalletNativePackage.java +30 -0
  16. package/cpp/host/HybridKernelHost.cpp +162 -0
  17. package/cpp/host/HybridKernelHost.hpp +49 -0
  18. package/cpp/host/KernelFile.cpp +120 -0
  19. package/cpp/host/KernelFile.hpp +24 -0
  20. package/cpp/host/Keystore.hpp +70 -0
  21. package/cpp/host/Lifecycle.cpp +77 -0
  22. package/cpp/host/Lifecycle.hpp +71 -0
  23. package/cpp/host/Passkey.hpp +39 -0
  24. package/cpp/host/VaultDirectory.hpp +16 -0
  25. package/cpp/platform/KeystoreAndroid.cpp +175 -0
  26. package/cpp/platform/KeystoreIos.mm +300 -0
  27. package/cpp/platform/LifecycleAndroid.cpp +55 -0
  28. package/cpp/platform/LifecycleIos.mm +36 -0
  29. package/cpp/platform/PasskeyAndroid.cpp +118 -0
  30. package/cpp/platform/PasskeyIos.mm +20 -0
  31. package/cpp/platform/VaultDirectory.mm +58 -0
  32. package/cpp/platform/VaultDirectoryAndroid.cpp +35 -0
  33. package/cpp/runtime/KernelBackends.cpp +297 -0
  34. package/cpp/runtime/KernelBackends.hpp +20 -0
  35. package/cpp/runtime/KernelBuffer.cpp +12 -0
  36. package/cpp/runtime/KernelBuffer.hpp +96 -0
  37. package/cpp/runtime/KernelGlobals.cpp +535 -0
  38. package/cpp/runtime/KernelGlobals.hpp +107 -0
  39. package/cpp/runtime/KernelJs.cpp +155 -0
  40. package/cpp/runtime/KernelJs.hpp +81 -0
  41. package/cpp/runtime/KernelKeystore.cpp +224 -0
  42. package/cpp/runtime/KernelKeystore.hpp +28 -0
  43. package/cpp/runtime/KernelPasskey.cpp +137 -0
  44. package/cpp/runtime/KernelPasskey.hpp +23 -0
  45. package/cpp/runtime/KernelRuntime.cpp +496 -0
  46. package/cpp/runtime/KernelRuntime.hpp +94 -0
  47. package/cpp/runtime/platform/KernelPbkdf2.cpp +198 -0
  48. package/cpp/runtime/platform/KernelPbkdf2.hpp +38 -0
  49. package/dist/index.d.mts +44 -0
  50. package/dist/index.mjs +170 -0
  51. package/dist/index.mjs.map +1 -0
  52. package/kernel/generated/kernel-entry.js +42967 -0
  53. package/kernel/generated/kernel.manifest.json +80 -0
  54. package/nitrogen/generated/.gitattributes +1 -0
  55. package/nitrogen/generated/android/WalletNative+autolinking.cmake +81 -0
  56. package/nitrogen/generated/android/WalletNative+autolinking.gradle +27 -0
  57. package/nitrogen/generated/android/WalletNativeOnLoad.cpp +49 -0
  58. package/nitrogen/generated/android/WalletNativeOnLoad.hpp +34 -0
  59. package/nitrogen/generated/android/kotlin/com/margelo/nitro/qntxwallet/WalletNativeOnLoad.kt +35 -0
  60. package/nitrogen/generated/ios/WalletNative+autolinking.rb +62 -0
  61. package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Bridge.cpp +17 -0
  62. package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Bridge.hpp +27 -0
  63. package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Umbrella.hpp +38 -0
  64. package/nitrogen/generated/ios/WalletNativeAutolinking.mm +35 -0
  65. package/nitrogen/generated/ios/WalletNativeAutolinking.swift +16 -0
  66. package/nitrogen/generated/shared/c++/HybridKernelHostSpec.cpp +25 -0
  67. package/nitrogen/generated/shared/c++/HybridKernelHostSpec.hpp +69 -0
  68. package/nitrogen/generated/shared/c++/KernelExitReason.hpp +84 -0
  69. package/package.json +65 -4
  70. package/react-native.config.js +14 -0
  71. package/scripts/compile-kernel.mjs +118 -0
@@ -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
@@ -0,0 +1,155 @@
1
+ #include "KernelJs.hpp"
2
+
3
+ #include "KernelBuffer.hpp"
4
+
5
+ #include <cmath>
6
+ #include <stdexcept>
7
+
8
+ namespace qntx {
9
+
10
+ jsi::Function hostFn(
11
+ jsi::Runtime& rt,
12
+ const char* name,
13
+ unsigned int paramCount,
14
+ jsi::HostFunctionType fn) {
15
+ return jsi::Function::createFromHostFunction(
16
+ rt, jsi::PropNameID::forAscii(rt, name), paramCount, std::move(fn));
17
+ }
18
+
19
+ double finiteNumber(jsi::Runtime& rt, const jsi::Value& value, const char* name) {
20
+ if (!value.isNumber() || !std::isfinite(value.getNumber())) {
21
+ throw jsi::JSError(rt, std::string(name) + " expects a finite number");
22
+ }
23
+ return value.getNumber();
24
+ }
25
+
26
+ uint64_t integerInRange(
27
+ jsi::Runtime& rt,
28
+ const jsi::Value& value,
29
+ const char* name,
30
+ double min,
31
+ double max) {
32
+ const double n = finiteNumber(rt, value, name);
33
+ if (n < min || n > max || std::floor(n) != n) {
34
+ throw jsi::JSError(rt, std::string(name) + " is out of range");
35
+ }
36
+ return static_cast<uint64_t>(n);
37
+ }
38
+
39
+ ByteView byteViewFromValue(
40
+ jsi::Runtime& rt,
41
+ const jsi::Value& value,
42
+ const char* name,
43
+ const KernelIntrinsics& intrinsics) {
44
+ if (!value.isObject()) {
45
+ throw jsi::JSError(rt, std::string(name) + " expects an ArrayBuffer or TypedArray");
46
+ }
47
+ auto object = value.asObject(rt);
48
+ if (object.isArrayBuffer(rt)) {
49
+ auto buffer = object.getArrayBuffer(rt);
50
+ uint8_t* data = buffer.data(rt);
51
+ const size_t size = buffer.size(rt);
52
+ return {std::move(buffer), data, size};
53
+ }
54
+ const jsi::Value bufferValue =
55
+ intrinsics.taBufferGet.callWithThis(rt, object);
56
+ const jsi::Value offsetValue =
57
+ intrinsics.taByteOffsetGet.callWithThis(rt, object);
58
+ const jsi::Value lengthValue =
59
+ intrinsics.taByteLengthGet.callWithThis(rt, object);
60
+ if (!bufferValue.isObject() || !bufferValue.asObject(rt).isArrayBuffer(rt)) {
61
+ throw jsi::JSError(rt, std::string(name) + " view is not backed by an ArrayBuffer");
62
+ }
63
+ const double offset = finiteNumber(rt, offsetValue, "byteOffset");
64
+ const double length = finiteNumber(rt, lengthValue, "byteLength");
65
+ if (offset < 0 || std::floor(offset) != offset ||
66
+ length < 0 || std::floor(length) != length) {
67
+ throw jsi::JSError(rt, std::string(name) + " view bounds must be non-negative integers");
68
+ }
69
+ auto buffer = bufferValue.asObject(rt).getArrayBuffer(rt);
70
+ uint8_t* data = buffer.data(rt);
71
+ const size_t bufferSize = buffer.size(rt);
72
+ if (offset + length > static_cast<double>(bufferSize)) {
73
+ throw jsi::JSError(rt, std::string(name) + " view exceeds its ArrayBuffer");
74
+ }
75
+ return {std::move(buffer), data + static_cast<size_t>(offset), static_cast<size_t>(length)};
76
+ }
77
+
78
+ jsi::ArrayBuffer arrayBufferFromBytes(
79
+ jsi::Runtime& rt,
80
+ const uint8_t* bytes,
81
+ size_t size) {
82
+ auto buffer = std::make_shared<OwningBuffer>(size);
83
+ if (bytes != nullptr && size > 0) {
84
+ std::memcpy(buffer->data(), bytes, size);
85
+ }
86
+ return rt.createArrayBuffer(std::move(buffer));
87
+ }
88
+
89
+ jsi::Object makeUint8Array(
90
+ jsi::Runtime& rt,
91
+ const KernelIntrinsics& intrinsics,
92
+ jsi::ArrayBuffer buffer) {
93
+ return intrinsics.uint8Array
94
+ .callAsConstructor(rt, jsi::Value(std::move(buffer)))
95
+ .asObject(rt);
96
+ }
97
+
98
+ std::shared_ptr<KernelIntrinsics> captureIntrinsics(jsi::Runtime& rt) {
99
+ auto global = rt.global();
100
+ auto objectCtor = global.getPropertyAsObject(rt, "Object");
101
+ auto getPrototypeOf = objectCtor.getPropertyAsFunction(rt, "getPrototypeOf");
102
+ auto getDescriptor = objectCtor.getPropertyAsFunction(rt, "getOwnPropertyDescriptor");
103
+
104
+ auto requireGetter = [&](const char* name,
105
+ const jsi::Value& container,
106
+ const jsi::Value& key) -> jsi::Function {
107
+ const jsi::Value descriptor = getDescriptor.call(rt, container, key);
108
+ const jsi::Value get = descriptor.isObject()
109
+ ? descriptor.asObject(rt).getProperty(rt, "get")
110
+ : jsi::Value::undefined();
111
+ if (!get.isObject() || !get.asObject(rt).isFunction(rt)) {
112
+ throw std::runtime_error(
113
+ std::string("kernel globals: missing TypedArray intrinsic ") + name);
114
+ }
115
+ return get.asObject(rt).asFunction(rt);
116
+ };
117
+
118
+ auto uint8Array = global.getPropertyAsFunction(rt, "Uint8Array");
119
+ const jsi::Value typedArrayProto =
120
+ getPrototypeOf.call(rt, uint8Array.getProperty(rt, "prototype"));
121
+ const jsi::Value toStringTag =
122
+ global.getPropertyAsObject(rt, "Symbol").getProperty(rt, "toStringTag");
123
+ const auto nameKey = [&](const char* name) {
124
+ return jsi::Value(rt, jsi::String::createFromAscii(rt, name));
125
+ };
126
+
127
+ return std::shared_ptr<KernelIntrinsics>(new KernelIntrinsics{
128
+ objectCtor.getPropertyAsFunction(rt, "defineProperty"),
129
+ objectCtor.getPropertyAsFunction(rt, "freeze"),
130
+ std::move(uint8Array),
131
+ requireGetter("buffer", typedArrayProto, nameKey("buffer")),
132
+ requireGetter("byteOffset", typedArrayProto, nameKey("byteOffset")),
133
+ requireGetter("byteLength", typedArrayProto, nameKey("byteLength")),
134
+ requireGetter("Symbol.toStringTag", typedArrayProto, toStringTag),
135
+ });
136
+ }
137
+
138
+ void defineHiddenGlobal(
139
+ jsi::Runtime& rt,
140
+ const KernelIntrinsics& intrinsics,
141
+ const char* name,
142
+ jsi::Value value) {
143
+ jsi::Object descriptor(rt);
144
+ descriptor.setProperty(rt, "value", std::move(value));
145
+ descriptor.setProperty(rt, "writable", jsi::Value(false));
146
+ descriptor.setProperty(rt, "configurable", jsi::Value(false));
147
+ descriptor.setProperty(rt, "enumerable", jsi::Value(false));
148
+ intrinsics.defineProperty.call(
149
+ rt,
150
+ rt.global(),
151
+ jsi::Value(rt, jsi::String::createFromAscii(rt, name)),
152
+ jsi::Value(std::move(descriptor)));
153
+ }
154
+
155
+ } // namespace qntx
@@ -0,0 +1,81 @@
1
+ #pragma once
2
+
3
+ #include <jsi/jsi.h>
4
+
5
+ #include <cstdint>
6
+ #include <memory>
7
+ #include <string>
8
+
9
+ namespace qntx {
10
+
11
+ namespace jsi = facebook::jsi;
12
+
13
+ // Captured JS intrinsics, taken on the pristine runtime before any script can
14
+ // tamper with the builtins they come from. Every member is a jsi value owned
15
+ // by the kernel thread; teardown resets the holding shared_ptr while the
16
+ // runtime is still alive.
17
+ struct KernelIntrinsics {
18
+ jsi::Function defineProperty;
19
+ jsi::Function freeze;
20
+ jsi::Function uint8Array;
21
+ // The real %TypedArray%.prototype accessors. Calling them reads internal
22
+ // slots, so forged objects throw and redefined prototype getters cannot lie.
23
+ jsi::Function taBufferGet;
24
+ jsi::Function taByteOffsetGet;
25
+ jsi::Function taByteLengthGet;
26
+ jsi::Function taTagGet;
27
+ };
28
+
29
+ std::shared_ptr<KernelIntrinsics> captureIntrinsics(jsi::Runtime& rt);
30
+
31
+ jsi::Function hostFn(
32
+ jsi::Runtime& rt,
33
+ const char* name,
34
+ unsigned int paramCount,
35
+ jsi::HostFunctionType fn);
36
+
37
+ void defineHiddenGlobal(
38
+ jsi::Runtime& rt,
39
+ const KernelIntrinsics& intrinsics,
40
+ const char* name,
41
+ jsi::Value value);
42
+
43
+ double finiteNumber(jsi::Runtime& rt, const jsi::Value& value, const char* name);
44
+
45
+ uint64_t integerInRange(
46
+ jsi::Runtime& rt,
47
+ const jsi::Value& value,
48
+ const char* name,
49
+ double min,
50
+ double max);
51
+
52
+ // A view into a live ArrayBuffer: `buffer` keeps the storage referenced while
53
+ // `data`/`size` are borrowed from it.
54
+ struct ByteView {
55
+ jsi::ArrayBuffer buffer;
56
+ uint8_t* data;
57
+ size_t size;
58
+ };
59
+
60
+ // The shared extractor for every byte input. Accepts a real ArrayBuffer or a
61
+ // genuine TypedArray — identified by calling the captured
62
+ // %TypedArray%.prototype getters, which read internal slots: forged plain
63
+ // objects throw TypeError and a redefined Uint8Array.prototype getter cannot
64
+ // lie.
65
+ ByteView byteViewFromValue(
66
+ jsi::Runtime& rt,
67
+ const jsi::Value& value,
68
+ const char* name,
69
+ const KernelIntrinsics& intrinsics);
70
+
71
+ jsi::ArrayBuffer arrayBufferFromBytes(
72
+ jsi::Runtime& rt,
73
+ const uint8_t* bytes,
74
+ size_t size);
75
+
76
+ jsi::Object makeUint8Array(
77
+ jsi::Runtime& rt,
78
+ const KernelIntrinsics& intrinsics,
79
+ jsi::ArrayBuffer buffer);
80
+
81
+ } // namespace qntx
@@ -0,0 +1,224 @@
1
+ #include "KernelKeystore.hpp"
2
+
3
+ #include <algorithm>
4
+ #include <string>
5
+ #include <utility>
6
+ #include <vector>
7
+
8
+ #include "../host/Keystore.hpp"
9
+ #include "KernelBuffer.hpp"
10
+ #include "KernelGlobals.hpp"
11
+ #include "KernelJs.hpp"
12
+
13
+ namespace qntx {
14
+
15
+ namespace jsi = facebook::jsi;
16
+
17
+ namespace {
18
+
19
+ constexpr size_t kFactorIdLength = 32;
20
+ constexpr size_t kTitleMax = 128;
21
+ constexpr size_t kDescriptionMax = 1024;
22
+ constexpr size_t kSecretLength = 32;
23
+ constexpr size_t kSealedMax = 512;
24
+
25
+ std::string factorId(jsi::Runtime& rt, const jsi::Value& value, const char* name) {
26
+ if (!value.isString()) {
27
+ throw jsi::JSError(rt, std::string(name) + " expects a factor id");
28
+ }
29
+ auto id = value.asString(rt).utf8(rt);
30
+ const bool hex = std::all_of(id.begin(), id.end(), [](char c) {
31
+ return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f');
32
+ });
33
+ if (id.size() != kFactorIdLength || !hex) {
34
+ throw jsi::JSError(rt, std::string(name) + " factor id must be 32 lowercase hex");
35
+ }
36
+ return id;
37
+ }
38
+
39
+ std::string boundedText(
40
+ jsi::Runtime& rt,
41
+ const jsi::Value& value,
42
+ const char* name,
43
+ size_t max) {
44
+ if (!value.isString()) {
45
+ throw jsi::JSError(rt, std::string(name) + " must be a string");
46
+ }
47
+ auto text = value.asString(rt).utf8(rt);
48
+ if (text.empty() || text.size() > max) {
49
+ throw jsi::JSError(rt, std::string(name) + " length is out of range");
50
+ }
51
+ return text;
52
+ }
53
+
54
+ KeystorePrompt promptFrom(jsi::Runtime& rt, const jsi::Value* args, const char* name) {
55
+ return KeystorePrompt{
56
+ boundedText(rt, args[0], name, kTitleMax),
57
+ boundedText(rt, args[1], name, kDescriptionMax),
58
+ };
59
+ }
60
+
61
+ jsi::Function callbackFrom(jsi::Runtime& rt, const jsi::Value& value, const char* name) {
62
+ if (!value.isObject() || !value.asObject(rt).isFunction(rt)) {
63
+ throw jsi::JSError(rt, std::string(name) + " expects a completion function");
64
+ }
65
+ return value.asObject(rt).asFunction(rt);
66
+ }
67
+
68
+ const char* errorCode(KeystoreStatus status) {
69
+ switch (status) {
70
+ case KeystoreStatus::cancelled:
71
+ return "cancelled";
72
+ case KeystoreStatus::invalidated:
73
+ return "factor-invalidated";
74
+ case KeystoreStatus::unavailable:
75
+ return "factor-unavailable";
76
+ case KeystoreStatus::ok:
77
+ case KeystoreStatus::failed:
78
+ break;
79
+ }
80
+ return "unavailable";
81
+ }
82
+
83
+ // Shared by every keystore call of one runtime: the number of platform calls
84
+ // whose completion has not yet returned (kernel thread only).
85
+ struct KeystoreCalls {
86
+ int inFlight = 0;
87
+ };
88
+
89
+ // Parks `done` as a pending call and returns the platform completion that
90
+ // hands the result back on the kernel thread. `withBytes` says whether a
91
+ // success carries bytes (seal, open) or nothing (authenticate, clear).
92
+ KeystoreDone park(
93
+ const std::shared_ptr<KernelGlobalsState>& state,
94
+ const std::shared_ptr<KeystoreCalls>& calls,
95
+ jsi::Function done,
96
+ bool withBytes) {
97
+ auto call = std::make_shared<KernelPendingCall>();
98
+ call->done = std::move(done);
99
+ state->pending.insert(call);
100
+ calls->inFlight += 1;
101
+ // The platform may hold this completion long after a lock tore the runtime
102
+ // down (a prompt left on screen), so it keeps only a weak reference.
103
+ std::weak_ptr<KernelGlobalsState> weak = state;
104
+ return [weak, calls, call, withBytes](KeystoreResult result) {
105
+ auto state = weak.lock();
106
+ if (state == nullptr || !state->active->load(std::memory_order_acquire)) {
107
+ return; // the runtime is gone; teardown already dropped `done`
108
+ }
109
+ auto shared = std::make_shared<KeystoreResult>(std::move(result));
110
+ state->enqueueTask([state, calls, call, shared, withBytes](jsi::Runtime& rt) {
111
+ calls->inFlight -= 1;
112
+ auto done = std::move(call->done);
113
+ state->pending.erase(call);
114
+ if (!done.has_value()) {
115
+ return;
116
+ }
117
+ if (shared->status != KeystoreStatus::ok) {
118
+ done->call(
119
+ rt,
120
+ jsi::Value::undefined(),
121
+ jsi::Value(rt, jsi::String::createFromAscii(rt, errorCode(shared->status))));
122
+ return;
123
+ }
124
+ if (!withBytes) {
125
+ done->call(rt, jsi::Value::undefined(), jsi::Value::undefined());
126
+ return;
127
+ }
128
+ if (shared->bytes.size() == 0) {
129
+ done->call(
130
+ rt,
131
+ jsi::Value::undefined(),
132
+ jsi::Value(rt, jsi::String::createFromAscii(rt, errorCode(KeystoreStatus::failed))));
133
+ return;
134
+ }
135
+ auto buffer = arrayBufferFromBytes(rt, shared->bytes.data(), shared->bytes.size());
136
+ auto array = makeUint8Array(rt, *state->intrinsics, std::move(buffer));
137
+ done->call(rt, jsi::Value(std::move(array)), jsi::Value::undefined());
138
+ });
139
+ };
140
+ }
141
+
142
+ } // namespace
143
+
144
+ void installKernelKeystore(
145
+ jsi::Runtime& rt,
146
+ jsi::Object& qntx,
147
+ const std::shared_ptr<KernelGlobalsState>& state) {
148
+ auto calls = std::make_shared<KeystoreCalls>();
149
+ state->cancelCeremonies.emplace_back([calls] {
150
+ if (calls->inFlight > 0) {
151
+ keystoreCancel();
152
+ }
153
+ });
154
+
155
+ auto keystore = jsi::Object(rt);
156
+ keystore.setProperty(rt, "locale", jsi::String::createFromUtf8(rt, keystoreLocale()));
157
+ keystore.setProperty(
158
+ rt,
159
+ "seal",
160
+ hostFn(rt, "keystore.seal", 5, [state, calls](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
161
+ if (count < 5) {
162
+ throw jsi::JSError(
163
+ rt, "__qntx.keystore.seal expects (id, secret, title, description, done)");
164
+ }
165
+ auto id = factorId(rt, args[0], "__qntx.keystore.seal");
166
+ auto view = byteViewFromValue(rt, args[1], "__qntx.keystore.seal secret", *state->intrinsics);
167
+ if (view.size != kSecretLength) {
168
+ throw jsi::JSError(rt, "__qntx.keystore.seal secret must be 32 bytes");
169
+ }
170
+ SecretBytes secret(view.data, view.size);
171
+ auto prompt = promptFrom(rt, args + 2, "__qntx.keystore.seal prompt");
172
+ auto done = park(state, calls, callbackFrom(rt, args[4], "__qntx.keystore.seal"), true);
173
+ keystoreSeal(id, std::move(secret), std::move(prompt), std::move(done));
174
+ return jsi::Value::undefined();
175
+ }));
176
+ keystore.setProperty(
177
+ rt,
178
+ "open",
179
+ hostFn(rt, "keystore.open", 5, [state, calls](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
180
+ if (count < 5) {
181
+ throw jsi::JSError(
182
+ rt, "__qntx.keystore.open expects (id, sealed, title, description, done)");
183
+ }
184
+ auto id = factorId(rt, args[0], "__qntx.keystore.open");
185
+ auto view = byteViewFromValue(rt, args[1], "__qntx.keystore.open sealed", *state->intrinsics);
186
+ if (view.size == 0 || view.size > kSealedMax) {
187
+ throw jsi::JSError(rt, "__qntx.keystore.open sealed length is out of range");
188
+ }
189
+ std::vector<uint8_t> sealed(view.data, view.data + view.size);
190
+ auto prompt = promptFrom(rt, args + 2, "__qntx.keystore.open prompt");
191
+ auto done = park(state, calls, callbackFrom(rt, args[4], "__qntx.keystore.open"), true);
192
+ keystoreOpen(id, std::move(sealed), std::move(prompt), std::move(done));
193
+ return jsi::Value::undefined();
194
+ }));
195
+ keystore.setProperty(
196
+ rt,
197
+ "authenticate",
198
+ hostFn(rt, "keystore.authenticate", 3, [state, calls](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
199
+ if (count < 3) {
200
+ throw jsi::JSError(
201
+ rt, "__qntx.keystore.authenticate expects (title, description, done)");
202
+ }
203
+ auto prompt = promptFrom(rt, args, "__qntx.keystore.authenticate prompt");
204
+ auto done =
205
+ park(state, calls, callbackFrom(rt, args[2], "__qntx.keystore.authenticate"), false);
206
+ keystoreAuthenticate(std::move(prompt), std::move(done));
207
+ return jsi::Value::undefined();
208
+ }));
209
+ keystore.setProperty(
210
+ rt,
211
+ "clear",
212
+ hostFn(rt, "keystore.clear", 1, [state, calls](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
213
+ if (count < 1) {
214
+ throw jsi::JSError(rt, "__qntx.keystore.clear expects (done)");
215
+ }
216
+ auto done = park(state, calls, callbackFrom(rt, args[0], "__qntx.keystore.clear"), false);
217
+ keystoreClear(std::move(done));
218
+ return jsi::Value::undefined();
219
+ }));
220
+ state->intrinsics->freeze.call(rt, jsi::Value(rt, keystore));
221
+ qntx.setProperty(rt, "keystore", jsi::Value(std::move(keystore)));
222
+ }
223
+
224
+ } // namespace qntx
@@ -0,0 +1,28 @@
1
+ #pragma once
2
+
3
+ #include <jsi/jsi.h>
4
+
5
+ #include <memory>
6
+
7
+ namespace qntx {
8
+
9
+ struct KernelGlobalsState;
10
+
11
+ // Installs the frozen `keystore` object on `__qntx` before the caller freezes
12
+ // it:
13
+ // locale platform language (BCP 47)
14
+ // seal(id, secret, title, description, done) done(sealed | undefined, code | undefined)
15
+ // open(id, sealed, title, description, done) done(kek | undefined, code | undefined)
16
+ // authenticate(title, description, done) done(undefined, code | undefined)
17
+ // clear(done) done(undefined, code | undefined)
18
+ // Arguments are validated before the platform sees them (id: 32 lowercase hex;
19
+ // title ≤ 128 and description ≤ 1024 UTF-8 bytes, both non-empty; secret
20
+ // exactly 32 bytes; sealed 1–512 bytes). Completions return to the kernel
21
+ // thread through the pending-call machinery pbkdf2 uses; `code` is the
22
+ // kernel error code for a failure. Teardown cancels an in-flight prompt.
23
+ void installKernelKeystore(
24
+ facebook::jsi::Runtime& rt,
25
+ facebook::jsi::Object& qntx,
26
+ const std::shared_ptr<KernelGlobalsState>& state);
27
+
28
+ } // namespace qntx