@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.
- package/LICENSE-APACHE +201 -0
- package/LICENSE-MIT +21 -0
- 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 +65 -4
- package/react-native.config.js +14 -0
- package/scripts/compile-kernel.mjs +118 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <jsi/jsi.h>
|
|
4
|
+
|
|
5
|
+
#include <cstdint>
|
|
6
|
+
#include <cstring>
|
|
7
|
+
#include <memory>
|
|
8
|
+
#include <vector>
|
|
9
|
+
|
|
10
|
+
namespace qntx {
|
|
11
|
+
|
|
12
|
+
namespace jsi = facebook::jsi;
|
|
13
|
+
|
|
14
|
+
// Zeroes secret bytes without letting the optimizer elide the store.
|
|
15
|
+
void kernelWipe(uint8_t* data, size_t size);
|
|
16
|
+
void kernelWipe(std::vector<uint8_t>& bytes);
|
|
17
|
+
|
|
18
|
+
// Read-only buffer over script bytes retained by shared_ptr; used for embedded
|
|
19
|
+
// bytecode, which Hermes may read lazily for the runtime's lifetime.
|
|
20
|
+
class VectorBuffer final : public jsi::Buffer {
|
|
21
|
+
public:
|
|
22
|
+
explicit VectorBuffer(std::shared_ptr<const std::vector<uint8_t>> bytes)
|
|
23
|
+
: bytes_(std::move(bytes)) {}
|
|
24
|
+
size_t size() const override {
|
|
25
|
+
return bytes_->size();
|
|
26
|
+
}
|
|
27
|
+
const uint8_t* data() const override {
|
|
28
|
+
return bytes_->data();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private:
|
|
32
|
+
std::shared_ptr<const std::vector<uint8_t>> bytes_;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Byte storage that always wipes itself on destruction. Move-only: a moved-from
|
|
36
|
+
// instance carries no bytes, so each live copy has exactly one wiper. Used for
|
|
37
|
+
// KDF inputs and outputs, where a dropped result must not linger in memory.
|
|
38
|
+
class SecretBytes {
|
|
39
|
+
public:
|
|
40
|
+
SecretBytes() = default;
|
|
41
|
+
explicit SecretBytes(std::vector<uint8_t> bytes) : bytes_(std::move(bytes)) {}
|
|
42
|
+
explicit SecretBytes(size_t size) : bytes_(size) {}
|
|
43
|
+
SecretBytes(const uint8_t* data, size_t size) : bytes_(data, data + size) {}
|
|
44
|
+
|
|
45
|
+
SecretBytes(const SecretBytes&) = delete;
|
|
46
|
+
SecretBytes& operator=(const SecretBytes&) = delete;
|
|
47
|
+
|
|
48
|
+
SecretBytes(SecretBytes&& other) noexcept = default;
|
|
49
|
+
SecretBytes& operator=(SecretBytes&& other) noexcept {
|
|
50
|
+
if (this != &other) {
|
|
51
|
+
kernelWipe(bytes_);
|
|
52
|
+
bytes_ = std::move(other.bytes_);
|
|
53
|
+
}
|
|
54
|
+
return *this;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
~SecretBytes() {
|
|
58
|
+
kernelWipe(bytes_);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const uint8_t* data() const {
|
|
62
|
+
return bytes_.data();
|
|
63
|
+
}
|
|
64
|
+
uint8_t* data() {
|
|
65
|
+
return bytes_.data();
|
|
66
|
+
}
|
|
67
|
+
size_t size() const {
|
|
68
|
+
return bytes_.size();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private:
|
|
72
|
+
std::vector<uint8_t> bytes_;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Owning MutableBuffer; the runtime shares ownership of the byte storage and
|
|
76
|
+
// the bytes are wiped when the last reference goes away, because these buffers
|
|
77
|
+
// back ArrayBuffers that can hold secrets.
|
|
78
|
+
class OwningBuffer final : public jsi::MutableBuffer {
|
|
79
|
+
public:
|
|
80
|
+
explicit OwningBuffer(std::vector<uint8_t> storage) : storage_(std::move(storage)) {}
|
|
81
|
+
explicit OwningBuffer(size_t size) : storage_(size) {}
|
|
82
|
+
~OwningBuffer() override {
|
|
83
|
+
kernelWipe(storage_);
|
|
84
|
+
}
|
|
85
|
+
size_t size() const override {
|
|
86
|
+
return storage_.size();
|
|
87
|
+
}
|
|
88
|
+
uint8_t* data() override {
|
|
89
|
+
return storage_.data();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private:
|
|
93
|
+
std::vector<uint8_t> storage_;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
} // namespace qntx
|
|
@@ -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
|