@qntx/wallet-native 0.0.0-stage → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/LICENSE-APACHE +201 -0
  2. package/LICENSE-MIT +21 -0
  3. package/README.md +123 -2
  4. package/WalletNative.podspec +74 -0
  5. package/android/CMakeLists.txt +99 -0
  6. package/android/build.gradle +177 -0
  7. package/android/consumer-rules.pro +4 -0
  8. package/android/src/main/AndroidManifest.xml +2 -0
  9. package/android/src/main/cpp/cpp-adapter.cpp +29 -0
  10. package/android/src/main/java/com/margelo/nitro/qntxwallet/AppLifecycle.kt +35 -0
  11. package/android/src/main/java/com/margelo/nitro/qntxwallet/KernelPbkdf2.java +59 -0
  12. package/android/src/main/java/com/margelo/nitro/qntxwallet/Keystore.kt +280 -0
  13. package/android/src/main/java/com/margelo/nitro/qntxwallet/Passkey.kt +147 -0
  14. package/android/src/main/java/com/margelo/nitro/qntxwallet/VaultDirectory.kt +28 -0
  15. package/android/src/main/java/com/margelo/nitro/qntxwallet/WalletNativePackage.java +30 -0
  16. package/cpp/host/HybridKernelHost.cpp +162 -0
  17. package/cpp/host/HybridKernelHost.hpp +49 -0
  18. package/cpp/host/KernelFile.cpp +120 -0
  19. package/cpp/host/KernelFile.hpp +24 -0
  20. package/cpp/host/Keystore.hpp +70 -0
  21. package/cpp/host/Lifecycle.cpp +77 -0
  22. package/cpp/host/Lifecycle.hpp +71 -0
  23. package/cpp/host/Passkey.hpp +39 -0
  24. package/cpp/host/VaultDirectory.hpp +16 -0
  25. package/cpp/platform/KeystoreAndroid.cpp +175 -0
  26. package/cpp/platform/KeystoreIos.mm +300 -0
  27. package/cpp/platform/LifecycleAndroid.cpp +55 -0
  28. package/cpp/platform/LifecycleIos.mm +36 -0
  29. package/cpp/platform/PasskeyAndroid.cpp +118 -0
  30. package/cpp/platform/PasskeyIos.mm +20 -0
  31. package/cpp/platform/VaultDirectory.mm +58 -0
  32. package/cpp/platform/VaultDirectoryAndroid.cpp +35 -0
  33. package/cpp/runtime/KernelBackends.cpp +297 -0
  34. package/cpp/runtime/KernelBackends.hpp +20 -0
  35. package/cpp/runtime/KernelBuffer.cpp +12 -0
  36. package/cpp/runtime/KernelBuffer.hpp +96 -0
  37. package/cpp/runtime/KernelGlobals.cpp +535 -0
  38. package/cpp/runtime/KernelGlobals.hpp +107 -0
  39. package/cpp/runtime/KernelJs.cpp +155 -0
  40. package/cpp/runtime/KernelJs.hpp +81 -0
  41. package/cpp/runtime/KernelKeystore.cpp +224 -0
  42. package/cpp/runtime/KernelKeystore.hpp +28 -0
  43. package/cpp/runtime/KernelPasskey.cpp +137 -0
  44. package/cpp/runtime/KernelPasskey.hpp +23 -0
  45. package/cpp/runtime/KernelRuntime.cpp +496 -0
  46. package/cpp/runtime/KernelRuntime.hpp +94 -0
  47. package/cpp/runtime/platform/KernelPbkdf2.cpp +198 -0
  48. package/cpp/runtime/platform/KernelPbkdf2.hpp +38 -0
  49. package/dist/index.d.mts +44 -0
  50. package/dist/index.mjs +170 -0
  51. package/dist/index.mjs.map +1 -0
  52. package/kernel/generated/kernel-entry.js +42967 -0
  53. package/kernel/generated/kernel.manifest.json +80 -0
  54. package/nitrogen/generated/.gitattributes +1 -0
  55. package/nitrogen/generated/android/WalletNative+autolinking.cmake +81 -0
  56. package/nitrogen/generated/android/WalletNative+autolinking.gradle +27 -0
  57. package/nitrogen/generated/android/WalletNativeOnLoad.cpp +49 -0
  58. package/nitrogen/generated/android/WalletNativeOnLoad.hpp +34 -0
  59. package/nitrogen/generated/android/kotlin/com/margelo/nitro/qntxwallet/WalletNativeOnLoad.kt +35 -0
  60. package/nitrogen/generated/ios/WalletNative+autolinking.rb +62 -0
  61. package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Bridge.cpp +17 -0
  62. package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Bridge.hpp +27 -0
  63. package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Umbrella.hpp +38 -0
  64. package/nitrogen/generated/ios/WalletNativeAutolinking.mm +35 -0
  65. package/nitrogen/generated/ios/WalletNativeAutolinking.swift +16 -0
  66. package/nitrogen/generated/shared/c++/HybridKernelHostSpec.cpp +25 -0
  67. package/nitrogen/generated/shared/c++/HybridKernelHostSpec.hpp +69 -0
  68. package/nitrogen/generated/shared/c++/KernelExitReason.hpp +84 -0
  69. package/package.json +65 -4
  70. package/react-native.config.js +14 -0
  71. package/scripts/compile-kernel.mjs +118 -0
