@qntx/wallet-native 0.0.0-stage → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-APACHE +201 -0
- package/LICENSE-MIT +21 -0
- package/README.md +123 -2
- package/WalletNative.podspec +74 -0
- package/android/CMakeLists.txt +99 -0
- package/android/build.gradle +177 -0
- package/android/consumer-rules.pro +4 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +29 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/AppLifecycle.kt +35 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/KernelPbkdf2.java +59 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/Keystore.kt +280 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/Passkey.kt +147 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/VaultDirectory.kt +28 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/WalletNativePackage.java +30 -0
- package/cpp/host/HybridKernelHost.cpp +162 -0
- package/cpp/host/HybridKernelHost.hpp +49 -0
- package/cpp/host/KernelFile.cpp +120 -0
- package/cpp/host/KernelFile.hpp +24 -0
- package/cpp/host/Keystore.hpp +70 -0
- package/cpp/host/Lifecycle.cpp +77 -0
- package/cpp/host/Lifecycle.hpp +71 -0
- package/cpp/host/Passkey.hpp +39 -0
- package/cpp/host/VaultDirectory.hpp +16 -0
- package/cpp/platform/KeystoreAndroid.cpp +175 -0
- package/cpp/platform/KeystoreIos.mm +300 -0
- package/cpp/platform/LifecycleAndroid.cpp +55 -0
- package/cpp/platform/LifecycleIos.mm +36 -0
- package/cpp/platform/PasskeyAndroid.cpp +118 -0
- package/cpp/platform/PasskeyIos.mm +20 -0
- package/cpp/platform/VaultDirectory.mm +58 -0
- package/cpp/platform/VaultDirectoryAndroid.cpp +35 -0
- package/cpp/runtime/KernelBackends.cpp +297 -0
- package/cpp/runtime/KernelBackends.hpp +20 -0
- package/cpp/runtime/KernelBuffer.cpp +12 -0
- package/cpp/runtime/KernelBuffer.hpp +96 -0
- package/cpp/runtime/KernelGlobals.cpp +535 -0
- package/cpp/runtime/KernelGlobals.hpp +107 -0
- package/cpp/runtime/KernelJs.cpp +155 -0
- package/cpp/runtime/KernelJs.hpp +81 -0
- package/cpp/runtime/KernelKeystore.cpp +224 -0
- package/cpp/runtime/KernelKeystore.hpp +28 -0
- package/cpp/runtime/KernelPasskey.cpp +137 -0
- package/cpp/runtime/KernelPasskey.hpp +23 -0
- package/cpp/runtime/KernelRuntime.cpp +496 -0
- package/cpp/runtime/KernelRuntime.hpp +94 -0
- package/cpp/runtime/platform/KernelPbkdf2.cpp +198 -0
- package/cpp/runtime/platform/KernelPbkdf2.hpp +38 -0
- package/dist/index.d.mts +44 -0
- package/dist/index.mjs +170 -0
- package/dist/index.mjs.map +1 -0
- package/kernel/generated/kernel-entry.js +42967 -0
- package/kernel/generated/kernel.manifest.json +80 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/WalletNative+autolinking.cmake +81 -0
- package/nitrogen/generated/android/WalletNative+autolinking.gradle +27 -0
- package/nitrogen/generated/android/WalletNativeOnLoad.cpp +49 -0
- package/nitrogen/generated/android/WalletNativeOnLoad.hpp +34 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/qntxwallet/WalletNativeOnLoad.kt +35 -0
- package/nitrogen/generated/ios/WalletNative+autolinking.rb +62 -0
- package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Bridge.cpp +17 -0
- package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Bridge.hpp +27 -0
- package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Umbrella.hpp +38 -0
- package/nitrogen/generated/ios/WalletNativeAutolinking.mm +35 -0
- package/nitrogen/generated/ios/WalletNativeAutolinking.swift +16 -0
- package/nitrogen/generated/shared/c++/HybridKernelHostSpec.cpp +25 -0
- package/nitrogen/generated/shared/c++/HybridKernelHostSpec.hpp +69 -0
- package/nitrogen/generated/shared/c++/KernelExitReason.hpp +84 -0
- package/package.json +65 -4
- package/react-native.config.js +14 -0
- package/scripts/compile-kernel.mjs +118 -0
|
@@ -0,0 +1,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
|
+
}
|