@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,198 @@
|
|
|
1
|
+
#include "KernelPbkdf2.hpp"
|
|
2
|
+
|
|
3
|
+
#include <stdexcept>
|
|
4
|
+
#include <string>
|
|
5
|
+
|
|
6
|
+
#if defined(__APPLE__)
|
|
7
|
+
#include <CommonCrypto/CommonCryptoError.h>
|
|
8
|
+
#include <CommonCrypto/CommonKeyDerivation.h>
|
|
9
|
+
#endif
|
|
10
|
+
|
|
11
|
+
#if defined(__APPLE__)
|
|
12
|
+
|
|
13
|
+
qntx::SecretBytes qntx::kernelPbkdf2(
|
|
14
|
+
KernelPbkdf2Hash hash,
|
|
15
|
+
const SecretBytes& password,
|
|
16
|
+
const SecretBytes& salt,
|
|
17
|
+
uint32_t iterations,
|
|
18
|
+
size_t length) {
|
|
19
|
+
const CCPseudoRandomAlgorithm algorithm =
|
|
20
|
+
hash == KernelPbkdf2Hash::sha256 ? kCCPRFHmacAlgSHA256 : kCCPRFHmacAlgSHA512;
|
|
21
|
+
SecretBytes out(length);
|
|
22
|
+
const int status = CCKeyDerivationPBKDF(
|
|
23
|
+
kCCPBKDF2,
|
|
24
|
+
reinterpret_cast<const char*>(password.data()),
|
|
25
|
+
password.size(),
|
|
26
|
+
salt.data(),
|
|
27
|
+
salt.size(),
|
|
28
|
+
algorithm,
|
|
29
|
+
iterations,
|
|
30
|
+
out.data(),
|
|
31
|
+
out.size());
|
|
32
|
+
if (status != kCCSuccess) {
|
|
33
|
+
throw std::runtime_error("CCKeyDerivationPBKDF failed: " + std::to_string(status));
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#elif defined(__ANDROID__)
|
|
39
|
+
|
|
40
|
+
namespace {
|
|
41
|
+
|
|
42
|
+
JavaVM* g_vm = nullptr;
|
|
43
|
+
jclass g_class = nullptr;
|
|
44
|
+
jmethodID g_derive = nullptr;
|
|
45
|
+
|
|
46
|
+
// Attaches the current thread to the JVM for the scope's lifetime.
|
|
47
|
+
struct JniScope {
|
|
48
|
+
JNIEnv* env = nullptr;
|
|
49
|
+
bool mustDetach = false;
|
|
50
|
+
|
|
51
|
+
JniScope() {
|
|
52
|
+
if (g_vm == nullptr) {
|
|
53
|
+
throw std::runtime_error("kernelPbkdf2OnLoad was not called");
|
|
54
|
+
}
|
|
55
|
+
if (g_vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) == JNI_EDETACHED) {
|
|
56
|
+
if (g_vm->AttachCurrentThread(&env, nullptr) != JNI_OK) {
|
|
57
|
+
throw std::runtime_error("AttachCurrentThread failed");
|
|
58
|
+
}
|
|
59
|
+
mustDetach = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
~JniScope() {
|
|
63
|
+
if (mustDetach) {
|
|
64
|
+
g_vm->DetachCurrentThread();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
JniScope(const JniScope&) = delete;
|
|
68
|
+
JniScope& operator=(const JniScope&) = delete;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
class JniByteArray {
|
|
72
|
+
public:
|
|
73
|
+
JniByteArray(JNIEnv* env, const qntx::SecretBytes& bytes) : env_(env) {
|
|
74
|
+
array_ = env->NewByteArray(static_cast<jsize>(bytes.size()));
|
|
75
|
+
if (array_ == nullptr) {
|
|
76
|
+
throw std::runtime_error("NewByteArray failed");
|
|
77
|
+
}
|
|
78
|
+
if (bytes.size() > 0) {
|
|
79
|
+
env->SetByteArrayRegion(
|
|
80
|
+
array_, 0, static_cast<jsize>(bytes.size()),
|
|
81
|
+
reinterpret_cast<const jbyte*>(bytes.data()));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
~JniByteArray() {
|
|
85
|
+
if (array_ != nullptr) {
|
|
86
|
+
// Wipe the heap copy before releasing the local ref.
|
|
87
|
+
std::vector<jbyte> zeros(env_->GetArrayLength(array_), 0);
|
|
88
|
+
env_->SetByteArrayRegion(array_, 0, static_cast<jsize>(zeros.size()), zeros.data());
|
|
89
|
+
env_->DeleteLocalRef(array_);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
JniByteArray(const JniByteArray&) = delete;
|
|
93
|
+
JniByteArray& operator=(const JniByteArray&) = delete;
|
|
94
|
+
jbyteArray get() const {
|
|
95
|
+
return array_;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private:
|
|
99
|
+
JNIEnv* env_;
|
|
100
|
+
jbyteArray array_ = nullptr;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
std::string describeJavaException(JNIEnv* env) {
|
|
104
|
+
if (!env->ExceptionCheck()) {
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
jthrowable throwable = env->ExceptionOccurred();
|
|
108
|
+
env->ExceptionClear();
|
|
109
|
+
auto objectClass = env->GetObjectClass(throwable);
|
|
110
|
+
auto toStringId = env->GetMethodID(objectClass, "toString", "()Ljava/lang/String;");
|
|
111
|
+
auto message = static_cast<jstring>(
|
|
112
|
+
env->CallObjectMethod(throwable, toStringId));
|
|
113
|
+
std::string text = "java exception";
|
|
114
|
+
if (message != nullptr) {
|
|
115
|
+
const char* utf = env->GetStringUTFChars(message, nullptr);
|
|
116
|
+
if (utf != nullptr) {
|
|
117
|
+
text = utf;
|
|
118
|
+
env->ReleaseStringUTFChars(message, utf);
|
|
119
|
+
}
|
|
120
|
+
env->DeleteLocalRef(message);
|
|
121
|
+
}
|
|
122
|
+
env->DeleteLocalRef(objectClass);
|
|
123
|
+
env->DeleteLocalRef(throwable);
|
|
124
|
+
return text;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
} // namespace
|
|
128
|
+
|
|
129
|
+
JavaVM* qntx::kernelJavaVM() {
|
|
130
|
+
return g_vm;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
void qntx::kernelPbkdf2OnLoad(JNIEnv* env, const char* className) {
|
|
134
|
+
if (env->GetJavaVM(&g_vm) != JNI_OK) {
|
|
135
|
+
throw std::runtime_error("GetJavaVM failed");
|
|
136
|
+
}
|
|
137
|
+
jclass local = env->FindClass(className);
|
|
138
|
+
if (local == nullptr) {
|
|
139
|
+
env->ExceptionClear();
|
|
140
|
+
throw std::runtime_error(std::string("pbkdf2 helper class not found: ") + className);
|
|
141
|
+
}
|
|
142
|
+
g_class = static_cast<jclass>(env->NewGlobalRef(local));
|
|
143
|
+
env->DeleteLocalRef(local);
|
|
144
|
+
g_derive = env->GetStaticMethodID(
|
|
145
|
+
g_class, "derive", "(Ljava/lang/String;[B[BII)[B");
|
|
146
|
+
if (g_derive == nullptr) {
|
|
147
|
+
env->ExceptionClear();
|
|
148
|
+
throw std::runtime_error("KernelPbkdf2.derive not found");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
qntx::SecretBytes qntx::kernelPbkdf2(
|
|
153
|
+
KernelPbkdf2Hash hash,
|
|
154
|
+
const SecretBytes& password,
|
|
155
|
+
const SecretBytes& salt,
|
|
156
|
+
uint32_t iterations,
|
|
157
|
+
size_t length) {
|
|
158
|
+
JniScope scope;
|
|
159
|
+
JNIEnv* env = scope.env;
|
|
160
|
+
|
|
161
|
+
jstring algorithm = env->NewStringUTF(
|
|
162
|
+
hash == KernelPbkdf2Hash::sha256 ? "HmacSHA256" : "HmacSHA512");
|
|
163
|
+
JniByteArray passwordArray(env, password);
|
|
164
|
+
JniByteArray saltArray(env, salt);
|
|
165
|
+
|
|
166
|
+
auto result = static_cast<jbyteArray>(env->CallStaticObjectMethod(
|
|
167
|
+
g_class,
|
|
168
|
+
g_derive,
|
|
169
|
+
algorithm,
|
|
170
|
+
passwordArray.get(),
|
|
171
|
+
saltArray.get(),
|
|
172
|
+
static_cast<jint>(iterations),
|
|
173
|
+
static_cast<jint>(length)));
|
|
174
|
+
env->DeleteLocalRef(algorithm);
|
|
175
|
+
if (result == nullptr) {
|
|
176
|
+
throw std::runtime_error("pbkdf2 failed: " + describeJavaException(env));
|
|
177
|
+
}
|
|
178
|
+
const jsize size = env->GetArrayLength(result);
|
|
179
|
+
SecretBytes out(static_cast<size_t>(size));
|
|
180
|
+
env->GetByteArrayRegion(result, 0, size, reinterpret_cast<jbyte*>(out.data()));
|
|
181
|
+
std::vector<jbyte> zeros(size, 0);
|
|
182
|
+
env->SetByteArrayRegion(result, 0, size, zeros.data());
|
|
183
|
+
env->DeleteLocalRef(result);
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
#else
|
|
188
|
+
|
|
189
|
+
qntx::SecretBytes qntx::kernelPbkdf2(
|
|
190
|
+
KernelPbkdf2Hash,
|
|
191
|
+
const SecretBytes&,
|
|
192
|
+
const SecretBytes&,
|
|
193
|
+
uint32_t,
|
|
194
|
+
size_t) {
|
|
195
|
+
throw std::runtime_error("kernelPbkdf2: unsupported platform");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#endif
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "../KernelBuffer.hpp"
|
|
4
|
+
|
|
5
|
+
#include <cstdint>
|
|
6
|
+
#include <vector>
|
|
7
|
+
|
|
8
|
+
#ifdef __ANDROID__
|
|
9
|
+
#include <jni.h>
|
|
10
|
+
#endif
|
|
11
|
+
|
|
12
|
+
namespace qntx {
|
|
13
|
+
|
|
14
|
+
enum class KernelPbkdf2Hash { sha256, sha512 };
|
|
15
|
+
|
|
16
|
+
// Synchronous PBKDF2: iOS/macOS via CommonCrypto, Android via JNI into
|
|
17
|
+
// javax.crypto.Mac. Callers run it on the KDF worker thread — never on the
|
|
18
|
+
// kernel thread. Throws std::runtime_error on failure. Inputs and the result
|
|
19
|
+
// are SecretBytes, so any path that drops them still wipes them.
|
|
20
|
+
SecretBytes kernelPbkdf2(
|
|
21
|
+
KernelPbkdf2Hash hash,
|
|
22
|
+
const SecretBytes& password,
|
|
23
|
+
const SecretBytes& salt,
|
|
24
|
+
uint32_t iterations,
|
|
25
|
+
size_t length);
|
|
26
|
+
|
|
27
|
+
#ifdef __ANDROID__
|
|
28
|
+
// Called from the module's JNI_OnLoad while the caller's classloader is
|
|
29
|
+
// available; caches the JavaVM and the Java helper's class/method.
|
|
30
|
+
void kernelPbkdf2OnLoad(JNIEnv* env, const char* className);
|
|
31
|
+
|
|
32
|
+
// The cached JavaVM, or nullptr before kernelPbkdf2OnLoad runs. The kernel
|
|
33
|
+
// thread attaches through this when a probe installs JNI-backed natives
|
|
34
|
+
// (e.g. Nitro's Java-bridged HybridObjectRegistry on Android).
|
|
35
|
+
JavaVM* kernelJavaVM();
|
|
36
|
+
#endif
|
|
37
|
+
|
|
38
|
+
} // namespace qntx
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { AutoLockPolicy, PasskeyConfig, Wallet } from "@qntx/wallet";
|
|
2
|
+
import { MainPort } from "@qntx/wallet/host";
|
|
3
|
+
import { ProtocolValue } from "@qntx/wallet/protocol";
|
|
4
|
+
//#region src/host.d.ts
|
|
5
|
+
type KernelExitReason = "locked" | "crashed" | "integrity" | "background";
|
|
6
|
+
/**
|
|
7
|
+
* Structural mirror of the Nitro `KernelHost` spec (W2b registers the real hybrid object; tests
|
|
8
|
+
* drive the same shape over an in-memory frame pipe).
|
|
9
|
+
*/
|
|
10
|
+
type KernelHostLike = {
|
|
11
|
+
readonly start: (backgroundLockSeconds: number) => void;
|
|
12
|
+
readonly post: (frame: ArrayBuffer) => void;
|
|
13
|
+
readonly onMessage: (cb: (frame: ArrayBuffer) => void) => void;
|
|
14
|
+
readonly onExit: (cb: (reason: KernelExitReason) => void) => void;
|
|
15
|
+
readonly lock: () => void;
|
|
16
|
+
};
|
|
17
|
+
export declare function spawnFromHost(host: KernelHostLike, backgroundLockSeconds: number): MainPort;
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/cbor.d.ts
|
|
20
|
+
export declare const FRAME_MAX_BYTES: number;
|
|
21
|
+
type CborErrorCode = "oversize" | "truncated" | "malformed" | "float" | "unsafe-int" | "bigint" | "undefined" | "tag" | "non-string-key" | "duplicate-key" | "indefinite" | "bad-type";
|
|
22
|
+
export declare class CborError extends Error {
|
|
23
|
+
readonly code: CborErrorCode;
|
|
24
|
+
constructor(code: CborErrorCode, message: string);
|
|
25
|
+
}
|
|
26
|
+
/** Encode a `ProtocolValue` to a CBOR frame, enforcing the 1 MiB cap. */
|
|
27
|
+
export declare function encodeFrame(value: ProtocolValue): ArrayBuffer;
|
|
28
|
+
/** Decode a CBOR frame back to a `ProtocolValue`, enforcing the 1 MiB cap and strictness. */
|
|
29
|
+
export declare function decodeFrame(frame: ArrayBuffer | Uint8Array): ProtocolValue;
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/index.d.ts
|
|
32
|
+
export type NativeWalletOptions = {
|
|
33
|
+
readonly passkey: PasskeyConfig;
|
|
34
|
+
readonly autoLock: AutoLockPolicy;
|
|
35
|
+
/**
|
|
36
|
+
* The Nitro `KernelHost` hybrid object (or a like-shaped fake for tests). Defaults to
|
|
37
|
+
* `NitroModules.createHybridObject("KernelHost")` — the only Nitro-registered object.
|
|
38
|
+
*/
|
|
39
|
+
readonly host?: KernelHostLike;
|
|
40
|
+
};
|
|
41
|
+
export declare function createNativeWallet(options: NativeWalletOptions): Promise<Wallet>;
|
|
42
|
+
//#endregion
|
|
43
|
+
export type { CborErrorCode, KernelExitReason, KernelHostLike };
|
|
44
|
+
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { WalletError } from "@qntx/wallet";
|
|
2
|
+
import { connectKernel } from "@qntx/wallet/host";
|
|
3
|
+
import { decode, encode } from "cborg";
|
|
4
|
+
//#region src/cbor.ts
|
|
5
|
+
/**
|
|
6
|
+
* CBOR framing for the kernel channel. `ProtocolValue` travels main-side ↔ kernel-side as CBOR; the
|
|
7
|
+
* C++ layer moves bytes only. Decode is deliberately strict: floats, unsafe integers, undefined,
|
|
8
|
+
* tags, non-string map keys, duplicate keys and indefinite-length items are all rejected — the wire
|
|
9
|
+
* format carries exactly one value domain.
|
|
10
|
+
*/
|
|
11
|
+
const FRAME_MAX_BYTES = 1048576;
|
|
12
|
+
var CborError = class extends Error {
|
|
13
|
+
code;
|
|
14
|
+
constructor(code, message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "CborError";
|
|
17
|
+
this.code = code;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const DECODE_OPTIONS = {
|
|
21
|
+
strict: true,
|
|
22
|
+
useMaps: false,
|
|
23
|
+
rejectDuplicateMapKeys: true,
|
|
24
|
+
allowUndefined: false,
|
|
25
|
+
allowIndefinite: false,
|
|
26
|
+
allowNaN: false,
|
|
27
|
+
allowInfinity: false,
|
|
28
|
+
allowBigInt: false
|
|
29
|
+
};
|
|
30
|
+
/** Reject non-safe-integer numbers and non-string map keys; cborg flags cover the rest. */
|
|
31
|
+
function validate(value) {
|
|
32
|
+
if (value === null || typeof value === "boolean" || typeof value === "string") return;
|
|
33
|
+
if (typeof value === "number") {
|
|
34
|
+
if (!Number.isInteger(value)) throw new CborError("float", "cbor frame: non-integer number");
|
|
35
|
+
if (!Number.isSafeInteger(value)) throw new CborError("unsafe-int", "cbor frame: integer outside the safe range");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (value instanceof Uint8Array) return;
|
|
39
|
+
if (Array.isArray(value)) {
|
|
40
|
+
for (const item of value) validate(item);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (typeof value === "object") {
|
|
44
|
+
for (const [key, item] of Object.entries(value)) {
|
|
45
|
+
if (typeof key !== "string") throw new CborError("non-string-key", "cbor frame: non-string map key");
|
|
46
|
+
validate(item);
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
throw new CborError("bad-type", `cbor frame: unsupported value type ${typeof value}`);
|
|
51
|
+
}
|
|
52
|
+
/** Classify a cborg rejection into the frame error taxonomy. */
|
|
53
|
+
function classify(error) {
|
|
54
|
+
if (error instanceof CborError) return error;
|
|
55
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
56
|
+
for (const [pattern, code] of [
|
|
57
|
+
[/duplicate key|repeat map key/i, "duplicate-key"],
|
|
58
|
+
[/indefinite/i, "indefinite"],
|
|
59
|
+
[/undefined/i, "undefined"],
|
|
60
|
+
[/bigint|safe integer range/i, "unsafe-int"],
|
|
61
|
+
[/tag/i, "tag"],
|
|
62
|
+
[/string/i, "non-string-key"]
|
|
63
|
+
]) if (pattern.test(text)) return new CborError(code, `cbor frame: ${text}`);
|
|
64
|
+
return new CborError("malformed", `cbor frame: ${text}`);
|
|
65
|
+
}
|
|
66
|
+
/** Encode a `ProtocolValue` to a CBOR frame, enforcing the 1 MiB cap. */
|
|
67
|
+
function encodeFrame(value) {
|
|
68
|
+
validate(value);
|
|
69
|
+
const bytes = encode(value);
|
|
70
|
+
if (bytes.byteLength > 1048576) throw new CborError("oversize", `cbor frame: ${bytes.byteLength} bytes exceeds the 1 MiB cap`);
|
|
71
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
72
|
+
}
|
|
73
|
+
/** Decode a CBOR frame back to a `ProtocolValue`, enforcing the 1 MiB cap and strictness. */
|
|
74
|
+
function decodeFrame(frame) {
|
|
75
|
+
const bytes = frame instanceof Uint8Array ? frame : new Uint8Array(frame);
|
|
76
|
+
if (bytes.byteLength > 1048576) throw new CborError("oversize", `cbor frame: ${bytes.byteLength} bytes exceeds the 1 MiB cap`);
|
|
77
|
+
if (bytes.byteLength === 0) throw new CborError("truncated", "cbor frame: empty frame");
|
|
78
|
+
try {
|
|
79
|
+
const value = decode(bytes, DECODE_OPTIONS);
|
|
80
|
+
validate(value);
|
|
81
|
+
return value;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
throw classify(error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/host.ts
|
|
88
|
+
function spawnFromHost(host, backgroundLockSeconds) {
|
|
89
|
+
const exitHandlers = /* @__PURE__ */ new Set();
|
|
90
|
+
let exited = false;
|
|
91
|
+
const exit = (reason) => {
|
|
92
|
+
if (exited) return;
|
|
93
|
+
exited = true;
|
|
94
|
+
for (const handler of exitHandlers) handler(reason);
|
|
95
|
+
host.lock();
|
|
96
|
+
};
|
|
97
|
+
host.onMessage((frame) => {
|
|
98
|
+
let message;
|
|
99
|
+
try {
|
|
100
|
+
message = decodeFrame(frame);
|
|
101
|
+
} catch {
|
|
102
|
+
exit("crashed");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
dispatch(message);
|
|
106
|
+
});
|
|
107
|
+
host.onExit((reason) => {
|
|
108
|
+
exit(reason === "background" ? "background" : "crashed");
|
|
109
|
+
});
|
|
110
|
+
const messageHandlers = /* @__PURE__ */ new Set();
|
|
111
|
+
const dispatch = (message) => {
|
|
112
|
+
for (const handler of messageHandlers) handler(message);
|
|
113
|
+
};
|
|
114
|
+
host.start(backgroundLockSeconds);
|
|
115
|
+
return {
|
|
116
|
+
post: (message, transfer) => {
|
|
117
|
+
const frame = encodeFrame(message);
|
|
118
|
+
host.post(frame);
|
|
119
|
+
for (const buffer of transfer) new Uint8Array(buffer).fill(0);
|
|
120
|
+
},
|
|
121
|
+
listen: (handler) => {
|
|
122
|
+
messageHandlers.add(handler);
|
|
123
|
+
return () => {
|
|
124
|
+
messageHandlers.delete(handler);
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
onExit: (handler) => {
|
|
128
|
+
exitHandlers.add(handler);
|
|
129
|
+
return () => {
|
|
130
|
+
exitHandlers.delete(handler);
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
terminate: () => {
|
|
134
|
+
exit("crashed");
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/index.ts
|
|
140
|
+
/**
|
|
141
|
+
* `@qntx/wallet-native`: the RN host entry. `createNativeWallet` adapts a `KernelHost` (Nitro
|
|
142
|
+
* hybrid from W2b, or any object with the same shape) to the wallet `connectKernel` entry — the
|
|
143
|
+
* returned `Wallet` is the same type as the web/node host.
|
|
144
|
+
*/
|
|
145
|
+
const setTimer = (delayMs, fn) => {
|
|
146
|
+
const id = setTimeout(fn, delayMs);
|
|
147
|
+
return () => {
|
|
148
|
+
clearTimeout(id);
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
async function defaultHost() {
|
|
152
|
+
const { NitroModules } = await import("react-native-nitro-modules");
|
|
153
|
+
return NitroModules.createHybridObject("KernelHost");
|
|
154
|
+
}
|
|
155
|
+
async function createNativeWallet(options) {
|
|
156
|
+
const { autoLock } = options;
|
|
157
|
+
const host = options.host ?? await defaultHost();
|
|
158
|
+
if (autoLock.platform !== "native") throw new WalletError("invalid-input", "createNativeWallet accepts only the native autoLock policy");
|
|
159
|
+
return connectKernel({
|
|
160
|
+
spawn: () => spawnFromHost(host, autoLock.backgroundSeconds),
|
|
161
|
+
factors: "hardware",
|
|
162
|
+
passkey: options.passkey,
|
|
163
|
+
ceremony: void 0,
|
|
164
|
+
setTimer
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
//#endregion
|
|
168
|
+
export { CborError, FRAME_MAX_BYTES, createNativeWallet, decodeFrame, encodeFrame, spawnFromHost };
|
|
169
|
+
|
|
170
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/cbor.ts","../src/host.ts","../src/index.ts"],"sourcesContent":["/**\n * CBOR framing for the kernel channel. `ProtocolValue` travels main-side ↔ kernel-side as CBOR; the\n * C++ layer moves bytes only. Decode is deliberately strict: floats, unsafe integers, undefined,\n * tags, non-string map keys, duplicate keys and indefinite-length items are all rejected — the wire\n * format carries exactly one value domain.\n */\nimport { decode, encode } from \"cborg\";\nimport type { DecodeOptions } from \"cborg\";\n\nimport type { ProtocolValue } from \"@qntx/wallet/protocol\";\n\nexport const FRAME_MAX_BYTES: number = 1024 * 1024;\n\nexport type CborErrorCode =\n | \"oversize\"\n | \"truncated\"\n | \"malformed\"\n | \"float\"\n | \"unsafe-int\"\n | \"bigint\"\n | \"undefined\"\n | \"tag\"\n | \"non-string-key\"\n | \"duplicate-key\"\n | \"indefinite\"\n | \"bad-type\";\n\nexport class CborError extends Error {\n readonly code: CborErrorCode;\n constructor(code: CborErrorCode, message: string) {\n super(message);\n this.name = \"CborError\";\n this.code = code;\n }\n}\n\nconst DECODE_OPTIONS: DecodeOptions = {\n strict: true,\n useMaps: false,\n rejectDuplicateMapKeys: true,\n allowUndefined: false,\n allowIndefinite: false,\n allowNaN: false,\n allowInfinity: false,\n allowBigInt: false,\n};\n\n/** Reject non-safe-integer numbers and non-string map keys; cborg flags cover the rest. */\nfunction validate(value: unknown): asserts value is ProtocolValue {\n if (value === null || typeof value === \"boolean\" || typeof value === \"string\") {\n return;\n }\n if (typeof value === \"number\") {\n if (!Number.isInteger(value)) {\n throw new CborError(\"float\", \"cbor frame: non-integer number\");\n }\n if (!Number.isSafeInteger(value)) {\n throw new CborError(\"unsafe-int\", \"cbor frame: integer outside the safe range\");\n }\n return;\n }\n if (value instanceof Uint8Array) {\n return;\n }\n if (Array.isArray(value)) {\n for (const item of value) {\n validate(item);\n }\n return;\n }\n if (typeof value === \"object\") {\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the object came from a CBOR map\n for (const [key, item] of Object.entries(value as Record<string, unknown>)) {\n if (typeof key !== \"string\") {\n throw new CborError(\"non-string-key\", \"cbor frame: non-string map key\");\n }\n validate(item);\n }\n return;\n }\n throw new CborError(\"bad-type\", `cbor frame: unsupported value type ${typeof value}`);\n}\n\n/** Classify a cborg rejection into the frame error taxonomy. */\nfunction classify(error: unknown): CborError {\n if (error instanceof CborError) {\n return error;\n }\n const text = error instanceof Error ? error.message : String(error);\n const table: ReadonlyArray<[RegExp, CborErrorCode]> = [\n [/duplicate key|repeat map key/i, \"duplicate-key\"],\n [/indefinite/i, \"indefinite\"],\n [/undefined/i, \"undefined\"],\n [/bigint|safe integer range/i, \"unsafe-int\"],\n [/tag/i, \"tag\"],\n [/string/i, \"non-string-key\"],\n ];\n for (const [pattern, code] of table) {\n if (pattern.test(text)) {\n return new CborError(code, `cbor frame: ${text}`);\n }\n }\n return new CborError(\"malformed\", `cbor frame: ${text}`);\n}\n\n/** Encode a `ProtocolValue` to a CBOR frame, enforcing the 1 MiB cap. */\nexport function encodeFrame(value: ProtocolValue): ArrayBuffer {\n validate(value);\n const bytes = encode(value);\n if (bytes.byteLength > FRAME_MAX_BYTES) {\n throw new CborError(\"oversize\", `cbor frame: ${bytes.byteLength} bytes exceeds the 1 MiB cap`);\n }\n return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);\n}\n\n/** Decode a CBOR frame back to a `ProtocolValue`, enforcing the 1 MiB cap and strictness. */\nexport function decodeFrame(frame: ArrayBuffer | Uint8Array): ProtocolValue {\n const bytes = frame instanceof Uint8Array ? frame : new Uint8Array(frame);\n if (bytes.byteLength > FRAME_MAX_BYTES) {\n throw new CborError(\"oversize\", `cbor frame: ${bytes.byteLength} bytes exceeds the 1 MiB cap`);\n }\n if (bytes.byteLength === 0) {\n throw new CborError(\"truncated\", \"cbor frame: empty frame\");\n }\n try {\n const value: unknown = decode(bytes, DECODE_OPTIONS);\n validate(value);\n return value;\n } catch (error) {\n throw classify(error);\n }\n}\n","/**\n * `KernelHost` JS wrapper: CBOR frames over a Nitro-host-shaped channel. The host moves bytes only\n * — encode/decode happens here and in the kernel entry — so a malformed inbound frame can never\n * reach the message domain: the port exits `integrity` and the host runtime is destroyed.\n */\nimport type { MainPort, PortExit } from \"@qntx/wallet/host\";\nimport type { ProtocolValue } from \"@qntx/wallet/protocol\";\n\nimport { decodeFrame, encodeFrame } from \"./cbor.ts\";\n\nexport type KernelExitReason = \"locked\" | \"crashed\" | \"integrity\" | \"background\";\n\n/**\n * Structural mirror of the Nitro `KernelHost` spec (W2b registers the real hybrid object; tests\n * drive the same shape over an in-memory frame pipe).\n */\nexport type KernelHostLike = {\n readonly start: (backgroundLockSeconds: number) => void;\n readonly post: (frame: ArrayBuffer) => void;\n readonly onMessage: (cb: (frame: ArrayBuffer) => void) => void;\n readonly onExit: (cb: (reason: KernelExitReason) => void) => void;\n readonly lock: () => void;\n};\n\nexport function spawnFromHost(host: KernelHostLike, backgroundLockSeconds: number): MainPort {\n const exitHandlers = new Set<(reason: PortExit) => void>();\n let exited = false;\n\n const exit = (reason: PortExit): void => {\n if (exited) {\n return;\n }\n exited = true;\n for (const handler of exitHandlers) {\n handler(reason);\n }\n host.lock();\n };\n\n host.onMessage((frame) => {\n let message: ProtocolValue;\n try {\n message = decodeFrame(frame);\n } catch {\n exit(\"crashed\");\n return;\n }\n dispatch(message);\n });\n\n // The platform locked the enclave in the background — report it honestly so\n // the channel takes the lock path, not the crash path.\n host.onExit((reason) => {\n exit(reason === \"background\" ? \"background\" : \"crashed\");\n });\n\n const messageHandlers = new Set<(message: unknown) => void>();\n const dispatch = (message: unknown): void => {\n for (const handler of messageHandlers) {\n handler(message);\n }\n };\n\n host.start(backgroundLockSeconds);\n\n return {\n post: (message, transfer) => {\n const frame = encodeFrame(message);\n host.post(frame);\n // Structured clone would neuter transferred buffers on the web; the main↔kernel\n // contract is the same here — the caller's buffers hold secrets (passwords, PRF\n // output) and are zeroed once their bytes are inside the frame.\n for (const buffer of transfer) {\n new Uint8Array(buffer).fill(0);\n }\n },\n listen: (handler) => {\n messageHandlers.add(handler);\n return () => {\n messageHandlers.delete(handler);\n };\n },\n onExit: (handler) => {\n exitHandlers.add(handler);\n return () => {\n exitHandlers.delete(handler);\n };\n },\n terminate: () => {\n exit(\"crashed\");\n },\n };\n}\n","import { WalletError } from \"@qntx/wallet\";\nimport type { AutoLockPolicy, PasskeyConfig, Wallet } from \"@qntx/wallet\";\nimport type { SetTimer } from \"@qntx/wallet/host\";\n/**\n * `@qntx/wallet-native`: the RN host entry. `createNativeWallet` adapts a `KernelHost` (Nitro\n * hybrid from W2b, or any object with the same shape) to the wallet `connectKernel` entry — the\n * returned `Wallet` is the same type as the web/node host.\n */\nimport { connectKernel } from \"@qntx/wallet/host\";\n\nimport { spawnFromHost } from \"./host.ts\";\nimport type { KernelHostLike } from \"./host.ts\";\nimport type { KernelHost } from \"./specs/kernel-host.nitro.ts\";\n\nexport type NativeWalletOptions = {\n readonly passkey: PasskeyConfig;\n readonly autoLock: AutoLockPolicy;\n /**\n * The Nitro `KernelHost` hybrid object (or a like-shaped fake for tests). Defaults to\n * `NitroModules.createHybridObject(\"KernelHost\")` — the only Nitro-registered object.\n */\n readonly host?: KernelHostLike;\n};\n\nconst setTimer: SetTimer = (delayMs, fn) => {\n const id = setTimeout(fn, delayMs);\n return () => {\n clearTimeout(id);\n };\n};\n\nasync function defaultHost(): Promise<KernelHostLike> {\n const { NitroModules } = await import(\"react-native-nitro-modules\");\n return NitroModules.createHybridObject<KernelHost>(\"KernelHost\");\n}\n\nexport async function createNativeWallet(options: NativeWalletOptions): Promise<Wallet> {\n const { autoLock } = options;\n const host = options.host ?? (await defaultHost());\n if (autoLock.platform !== \"native\") {\n throw new WalletError(\n \"invalid-input\",\n \"createNativeWallet accepts only the native autoLock policy\",\n );\n }\n // The native host owns the auto-lock signal: the platform layer calls host.lock() on\n // background expiry — the kernel never sees a presence source here.\n return connectKernel({\n spawn: () => spawnFromHost(host, autoLock.backgroundSeconds),\n factors: \"hardware\",\n passkey: options.passkey,\n ceremony: undefined,\n setTimer,\n });\n}\n\nexport { spawnFromHost, type KernelHostLike, type KernelExitReason } from \"./host.ts\";\nexport {\n encodeFrame,\n decodeFrame,\n CborError,\n FRAME_MAX_BYTES,\n type CborErrorCode,\n} from \"./cbor.ts\";\n"],"mappings":"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"}
|