@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,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
|