@@ -0,0 +1,55 @@
1
+ // Android background-lock lifecycle: the Kotlin AppLifecycle object registers
2
+ // a ProcessLifecycleOwner observer on the main looper; its DefaultLifecycleObserver
3
+ // callbacks reach the dispatcher through the JNI exports below.
4
+ #include "../host/Lifecycle.hpp"
5
+
6
+ #ifdef __ANDROID__
7
+
8
+ #include <fbjni/fbjni.h>
9
+ #include <jni.h>
10
+ #include <stdexcept>
11
+
12
+ namespace qntx {
13
+ namespace {
14
+ facebook::jni::global_ref<jclass> g_appLifecycleClass;
15
+ } // namespace
16
+
17
+ void kernelLifecycleInit(facebook::jni::alias_ref<jclass> clazz) {
18
+ g_appLifecycleClass = facebook::jni::make_global(clazz);
19
+ }
20
+
21
+ int64_t sleepInclusiveMonotonicMs() {
22
+ // CLOCK_BOOTTIME counts suspend; CLOCK_MONOTONIC does not.
23
+ struct timespec ts;
24
+ ::clock_gettime(CLOCK_BOOTTIME, &ts);
25
+ return static_cast<int64_t>(ts.tv_sec) * 1000 + ts.tv_nsec / 1000000;
26
+ }
27
+
28
+ void installLifecycleObserver() {
29
+ static std::once_flag once;
30
+ std::call_once(once, [] {
31
+ facebook::jni::ThreadScope threadScope;
32
+ if (!g_appLifecycleClass) {
33
+ throw std::runtime_error("lifecycle: JNI_OnLoad init did not run");
34
+ }
35
+ auto install = g_appLifecycleClass->getStaticMethod<void()>("install");
36
+ install(g_appLifecycleClass.get());
37
+ });
38
+ }
39
+
40
+ } // namespace qntx
41
+
42
+ // The Kotlin object's DefaultLifecycleObserver dispatches here.
43
+ extern "C" JNIEXPORT void JNICALL
44
+ Java_com_margelo_nitro_qntxwallet_AppLifecycle_onAppBackground(
45
+ JNIEnv*, jclass) {
46
+ qntx::KernelLifecycleDispatcher::shared().onBackground();
47
+ }
48
+
49
+ extern "C" JNIEXPORT void JNICALL
50
+ Java_com_margelo_nitro_qntxwallet_AppLifecycle_onAppForeground(
51
+ JNIEnv*, jclass) {
52
+ qntx::KernelLifecycleDispatcher::shared().onForeground();
53
+ }
54
+
55
+ #endif // __ANDROID__
@@ -0,0 +1,36 @@
1
+ #import <Foundation/Foundation.h>
2
+ #import <UIKit/UIKit.h>
3
+
4
+ #include <mutex>
5
+ #include <time.h>
6
+
7
+ #include "../host/Lifecycle.hpp"
8
+
9
+ namespace qntx {
10
+
11
+ int64_t sleepInclusiveMonotonicMs() {
12
+ // CLOCK_MONOTONIC counts sleep (steady_clock/mach_absolute_time does not).
13
+ const uint64_t ns = clock_gettime_nsec_np(CLOCK_MONOTONIC);
14
+ return static_cast<int64_t>(ns / 1000000);
15
+ }
16
+
17
+ void installLifecycleObserver() {
18
+ static std::once_flag once;
19
+ std::call_once(once, [] {
20
+ auto* center = NSNotificationCenter.defaultCenter;
21
+ [center addObserverForName:UIApplicationDidEnterBackgroundNotification
22
+ object:nil
23
+ queue:nil
24
+ usingBlock:^(NSNotification*) {
25
+ KernelLifecycleDispatcher::shared().onBackground();
26
+ }];
27
+ [center addObserverForName:UIApplicationWillEnterForegroundNotification
28
+ object:nil
29
+ queue:nil
30
+ usingBlock:^(NSNotification*) {
31
+ KernelLifecycleDispatcher::shared().onForeground();
32
+ }];
33
+ });
34
+ }
35
+
36
+ } // namespace qntx
@@ -0,0 +1,118 @@
1
+ // Android passkey authenticator: the Kotlin Passkey object runs Credential
2
+ // Manager; this file forwards each request JSON through fbjni with a
3
+ // completion token and resolves the parked completion when Kotlin calls back
4
+ // through nativeComplete (on the main thread).
5
+ #include "../host/Passkey.hpp"
6
+
7
+ #ifdef __ANDROID__
8
+
9
+ #include <fbjni/fbjni.h>
10
+ #include <jni.h>
11
+
12
+ #include <cstdint>
13
+ #include <mutex>
14
+ #include <stdexcept>
15
+ #include <unordered_map>
16
+ #include <utility>
17
+
18
+ namespace qntx {
19
+ namespace {
20
+
21
+ namespace jni = facebook::jni;
22
+
23
+ jni::global_ref<jclass> g_passkeyClass;
24
+
25
+ std::mutex g_mutex;
26
+ int64_t g_nextToken = 1;
27
+ std::unordered_map<int64_t, PasskeyDone> g_pending;
28
+
29
+ int64_t park(PasskeyDone done) {
30
+ std::lock_guard<std::mutex> lock(g_mutex);
31
+ const int64_t token = g_nextToken++;
32
+ g_pending.emplace(token, std::move(done));
33
+ return token;
34
+ }
35
+
36
+ PasskeyDone take(int64_t token) {
37
+ std::lock_guard<std::mutex> lock(g_mutex);
38
+ auto found = g_pending.find(token);
39
+ if (found == g_pending.end()) {
40
+ return nullptr;
41
+ }
42
+ auto done = std::move(found->second);
43
+ g_pending.erase(found);
44
+ return done;
45
+ }
46
+
47
+ jni::alias_ref<jclass> passkeyClass() {
48
+ if (!g_passkeyClass) {
49
+ throw std::runtime_error("passkey: JNI_OnLoad init did not run");
50
+ }
51
+ return g_passkeyClass;
52
+ }
53
+
54
+ // Runs one Kotlin entry point; a JNI failure completes the call as `failed`
55
+ // instead of leaving it parked forever.
56
+ void dispatch(const char* method, const std::string& requestJson, PasskeyDone done) {
57
+ const int64_t token = park(std::move(done));
58
+ try {
59
+ jni::ThreadScope threadScope;
60
+ auto entry = passkeyClass()->getStaticMethod<void(jlong, jni::alias_ref<jstring>)>(method);
61
+ entry(passkeyClass(), static_cast<jlong>(token), jni::make_jstring(requestJson));
62
+ } catch (...) {
63
+ if (auto parked = take(token)) {
64
+ parked(PasskeyResult{PasskeyStatus::failed, {}});
65
+ }
66
+ }
67
+ }
68
+
69
+ } // namespace
70
+
71
+ void kernelPasskeyInit(jni::alias_ref<jclass> clazz) {
72
+ g_passkeyClass = jni::make_global(clazz);
73
+ }
74
+
75
+ void passkeyCreate(std::string requestJson, PasskeyDone done) {
76
+ dispatch("create", requestJson, std::move(done));
77
+ }
78
+
79
+ void passkeyGet(std::string requestJson, PasskeyDone done) {
80
+ dispatch("get", requestJson, std::move(done));
81
+ }
82
+
83
+ void passkeyCancel() {
84
+ jni::ThreadScope threadScope;
85
+ auto method = passkeyClass()->getStaticMethod<void()>("cancel");
86
+ method(passkeyClass());
87
+ }
88
+
89
+ } // namespace qntx
90
+
91
+ extern "C" JNIEXPORT void JNICALL
92
+ Java_com_margelo_nitro_qntxwallet_Passkey_nativeComplete(
93
+ JNIEnv* env, jclass, jlong token, jint status, jstring responseJson) {
94
+ auto done = qntx::take(static_cast<int64_t>(token));
95
+ if (!done) {
96
+ return;
97
+ }
98
+ qntx::PasskeyResult result;
99
+ switch (status) {
100
+ case static_cast<jint>(qntx::PasskeyStatus::ok):
101
+ case static_cast<jint>(qntx::PasskeyStatus::cancelled):
102
+ case static_cast<jint>(qntx::PasskeyStatus::unavailable):
103
+ result.status = static_cast<qntx::PasskeyStatus>(status);
104
+ break;
105
+ default:
106
+ result.status = qntx::PasskeyStatus::failed;
107
+ }
108
+ if (responseJson != nullptr && result.status == qntx::PasskeyStatus::ok) {
109
+ const char* utf = env->GetStringUTFChars(responseJson, nullptr);
110
+ if (utf != nullptr) {
111
+ result.responseJson = utf;
112
+ env->ReleaseStringUTFChars(responseJson, utf);
113
+ }
114
+ }
115
+ done(std::move(result));
116
+ }
117
+
118
+ #endif // __ANDROID__
@@ -0,0 +1,20 @@
1
+ // iOS passkey authenticator: not available yet. Native passkeys need the
2
+ // app's associated domain (webcredentials), which the current free signing
3
+ // team cannot provision; W6c implements ASAuthorization with PRF once a paid
4
+ // team exists. Until then every ceremony reports `unavailable`, which the
5
+ // kernel surfaces as factor-unavailable (no passkey wallet on this platform).
6
+ #include "../host/Passkey.hpp"
7
+
8
+ namespace qntx {
9
+
10
+ void passkeyCreate(std::string, PasskeyDone done) {
11
+ done(PasskeyResult{PasskeyStatus::unavailable, {}});
12
+ }
13
+
14
+ void passkeyGet(std::string, PasskeyDone done) {
15
+ done(PasskeyResult{PasskeyStatus::unavailable, {}});
16
+ }
17
+
18
+ void passkeyCancel() {}
19
+
20
+ } // namespace qntx
@@ -0,0 +1,58 @@
1
+ // iOS vault directory: Application Support/qntx-wallet/ created with
2
+ // NSFileProtectionComplete and excluded from backup — the vault image must
3
+ // never ride into iCloud or unencrypted backups.
4
+ #include "../host/VaultDirectory.hpp"
5
+
6
+ #import <Foundation/Foundation.h>
7
+
8
+ #include <stdexcept>
9
+
10
+ namespace qntx {
11
+ namespace {
12
+
13
+ // NSError detail is reduced to its numeric code: failure messages propagate
14
+ // through the kernel's error path and must never carry user-data paths.
15
+ [[noreturn]] void failStep(const char* step, NSError* error) {
16
+ if (error == nil) {
17
+ throw std::runtime_error(step);
18
+ }
19
+ throw std::runtime_error(
20
+ std::string(step) + " (Cocoa error " + std::to_string(error.code) + ")");
21
+ }
22
+
23
+ } // namespace
24
+
25
+ std::string platformVaultDirectory() {
26
+ NSFileManager* fileManager = [NSFileManager defaultManager];
27
+ NSError* error = nil;
28
+ NSURL* appSupport = [fileManager URLForDirectory:NSApplicationSupportDirectory
29
+ inDomain:NSUserDomainMask
30
+ appropriateForURL:nil
31
+ create:YES
32
+ error:&error];
33
+ if (appSupport == nil) {
34
+ failStep("resolve Application Support directory", error);
35
+ }
36
+ NSURL* dir = [appSupport URLByAppendingPathComponent:@"qntx-wallet" isDirectory:YES];
37
+ if (dir == nil) {
38
+ throw std::runtime_error("resolve vault directory");
39
+ }
40
+ error = nil;
41
+ if (![fileManager createDirectoryAtURL:dir
42
+ withIntermediateDirectories:YES
43
+ attributes:@{NSFileProtectionKey : NSFileProtectionComplete}
44
+ error:&error]) {
45
+ failStep("create vault directory", error);
46
+ }
47
+ error = nil;
48
+ if (![dir setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:&error]) {
49
+ failStep("exclude vault directory from backup", error);
50
+ }
51
+ const char* path = dir.fileSystemRepresentation;
52
+ if (path == nullptr) {
53
+ throw std::runtime_error("resolve vault directory path");
54
+ }
55
+ return path;
56
+ }
57
+
58
+ } // namespace qntx
@@ -0,0 +1,35 @@
1
+ // Android vault directory: <noBackupFilesDir>/qntx-wallet, pulled from the
2
+ // Kotlin VaultDirectory.resolve() through fbjni — one supported-API path, no
3
+ // C++-side caching of the path.
4
+ #include "../host/VaultDirectory.hpp"
5
+
6
+ #ifdef __ANDROID__
7
+
8
+ #include <fbjni/fbjni.h>
9
+ #include <jni.h>
10
+ #include <stdexcept>
11
+ #include <string>
12
+
13
+ namespace qntx {
14
+ namespace {
15
+ facebook::jni::global_ref<jclass> g_vaultDirectoryClass;
16
+ } // namespace
17
+
18
+ void kernelVaultDirectoryInit(facebook::jni::alias_ref<jclass> clazz) {
19
+ g_vaultDirectoryClass = facebook::jni::make_global(clazz);
20
+ }
21
+
22
+ std::string platformVaultDirectory() {
23
+ facebook::jni::ThreadScope threadScope;
24
+ if (!g_vaultDirectoryClass) {
25
+ throw std::runtime_error("vault directory: JNI_OnLoad init did not run");
26
+ }
27
+ auto method = g_vaultDirectoryClass->getStaticMethod<facebook::jni::JString::javaobject()>(
28
+ "resolve");
29
+ auto path = method(g_vaultDirectoryClass.get());
30
+ return path->toStdString();
31
+ }
32
+
33
+ } // namespace qntx
34
+
35
+ #endif // __ANDROID__
@@ -0,0 +1,297 @@
1
+ #include "KernelBackends.hpp"
2
+
3
+ #include "KernelJs.hpp"
4
+
5
+ // qntx_spark.h carries its own extern "C" guard since 0.5.1. Every build
6
+ // resolves the bare include through a search path: iOS via the SparkNative
7
+ // pod's headers (added by the WalletNative podspec), Android and the host
8
+ // test via the package's include/ directory.
9
+ #include "qntx_spark.h"
10
+ #include "nostr_native.h"
11
+
12
+ #include <string>
13
+
14
+ namespace qntx {
15
+
16
+ namespace {
17
+
18
+ constexpr double kUint32Max = 4294967295.0;
19
+
20
+ // Every qs_* call shares this shape: byte inputs go in as borrowed (data,
21
+ // size) views; the library-owned output is copied into a fresh Uint8Array
22
+ // before qs_free wipes and releases it. A nonzero code throws
23
+ // `qntx_spark:<code>` — the kernel-side wrapper maps it to SparkCryptoError.
24
+ template <typename Invoke>
25
+ jsi::Value callOut(
26
+ jsi::Runtime& rt,
27
+ const KernelIntrinsics& intrinsics,
28
+ Invoke&& invoke) {
29
+ uint8_t* out = nullptr;
30
+ size_t outLen = 0;
31
+ const int32_t code = invoke(&out, &outLen);
32
+ jsi::Value result;
33
+ if (code == 0) {
34
+ auto buffer = arrayBufferFromBytes(rt, out, outLen);
35
+ result = jsi::Value(
36
+ rt, makeUint8Array(rt, intrinsics, std::move(buffer)));
37
+ }
38
+ if (out != nullptr) {
39
+ qs_free(out, outLen);
40
+ }
41
+ if (code != 0) {
42
+ throw jsi::JSError(rt, "qntx_spark:" + std::to_string(code));
43
+ }
44
+ return result;
45
+ }
46
+
47
+ uint32_t uint32Arg(
48
+ jsi::Runtime& rt,
49
+ const jsi::Value& value,
50
+ const char* name) {
51
+ return static_cast<uint32_t>(integerInRange(rt, value, name, 0, kUint32Max));
52
+ }
53
+
54
+ } // namespace
55
+
56
+ void installKernelBackends(
57
+ jsi::Runtime& rt,
58
+ jsi::Object& qntx,
59
+ const KernelIntrinsics& intrinsics) {
60
+ auto spark = jsi::Object(rt);
61
+
62
+ spark.setProperty(
63
+ rt, "abiVersion", jsi::Value(static_cast<double>(qs_abi_version())));
64
+
65
+ spark.setProperty(
66
+ rt,
67
+ "frostCommit",
68
+ hostFn(rt, "spark.frostCommit", 2, [&intrinsics](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
69
+ if (count < 2) {
70
+ throw jsi::JSError(
71
+ rt, "__qntx.backends.spark.frostCommit expects (hedge, random)");
72
+ }
73
+ auto hedge = byteViewFromValue(rt, args[0], "__qntx.backends.spark.frostCommit hedge", intrinsics);
74
+ auto random = byteViewFromValue(rt, args[1], "__qntx.backends.spark.frostCommit random", intrinsics);
75
+ return callOut(rt, intrinsics, [&](uint8_t** out, size_t* outLen) {
76
+ return qs_frost_commit(
77
+ hedge.data, hedge.size, random.data, random.size, out, outLen);
78
+ });
79
+ }));
80
+
81
+ spark.setProperty(
82
+ rt,
83
+ "frostSign",
84
+ hostFn(rt, "spark.frostSign", 1, [&intrinsics](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
85
+ if (count < 1) {
86
+ throw jsi::JSError(
87
+ rt, "__qntx.backends.spark.frostSign expects (request)");
88
+ }
89
+ auto request = byteViewFromValue(rt, args[0], "__qntx.backends.spark.frostSign request", intrinsics);
90
+ return callOut(rt, intrinsics, [&](uint8_t** out, size_t* outLen) {
91
+ return qs_frost_sign(request.data, request.size, out, outLen);
92
+ });
93
+ }));
94
+
95
+ spark.setProperty(
96
+ rt,
97
+ "frostAggregate",
98
+ hostFn(rt, "spark.frostAggregate", 1, [&intrinsics](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
99
+ if (count < 1) {
100
+ throw jsi::JSError(
101
+ rt, "__qntx.backends.spark.frostAggregate expects (request)");
102
+ }
103
+ auto request = byteViewFromValue(rt, args[0], "__qntx.backends.spark.frostAggregate request", intrinsics);
104
+ return callOut(rt, intrinsics, [&](uint8_t** out, size_t* outLen) {
105
+ return qs_frost_aggregate(request.data, request.size, out, outLen);
106
+ });
107
+ }));
108
+
109
+ spark.setProperty(
110
+ rt,
111
+ "eciesEncrypt",
112
+ hostFn(rt, "spark.eciesEncrypt", 3, [&intrinsics](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
113
+ if (count < 3) {
114
+ throw jsi::JSError(
115
+ rt, "__qntx.backends.spark.eciesEncrypt expects (publicKey, plaintext, random)");
116
+ }
117
+ auto publicKey = byteViewFromValue(rt, args[0], "__qntx.backends.spark.eciesEncrypt publicKey", intrinsics);
118
+ auto plaintext = byteViewFromValue(rt, args[1], "__qntx.backends.spark.eciesEncrypt plaintext", intrinsics);
119
+ auto random = byteViewFromValue(rt, args[2], "__qntx.backends.spark.eciesEncrypt random", intrinsics);
120
+ return callOut(rt, intrinsics, [&](uint8_t** out, size_t* outLen) {
121
+ return qs_ecies_encrypt(
122
+ publicKey.data,
123
+ publicKey.size,
124
+ plaintext.data,
125
+ plaintext.size,
126
+ random.data,
127
+ random.size,
128
+ out,
129
+ outLen);
130
+ });
131
+ }));
132
+
133
+ spark.setProperty(
134
+ rt,
135
+ "eciesDecrypt",
136
+ hostFn(rt, "spark.eciesDecrypt", 2, [&intrinsics](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
137
+ if (count < 2) {
138
+ throw jsi::JSError(
139
+ rt, "__qntx.backends.spark.eciesDecrypt expects (privateKey, ciphertext)");
140
+ }
141
+ auto privateKey = byteViewFromValue(rt, args[0], "__qntx.backends.spark.eciesDecrypt privateKey", intrinsics);
142
+ auto ciphertext = byteViewFromValue(rt, args[1], "__qntx.backends.spark.eciesDecrypt ciphertext", intrinsics);
143
+ return callOut(rt, intrinsics, [&](uint8_t** out, size_t* outLen) {
144
+ return qs_ecies_decrypt(
145
+ privateKey.data, privateKey.size, ciphertext.data, ciphertext.size, out, outLen);
146
+ });
147
+ }));
148
+
149
+ spark.setProperty(
150
+ rt,
151
+ "splitSecretWithProofs",
152
+ hostFn(rt, "spark.splitSecretWithProofs", 4, [&intrinsics](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
153
+ if (count < 4) {
154
+ throw jsi::JSError(
155
+ rt, "__qntx.backends.spark.splitSecretWithProofs expects (secret, threshold, shares, random)");
156
+ }
157
+ auto secret = byteViewFromValue(rt, args[0], "__qntx.backends.spark.splitSecretWithProofs secret", intrinsics);
158
+ const auto threshold =
159
+ uint32Arg(rt, args[1], "__qntx.backends.spark.splitSecretWithProofs threshold");
160
+ const auto shares =
161
+ uint32Arg(rt, args[2], "__qntx.backends.spark.splitSecretWithProofs shares");
162
+ auto random = byteViewFromValue(rt, args[3], "__qntx.backends.spark.splitSecretWithProofs random", intrinsics);
163
+ return callOut(rt, intrinsics, [&](uint8_t** out, size_t* outLen) {
164
+ return qs_split_secret_with_proofs(
165
+ secret.data,
166
+ secret.size,
167
+ threshold,
168
+ shares,
169
+ random.data,
170
+ random.size,
171
+ out,
172
+ outLen);
173
+ });
174
+ }));
175
+
176
+ intrinsics.freeze.call(rt, jsi::Value(rt, spark));
177
+
178
+ // The nostr_native_* calls write fixed-size outputs on the stack and return
179
+ // 0/1 rather than an allocator + error code, so they do not go through
180
+ // callOut. Exact-length inputs are checked before the call; verifySerialized
181
+ // answers with a boolean like nobleBackend instead of throwing.
182
+ auto nostr = jsi::Object(rt);
183
+ nostr.setProperty(
184
+ rt, "abiVersion", jsi::Value(static_cast<double>(nostr_native_abi_version())));
185
+
186
+ auto exact32 = [](jsi::Runtime& rt, const ByteView& view, const char* name) {
187
+ if (view.size != 32) {
188
+ throw jsi::JSError(rt, std::string(name) + " expects a 32-byte view");
189
+ }
190
+ };
191
+ auto wipe = [](uint8_t* data, size_t size) {
192
+ volatile uint8_t* p = data;
193
+ while (size--) {
194
+ *p++ = 0;
195
+ }
196
+ };
197
+
198
+ nostr.setProperty(
199
+ rt,
200
+ "publicKey",
201
+ hostFn(rt, "nostr.publicKey", 1, [&intrinsics, &exact32, &wipe](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
202
+ if (count < 1) {
203
+ throw jsi::JSError(rt, "__qntx.backends.nostr.publicKey expects (secretKey)");
204
+ }
205
+ auto sk = byteViewFromValue(rt, args[0], "__qntx.backends.nostr.publicKey secretKey", intrinsics);
206
+ exact32(rt, sk, "__qntx.backends.nostr.publicKey secretKey");
207
+ uint8_t out[32];
208
+ const int ok = nostr_native_public_key(sk.data, out);
209
+ jsi::Value result;
210
+ if (ok == 1) {
211
+ result = jsi::Value(rt, makeUint8Array(rt, intrinsics, arrayBufferFromBytes(rt, out, sizeof out)));
212
+ }
213
+ wipe(out, sizeof out);
214
+ if (ok != 1) {
215
+ throw jsi::JSError(rt, "nostr_native: publicKey rejected the input");
216
+ }
217
+ return result;
218
+ }));
219
+
220
+ nostr.setProperty(
221
+ rt,
222
+ "sign",
223
+ hostFn(rt, "nostr.sign", 3, [&intrinsics, &exact32, &wipe](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
224
+ if (count < 3) {
225
+ throw jsi::JSError(rt, "__qntx.backends.nostr.sign expects (id, secretKey, auxRand)");
226
+ }
227
+ auto id = byteViewFromValue(rt, args[0], "__qntx.backends.nostr.sign id", intrinsics);
228
+ auto sk = byteViewFromValue(rt, args[1], "__qntx.backends.nostr.sign secretKey", intrinsics);
229
+ auto aux = byteViewFromValue(rt, args[2], "__qntx.backends.nostr.sign auxRand", intrinsics);
230
+ exact32(rt, id, "__qntx.backends.nostr.sign id");
231
+ exact32(rt, sk, "__qntx.backends.nostr.sign secretKey");
232
+ exact32(rt, aux, "__qntx.backends.nostr.sign auxRand");
233
+ uint8_t out[64];
234
+ const int ok = nostr_native_sign(id.data, sk.data, aux.data, out);
235
+ jsi::Value result;
236
+ if (ok == 1) {
237
+ result = jsi::Value(rt, makeUint8Array(rt, intrinsics, arrayBufferFromBytes(rt, out, sizeof out)));
238
+ }
239
+ wipe(out, sizeof out);
240
+ if (ok != 1) {
241
+ throw jsi::JSError(rt, "nostr_native: sign rejected the input");
242
+ }
243
+ return result;
244
+ }));
245
+
246
+ nostr.setProperty(
247
+ rt,
248
+ "verifySerialized",
249
+ hostFn(rt, "nostr.verifySerialized", 4, [&intrinsics](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
250
+ if (count < 4) {
251
+ throw jsi::JSError(rt, "__qntx.backends.nostr.verifySerialized expects (serialized, id, pubkey, sig)");
252
+ }
253
+ auto serialized = byteViewFromValue(rt, args[0], "__qntx.backends.nostr.verifySerialized serialized", intrinsics);
254
+ auto id = byteViewFromValue(rt, args[1], "__qntx.backends.nostr.verifySerialized id", intrinsics);
255
+ auto pubkey = byteViewFromValue(rt, args[2], "__qntx.backends.nostr.verifySerialized pubkey", intrinsics);
256
+ auto sig = byteViewFromValue(rt, args[3], "__qntx.backends.nostr.verifySerialized sig", intrinsics);
257
+ if (id.size != 32 || pubkey.size != 32 || sig.size != 64) {
258
+ return jsi::Value(false);
259
+ }
260
+ return jsi::Value(nostr_native_verify_serialized(
261
+ serialized.data, serialized.size, id.data, pubkey.data, sig.data) == 1);
262
+ }));
263
+
264
+ nostr.setProperty(
265
+ rt,
266
+ "ecdhX",
267
+ hostFn(rt, "nostr.ecdhX", 2, [&intrinsics, &exact32, &wipe](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
268
+ if (count < 2) {
269
+ throw jsi::JSError(rt, "__qntx.backends.nostr.ecdhX expects (secretKey, publicKeyX)");
270
+ }
271
+ auto sk = byteViewFromValue(rt, args[0], "__qntx.backends.nostr.ecdhX secretKey", intrinsics);
272
+ auto pubX = byteViewFromValue(rt, args[1], "__qntx.backends.nostr.ecdhX publicKeyX", intrinsics);
273
+ exact32(rt, sk, "__qntx.backends.nostr.ecdhX secretKey");
274
+ exact32(rt, pubX, "__qntx.backends.nostr.ecdhX publicKeyX");
275
+ uint8_t out[32];
276
+ const int ok = nostr_native_ecdh_x(sk.data, pubX.data, out);
277
+ jsi::Value result;
278
+ if (ok == 1) {
279
+ result = jsi::Value(rt, makeUint8Array(rt, intrinsics, arrayBufferFromBytes(rt, out, sizeof out)));
280
+ }
281
+ wipe(out, sizeof out);
282
+ if (ok != 1) {
283
+ throw jsi::JSError(rt, "nostr_native: ecdhX rejected the input");
284
+ }
285
+ return result;
286
+ }));
287
+
288
+ intrinsics.freeze.call(rt, jsi::Value(rt, nostr));
289
+
290
+ auto backends = jsi::Object(rt);
291
+ backends.setProperty(rt, "spark", jsi::Value(std::move(spark)));
292
+ backends.setProperty(rt, "nostr", jsi::Value(std::move(nostr)));
293
+ intrinsics.freeze.call(rt, jsi::Value(rt, backends));
294
+ qntx.setProperty(rt, "backends", jsi::Value(std::move(backends)));
295
+ }
296
+
297
+ } // namespace qntx
@@ -0,0 +1,20 @@
1
+ #pragma once
2
+
3
+ #include <jsi/jsi.h>
4
+
5
+ namespace qntx {
6
+
7
+ namespace jsi = facebook::jsi;
8
+
9
+ struct KernelIntrinsics;
10
+
11
+ // Installs `backends` on the `__qntx` object before the caller freezes it:
12
+ // `backends.spark` is the qntx_spark C ABI bound as host functions. Byte
13
+ // inputs are borrowed views (no copy); every qs_* output is copied into a
14
+ // fresh Uint8Array and then freed with qs_free, which wipes the buffer first.
15
+ void installKernelBackends(
16
+ jsi::Runtime& rt,
17
+ jsi::Object& qntx,
18
+ const KernelIntrinsics& intrinsics);
19
+
20
+ } // namespace qntx
@@ -0,0 +1,12 @@
1
+ #include "KernelBuffer.hpp"
2
+
3
+ void qntx::kernelWipe(uint8_t* data, size_t size) {
4
+ volatile uint8_t* p = data;
5
+ for (size_t i = 0; i < size; i++) {
6
+ p[i] = 0;
7
+ }
8
+ }
9
+
10
+ void qntx::kernelWipe(std::vector<uint8_t>& bytes) {
11
+ kernelWipe(bytes.data(), bytes.size());
12
+ }