@qntx/wallet-native 0.0.0-stage → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +123 -2
- package/WalletNative.podspec +74 -0
- package/android/CMakeLists.txt +99 -0
- package/android/build.gradle +177 -0
- package/android/consumer-rules.pro +4 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +29 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/AppLifecycle.kt +35 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/KernelPbkdf2.java +59 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/Keystore.kt +280 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/Passkey.kt +147 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/VaultDirectory.kt +28 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/WalletNativePackage.java +30 -0
- package/cpp/host/HybridKernelHost.cpp +162 -0
- package/cpp/host/HybridKernelHost.hpp +49 -0
- package/cpp/host/KernelFile.cpp +120 -0
- package/cpp/host/KernelFile.hpp +24 -0
- package/cpp/host/Keystore.hpp +70 -0
- package/cpp/host/Lifecycle.cpp +77 -0
- package/cpp/host/Lifecycle.hpp +71 -0
- package/cpp/host/Passkey.hpp +39 -0
- package/cpp/host/VaultDirectory.hpp +16 -0
- package/cpp/platform/KeystoreAndroid.cpp +175 -0
- package/cpp/platform/KeystoreIos.mm +300 -0
- package/cpp/platform/LifecycleAndroid.cpp +55 -0
- package/cpp/platform/LifecycleIos.mm +36 -0
- package/cpp/platform/PasskeyAndroid.cpp +118 -0
- package/cpp/platform/PasskeyIos.mm +20 -0
- package/cpp/platform/VaultDirectory.mm +58 -0
- package/cpp/platform/VaultDirectoryAndroid.cpp +35 -0
- package/cpp/runtime/KernelBackends.cpp +297 -0
- package/cpp/runtime/KernelBackends.hpp +20 -0
- package/cpp/runtime/KernelBuffer.cpp +12 -0
- package/cpp/runtime/KernelBuffer.hpp +96 -0
- package/cpp/runtime/KernelGlobals.cpp +535 -0
- package/cpp/runtime/KernelGlobals.hpp +107 -0
- package/cpp/runtime/KernelJs.cpp +155 -0
- package/cpp/runtime/KernelJs.hpp +81 -0
- package/cpp/runtime/KernelKeystore.cpp +224 -0
- package/cpp/runtime/KernelKeystore.hpp +28 -0
- package/cpp/runtime/KernelPasskey.cpp +137 -0
- package/cpp/runtime/KernelPasskey.hpp +23 -0
- package/cpp/runtime/KernelRuntime.cpp +496 -0
- package/cpp/runtime/KernelRuntime.hpp +94 -0
- package/cpp/runtime/platform/KernelPbkdf2.cpp +198 -0
- package/cpp/runtime/platform/KernelPbkdf2.hpp +38 -0
- package/dist/index.d.mts +44 -0
- package/dist/index.mjs +170 -0
- package/dist/index.mjs.map +1 -0
- package/kernel/generated/kernel-entry.js +42967 -0
- package/kernel/generated/kernel.manifest.json +80 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/WalletNative+autolinking.cmake +81 -0
- package/nitrogen/generated/android/WalletNative+autolinking.gradle +27 -0
- package/nitrogen/generated/android/WalletNativeOnLoad.cpp +49 -0
- package/nitrogen/generated/android/WalletNativeOnLoad.hpp +34 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/qntxwallet/WalletNativeOnLoad.kt +35 -0
- package/nitrogen/generated/ios/WalletNative+autolinking.rb +62 -0
- package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Bridge.cpp +17 -0
- package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Bridge.hpp +27 -0
- package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Umbrella.hpp +38 -0
- package/nitrogen/generated/ios/WalletNativeAutolinking.mm +35 -0
- package/nitrogen/generated/ios/WalletNativeAutolinking.swift +16 -0
- package/nitrogen/generated/shared/c++/HybridKernelHostSpec.cpp +25 -0
- package/nitrogen/generated/shared/c++/HybridKernelHostSpec.hpp +69 -0
- package/nitrogen/generated/shared/c++/KernelExitReason.hpp +84 -0
- package/package.json +61 -4
- package/react-native.config.js +14 -0
- package/scripts/compile-kernel.mjs +118 -0
|
@@ -0,0 +1,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
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#include "KernelPasskey.hpp"
|
|
2
|
+
|
|
3
|
+
#include <string>
|
|
4
|
+
#include <utility>
|
|
5
|
+
|
|
6
|
+
#include "../host/Passkey.hpp"
|
|
7
|
+
#include "KernelGlobals.hpp"
|
|
8
|
+
#include "KernelJs.hpp"
|
|
9
|
+
|
|
10
|
+
namespace qntx {
|
|
11
|
+
|
|
12
|
+
namespace jsi = facebook::jsi;
|
|
13
|
+
|
|
14
|
+
namespace {
|
|
15
|
+
|
|
16
|
+
constexpr size_t kRequestMax = 16 * 1024;
|
|
17
|
+
|
|
18
|
+
const char* errorCode(PasskeyStatus status) {
|
|
19
|
+
switch (status) {
|
|
20
|
+
case PasskeyStatus::cancelled:
|
|
21
|
+
return "cancelled";
|
|
22
|
+
case PasskeyStatus::unavailable:
|
|
23
|
+
return "factor-unavailable";
|
|
24
|
+
case PasskeyStatus::ok:
|
|
25
|
+
case PasskeyStatus::failed:
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
return "unavailable";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
struct PasskeyCalls {
|
|
32
|
+
int inFlight = 0; // kernel thread only
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
std::string requestFrom(jsi::Runtime& rt, const jsi::Value& value, const char* name) {
|
|
36
|
+
if (!value.isString()) {
|
|
37
|
+
throw jsi::JSError(rt, std::string(name) + " expects a request JSON string");
|
|
38
|
+
}
|
|
39
|
+
auto request = value.asString(rt).utf8(rt);
|
|
40
|
+
if (request.empty() || request.size() > kRequestMax) {
|
|
41
|
+
throw jsi::JSError(rt, std::string(name) + " request length is out of range");
|
|
42
|
+
}
|
|
43
|
+
return request;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Parks `done` as a pending call; the returned completion hands the outcome
|
|
47
|
+
// back on the kernel thread. Like the keystore, the platform may hold the
|
|
48
|
+
// completion past teardown, so it keeps the globals state only weakly.
|
|
49
|
+
PasskeyDone park(
|
|
50
|
+
const std::shared_ptr<KernelGlobalsState>& state,
|
|
51
|
+
const std::shared_ptr<PasskeyCalls>& calls,
|
|
52
|
+
jsi::Function done) {
|
|
53
|
+
auto call = std::make_shared<KernelPendingCall>();
|
|
54
|
+
call->done = std::move(done);
|
|
55
|
+
state->pending.insert(call);
|
|
56
|
+
calls->inFlight += 1;
|
|
57
|
+
std::weak_ptr<KernelGlobalsState> weak = state;
|
|
58
|
+
return [weak, calls, call](PasskeyResult result) {
|
|
59
|
+
auto state = weak.lock();
|
|
60
|
+
if (state == nullptr || !state->active->load(std::memory_order_acquire)) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
auto shared = std::make_shared<PasskeyResult>(std::move(result));
|
|
64
|
+
state->enqueueTask([state, calls, call, shared](jsi::Runtime& rt) {
|
|
65
|
+
calls->inFlight -= 1;
|
|
66
|
+
auto done = std::move(call->done);
|
|
67
|
+
state->pending.erase(call);
|
|
68
|
+
if (!done.has_value()) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (shared->status != PasskeyStatus::ok || shared->responseJson.empty()) {
|
|
72
|
+
const auto status =
|
|
73
|
+
shared->status == PasskeyStatus::ok ? PasskeyStatus::failed : shared->status;
|
|
74
|
+
done->call(
|
|
75
|
+
rt,
|
|
76
|
+
jsi::Value::undefined(),
|
|
77
|
+
jsi::Value(rt, jsi::String::createFromAscii(rt, errorCode(status))));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
done->call(
|
|
81
|
+
rt,
|
|
82
|
+
jsi::Value(rt, jsi::String::createFromUtf8(rt, shared->responseJson)),
|
|
83
|
+
jsi::Value::undefined());
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
jsi::Function callbackFrom(jsi::Runtime& rt, const jsi::Value& value, const char* name) {
|
|
89
|
+
if (!value.isObject() || !value.asObject(rt).isFunction(rt)) {
|
|
90
|
+
throw jsi::JSError(rt, std::string(name) + " expects a completion function");
|
|
91
|
+
}
|
|
92
|
+
return value.asObject(rt).asFunction(rt);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
} // namespace
|
|
96
|
+
|
|
97
|
+
void installKernelPasskey(
|
|
98
|
+
jsi::Runtime& rt,
|
|
99
|
+
jsi::Object& qntx,
|
|
100
|
+
const std::shared_ptr<KernelGlobalsState>& state) {
|
|
101
|
+
auto calls = std::make_shared<PasskeyCalls>();
|
|
102
|
+
state->cancelCeremonies.emplace_back([calls] {
|
|
103
|
+
if (calls->inFlight > 0) {
|
|
104
|
+
passkeyCancel();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
auto passkey = jsi::Object(rt);
|
|
109
|
+
passkey.setProperty(
|
|
110
|
+
rt,
|
|
111
|
+
"create",
|
|
112
|
+
hostFn(rt, "passkey.create", 2, [state, calls](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
|
|
113
|
+
if (count < 2) {
|
|
114
|
+
throw jsi::JSError(rt, "__qntx.passkey.create expects (requestJson, done)");
|
|
115
|
+
}
|
|
116
|
+
auto request = requestFrom(rt, args[0], "__qntx.passkey.create");
|
|
117
|
+
auto done = park(state, calls, callbackFrom(rt, args[1], "__qntx.passkey.create"));
|
|
118
|
+
passkeyCreate(std::move(request), std::move(done));
|
|
119
|
+
return jsi::Value::undefined();
|
|
120
|
+
}));
|
|
121
|
+
passkey.setProperty(
|
|
122
|
+
rt,
|
|
123
|
+
"get",
|
|
124
|
+
hostFn(rt, "passkey.get", 2, [state, calls](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
|
|
125
|
+
if (count < 2) {
|
|
126
|
+
throw jsi::JSError(rt, "__qntx.passkey.get expects (requestJson, done)");
|
|
127
|
+
}
|
|
128
|
+
auto request = requestFrom(rt, args[0], "__qntx.passkey.get");
|
|
129
|
+
auto done = park(state, calls, callbackFrom(rt, args[1], "__qntx.passkey.get"));
|
|
130
|
+
passkeyGet(std::move(request), std::move(done));
|
|
131
|
+
return jsi::Value::undefined();
|
|
132
|
+
}));
|
|
133
|
+
state->intrinsics->freeze.call(rt, jsi::Value(rt, passkey));
|
|
134
|
+
qntx.setProperty(rt, "passkey", jsi::Value(std::move(passkey)));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
} // namespace qntx
|
|
@@ -0,0 +1,23 @@
|
|
|
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 `passkey` object on `__qntx` before the caller freezes
|
|
12
|
+
// it:
|
|
13
|
+
// create(requestJson, done) done(responseJson | undefined, code | undefined)
|
|
14
|
+
// get(requestJson, done) done(responseJson | undefined, code | undefined)
|
|
15
|
+
// The request is a non-empty string of at most 16 KiB. Completions return to
|
|
16
|
+
// the kernel thread through the pending-call machinery; `code` is the kernel
|
|
17
|
+
// error code for a failure. Teardown cancels an in-flight ceremony.
|
|
18
|
+
void installKernelPasskey(
|
|
19
|
+
facebook::jsi::Runtime& rt,
|
|
20
|
+
facebook::jsi::Object& qntx,
|
|
21
|
+
const std::shared_ptr<KernelGlobalsState>& state);
|
|
22
|
+
|
|
23
|
+
} // namespace qntx
|