@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,147 @@
|
|
|
1
|
+
package com.margelo.nitro.qntxwallet
|
|
2
|
+
|
|
3
|
+
import android.os.CancellationSignal
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import androidx.annotation.Keep
|
|
6
|
+
import androidx.core.content.ContextCompat
|
|
7
|
+
import androidx.credentials.CreateCredentialResponse
|
|
8
|
+
import androidx.credentials.CreatePublicKeyCredentialRequest
|
|
9
|
+
import androidx.credentials.CreatePublicKeyCredentialResponse
|
|
10
|
+
import androidx.credentials.CredentialManager
|
|
11
|
+
import androidx.credentials.CredentialManagerCallback
|
|
12
|
+
import androidx.credentials.GetCredentialRequest
|
|
13
|
+
import androidx.credentials.GetCredentialResponse
|
|
14
|
+
import androidx.credentials.GetPublicKeyCredentialOption
|
|
15
|
+
import androidx.credentials.PublicKeyCredential
|
|
16
|
+
import androidx.credentials.exceptions.CreateCredentialCancellationException
|
|
17
|
+
import androidx.credentials.exceptions.CreateCredentialException
|
|
18
|
+
import androidx.credentials.exceptions.CreateCredentialNoCreateOptionException
|
|
19
|
+
import androidx.credentials.exceptions.CreateCredentialProviderConfigurationException
|
|
20
|
+
import androidx.credentials.exceptions.CreateCredentialUnsupportedException
|
|
21
|
+
import androidx.credentials.exceptions.GetCredentialCancellationException
|
|
22
|
+
import androidx.credentials.exceptions.GetCredentialException
|
|
23
|
+
import androidx.credentials.exceptions.GetCredentialProviderConfigurationException
|
|
24
|
+
import androidx.credentials.exceptions.GetCredentialUnsupportedException
|
|
25
|
+
import androidx.credentials.exceptions.NoCredentialException
|
|
26
|
+
import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialDomException
|
|
27
|
+
import androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialDomException
|
|
28
|
+
import androidx.fragment.app.FragmentActivity
|
|
29
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
30
|
+
import com.margelo.nitro.NitroModules
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Android passkey authenticator (W6): Credential Manager with the kernel's
|
|
34
|
+
* WebAuthn JSON. Requests and responses stay JSON — the kernel builds the
|
|
35
|
+
* options (PRF eval included) and parses the response itself, so this object
|
|
36
|
+
* only runs the system ceremony. The C++ host calls [create] and [get] with a
|
|
37
|
+
* completion token and receives the outcome through [nativeComplete].
|
|
38
|
+
*/
|
|
39
|
+
@DoNotStrip
|
|
40
|
+
@Keep
|
|
41
|
+
object Passkey {
|
|
42
|
+
// Status codes shared with PasskeyAndroid.cpp.
|
|
43
|
+
private const val OK = 0
|
|
44
|
+
private const val CANCELLED = 1
|
|
45
|
+
private const val UNAVAILABLE = 3
|
|
46
|
+
private const val FAILED = 4
|
|
47
|
+
// The exception type only — never the message, which can name the account.
|
|
48
|
+
private const val TAG = "QntxPasskey"
|
|
49
|
+
|
|
50
|
+
@Volatile private var signal: CancellationSignal? = null
|
|
51
|
+
|
|
52
|
+
@JvmStatic
|
|
53
|
+
fun create(token: Long, requestJson: String) {
|
|
54
|
+
val activity = foregroundActivity() ?: return nativeComplete(token, UNAVAILABLE, null)
|
|
55
|
+
val cancel = CancellationSignal().also { signal = it }
|
|
56
|
+
CredentialManager.create(activity).createCredentialAsync(
|
|
57
|
+
activity,
|
|
58
|
+
CreatePublicKeyCredentialRequest(requestJson),
|
|
59
|
+
cancel,
|
|
60
|
+
ContextCompat.getMainExecutor(activity),
|
|
61
|
+
object : CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException> {
|
|
62
|
+
override fun onResult(result: CreateCredentialResponse) {
|
|
63
|
+
signal = null
|
|
64
|
+
val response = result as? CreatePublicKeyCredentialResponse
|
|
65
|
+
nativeComplete(
|
|
66
|
+
token,
|
|
67
|
+
if (response == null) FAILED else OK,
|
|
68
|
+
response?.registrationResponseJson,
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
override fun onError(e: CreateCredentialException) {
|
|
73
|
+
signal = null
|
|
74
|
+
Log.w(TAG, "create failed: ${e.type}")
|
|
75
|
+
nativeComplete(token, statusOf(e), null)
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@JvmStatic
|
|
82
|
+
fun get(token: Long, requestJson: String) {
|
|
83
|
+
val activity = foregroundActivity() ?: return nativeComplete(token, UNAVAILABLE, null)
|
|
84
|
+
val cancel = CancellationSignal().also { signal = it }
|
|
85
|
+
CredentialManager.create(activity).getCredentialAsync(
|
|
86
|
+
activity,
|
|
87
|
+
GetCredentialRequest(listOf(GetPublicKeyCredentialOption(requestJson))),
|
|
88
|
+
cancel,
|
|
89
|
+
ContextCompat.getMainExecutor(activity),
|
|
90
|
+
object : CredentialManagerCallback<GetCredentialResponse, GetCredentialException> {
|
|
91
|
+
override fun onResult(result: GetCredentialResponse) {
|
|
92
|
+
signal = null
|
|
93
|
+
val credential = result.credential as? PublicKeyCredential
|
|
94
|
+
nativeComplete(
|
|
95
|
+
token,
|
|
96
|
+
if (credential == null) FAILED else OK,
|
|
97
|
+
credential?.authenticationResponseJson,
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
override fun onError(e: GetCredentialException) {
|
|
102
|
+
signal = null
|
|
103
|
+
Log.w(TAG, "get failed: ${e.type}")
|
|
104
|
+
nativeComplete(token, statusOf(e), null)
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@JvmStatic
|
|
111
|
+
fun cancel() {
|
|
112
|
+
signal?.cancel()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private fun foregroundActivity(): FragmentActivity? =
|
|
116
|
+
NitroModules.applicationContext?.currentActivity as? FragmentActivity
|
|
117
|
+
|
|
118
|
+
// NotAllowedError is what the platform reports when the user dismisses or
|
|
119
|
+
// times out the sheet; SecurityError means the RP id is not associated
|
|
120
|
+
// with this app (assetlinks), i.e. passkeys are unavailable here.
|
|
121
|
+
private fun statusOf(e: CreateCredentialException): Int = when (e) {
|
|
122
|
+
is CreateCredentialCancellationException -> CANCELLED
|
|
123
|
+
is CreateCredentialNoCreateOptionException,
|
|
124
|
+
is CreateCredentialProviderConfigurationException,
|
|
125
|
+
is CreateCredentialUnsupportedException -> UNAVAILABLE
|
|
126
|
+
is CreatePublicKeyCredentialDomException -> domStatus(e.type)
|
|
127
|
+
else -> FAILED
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private fun statusOf(e: GetCredentialException): Int = when (e) {
|
|
131
|
+
is GetCredentialCancellationException -> CANCELLED
|
|
132
|
+
is NoCredentialException,
|
|
133
|
+
is GetCredentialProviderConfigurationException,
|
|
134
|
+
is GetCredentialUnsupportedException -> UNAVAILABLE
|
|
135
|
+
is GetPublicKeyCredentialDomException -> domStatus(e.type)
|
|
136
|
+
else -> FAILED
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private fun domStatus(type: String): Int = when {
|
|
140
|
+
type.endsWith("NotAllowedError") -> CANCELLED
|
|
141
|
+
type.endsWith("SecurityError") || type.endsWith("NotSupportedError") -> UNAVAILABLE
|
|
142
|
+
else -> FAILED
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@JvmStatic
|
|
146
|
+
private external fun nativeComplete(token: Long, status: Int, responseJson: String?)
|
|
147
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package com.margelo.nitro.qntxwallet
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.Keep
|
|
4
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
5
|
+
import com.margelo.nitro.NitroModules
|
|
6
|
+
import java.io.File
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Vault directory hook: `<noBackupFilesDir>/qntx-wallet`, pulled from C++
|
|
10
|
+
* through fbjni — `noBackupFilesDir` needs a Context, which Nitro hands us once
|
|
11
|
+
* its TurboModule is constructed (before any `createHybridObject("KernelHost")`).
|
|
12
|
+
*/
|
|
13
|
+
object VaultDirectory {
|
|
14
|
+
@JvmStatic
|
|
15
|
+
@DoNotStrip
|
|
16
|
+
@Keep
|
|
17
|
+
fun resolve(): String {
|
|
18
|
+
val context = NitroModules.applicationContext
|
|
19
|
+
?: throw IllegalStateException(
|
|
20
|
+
"vault directory: NitroModules.applicationContext is not set")
|
|
21
|
+
val dir = File(context.noBackupFilesDir, "qntx-wallet")
|
|
22
|
+
dir.mkdirs()
|
|
23
|
+
if (!dir.isDirectory) {
|
|
24
|
+
throw IllegalStateException("vault directory: mkdirs failed")
|
|
25
|
+
}
|
|
26
|
+
return dir.absolutePath
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package com.margelo.nitro.qntxwallet;
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.Nullable;
|
|
4
|
+
|
|
5
|
+
import com.facebook.react.bridge.NativeModule;
|
|
6
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider;
|
|
8
|
+
import com.facebook.react.BaseReactPackage;
|
|
9
|
+
|
|
10
|
+
import java.util.HashMap;
|
|
11
|
+
|
|
12
|
+
public class WalletNativePackage extends BaseReactPackage {
|
|
13
|
+
@Nullable
|
|
14
|
+
@Override
|
|
15
|
+
public NativeModule getModule(String name, ReactApplicationContext reactContext) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@Override
|
|
20
|
+
public ReactModuleInfoProvider getReactModuleInfoProvider() {
|
|
21
|
+
return () -> {
|
|
22
|
+
return new HashMap<>();
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static {
|
|
27
|
+
WalletNativeOnLoad.initializeNative();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#include "HybridKernelHost.hpp"
|
|
2
|
+
|
|
3
|
+
#include "kernel.hpp"
|
|
4
|
+
#include "VaultDirectory.hpp"
|
|
5
|
+
|
|
6
|
+
#if defined(__ANDROID__)
|
|
7
|
+
#include <android/log.h>
|
|
8
|
+
#elif defined(__APPLE__)
|
|
9
|
+
#include <os/log.h>
|
|
10
|
+
#endif
|
|
11
|
+
|
|
12
|
+
namespace margelo::nitro::qntxwallet {
|
|
13
|
+
|
|
14
|
+
namespace {
|
|
15
|
+
|
|
16
|
+
// A kernel crash reaches JS only as "crashed"; the reason goes to the system
|
|
17
|
+
// log so field reports and device runs can tell causes apart. Kernel error
|
|
18
|
+
// messages are static strings that never echo input or secrets.
|
|
19
|
+
void logKernelCrash(const std::string& reason) {
|
|
20
|
+
#if defined(__ANDROID__)
|
|
21
|
+
__android_log_print(ANDROID_LOG_ERROR, "QntxKernel", "kernel runtime crashed: %s", reason.c_str());
|
|
22
|
+
#elif defined(__APPLE__)
|
|
23
|
+
os_log_error(OS_LOG_DEFAULT, "qntx kernel runtime crashed: %{public}s", reason.c_str());
|
|
24
|
+
#else
|
|
25
|
+
(void)reason;
|
|
26
|
+
#endif
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
} // namespace
|
|
30
|
+
|
|
31
|
+
HybridKernelHost::~HybridKernelHost() {
|
|
32
|
+
std::shared_ptr<qntx::KernelRuntime> runtime;
|
|
33
|
+
{
|
|
34
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
35
|
+
runtime = std::move(runtime_);
|
|
36
|
+
}
|
|
37
|
+
if (runtime) {
|
|
38
|
+
runtime->destroy();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
void HybridKernelHost::start(double backgroundLockSeconds) {
|
|
43
|
+
// Compare as doubles before converting: casting NaN or an out-of-range value
|
|
44
|
+
// to int is undefined behaviour.
|
|
45
|
+
if (backgroundLockSeconds != 0 && backgroundLockSeconds != 60 &&
|
|
46
|
+
backgroundLockSeconds != 300 && backgroundLockSeconds != 900) {
|
|
47
|
+
throw std::runtime_error("backgroundLockSeconds must be one of 0, 60, 300, 900");
|
|
48
|
+
}
|
|
49
|
+
const int seconds = static_cast<int>(backgroundLockSeconds);
|
|
50
|
+
{
|
|
51
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
52
|
+
if (runtime_ || exited_) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// The platform observer installs once; this host registers its deadline so
|
|
57
|
+
// the dispatcher can stamp or lock it on background/foreground transitions.
|
|
58
|
+
qntx::installLifecycleObserver();
|
|
59
|
+
qntx::KernelLifecycleDispatcher::shared().registerHost(
|
|
60
|
+
std::dynamic_pointer_cast<HybridKernelHost>(shared_from_this()), seconds);
|
|
61
|
+
// Resolved once here: a platform failure rejects start() for the JS caller
|
|
62
|
+
// instead of surfacing later as a store error inside the kernel.
|
|
63
|
+
const std::string vaultDirectory = qntx::platformVaultDirectory();
|
|
64
|
+
|
|
65
|
+
std::weak_ptr<HybridKernelHost> weak =
|
|
66
|
+
std::dynamic_pointer_cast<HybridKernelHost>(shared_from_this());
|
|
67
|
+
qntx::KernelRuntime::Hooks hooks;
|
|
68
|
+
hooks.onPost = [weak](std::vector<uint8_t> bytes) {
|
|
69
|
+
if (const auto self = weak.lock()) {
|
|
70
|
+
std::function<void(const std::shared_ptr<ArrayBuffer>&)> listener;
|
|
71
|
+
{
|
|
72
|
+
std::lock_guard<std::mutex> lock(self->mutex_);
|
|
73
|
+
listener = self->messageListener_;
|
|
74
|
+
}
|
|
75
|
+
if (listener) {
|
|
76
|
+
listener(ArrayBuffer::copy(bytes.data(), bytes.size()));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
hooks.onError = [weak](std::string reason) {
|
|
81
|
+
logKernelCrash(reason);
|
|
82
|
+
if (const auto self = weak.lock()) {
|
|
83
|
+
self->exit(KernelExitReason::CRASHED);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
hooks.onIntegrity = [weak] {
|
|
87
|
+
if (const auto self = weak.lock()) {
|
|
88
|
+
self->exit(KernelExitReason::INTEGRITY);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const auto script = qntx::KernelScript{
|
|
93
|
+
qntx::KernelScript::Kind::bytecode,
|
|
94
|
+
std::make_shared<const std::vector<uint8_t>>(
|
|
95
|
+
qntx::KERNEL_BYTECODE, qntx::KERNEL_BYTECODE + qntx::KERNEL_BYTECODE_SIZE)};
|
|
96
|
+
const auto runtime =
|
|
97
|
+
qntx::KernelRuntime::start(script, std::move(hooks), vaultDirectory);
|
|
98
|
+
{
|
|
99
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
100
|
+
if (exited_) {
|
|
101
|
+
// An exit (integrity hash check raced a lock) already fired — drop the
|
|
102
|
+
// fresh runtime instead of resurrecting a dead host.
|
|
103
|
+
runtime->destroy();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
runtime_ = runtime;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
void HybridKernelHost::post(const std::shared_ptr<ArrayBuffer>& frame) {
|
|
111
|
+
if (frame->size() > kMaxFrameBytes) {
|
|
112
|
+
exit(KernelExitReason::INTEGRITY);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
std::shared_ptr<qntx::KernelRuntime> runtime;
|
|
116
|
+
{
|
|
117
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
118
|
+
runtime = runtime_;
|
|
119
|
+
}
|
|
120
|
+
if (runtime) {
|
|
121
|
+
const auto* data = static_cast<const uint8_t*>(frame->data());
|
|
122
|
+
runtime->post(std::vector<uint8_t>(data, data + frame->size()));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
void HybridKernelHost::onMessage(
|
|
127
|
+
const std::function<void(const std::shared_ptr<ArrayBuffer>&)>& listener) {
|
|
128
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
129
|
+
messageListener_ = listener;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
void HybridKernelHost::onExit(const std::function<void(KernelExitReason)>& listener) {
|
|
133
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
134
|
+
exitListener_ = listener;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
void HybridKernelHost::lock() {
|
|
138
|
+
exit(KernelExitReason::LOCKED);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
void HybridKernelHost::exit(KernelExitReason reason) {
|
|
142
|
+
std::shared_ptr<qntx::KernelRuntime> runtime;
|
|
143
|
+
std::function<void(KernelExitReason)> listener;
|
|
144
|
+
{
|
|
145
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
146
|
+
if (exited_) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
exited_ = true;
|
|
150
|
+
runtime = std::move(runtime_);
|
|
151
|
+
listener = exitListener_;
|
|
152
|
+
}
|
|
153
|
+
if (runtime) {
|
|
154
|
+
runtime->destroy();
|
|
155
|
+
}
|
|
156
|
+
qntx::KernelLifecycleDispatcher::shared().unregister(this);
|
|
157
|
+
if (listener) {
|
|
158
|
+
listener(reason);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
} // namespace margelo::nitro::qntxwallet
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "HybridKernelHostSpec.hpp"
|
|
4
|
+
|
|
5
|
+
#include <functional>
|
|
6
|
+
#include <memory>
|
|
7
|
+
#include <mutex>
|
|
8
|
+
|
|
9
|
+
#include "host/Lifecycle.hpp"
|
|
10
|
+
|
|
11
|
+
#include "../runtime/KernelRuntime.hpp"
|
|
12
|
+
|
|
13
|
+
namespace margelo::nitro::qntxwallet {
|
|
14
|
+
|
|
15
|
+
// The only Nitro-registered object: drives one kernel runtime. `start`
|
|
16
|
+
// verifies the embedded bytecode's SHA-256 before loading; `post` enforces the
|
|
17
|
+
// 1 MiB frame cap (the inbound queue cap lives inside KernelRuntime::post and
|
|
18
|
+
// reports through the same integrity exit); `lock` destroys the runtime. Exit
|
|
19
|
+
// reasons reach JS through the `onExit` listener, at most once.
|
|
20
|
+
class HybridKernelHost final : public HybridKernelHostSpec,
|
|
21
|
+
public qntx::LifecycleLockTarget {
|
|
22
|
+
public:
|
|
23
|
+
HybridKernelHost() : HybridObject(TAG) {}
|
|
24
|
+
~HybridKernelHost() override;
|
|
25
|
+
|
|
26
|
+
void start(double backgroundLockSeconds) override;
|
|
27
|
+
// The platform's lifecycle dispatcher locks through here (main thread).
|
|
28
|
+
void lockForBackground() override { exit(KernelExitReason::BACKGROUND); }
|
|
29
|
+
void post(const std::shared_ptr<ArrayBuffer>& frame) override;
|
|
30
|
+
void onMessage(const std::function<void(const std::shared_ptr<ArrayBuffer>&)>& listener)
|
|
31
|
+
override;
|
|
32
|
+
void onExit(const std::function<void(KernelExitReason)>& listener) override;
|
|
33
|
+
void lock() override;
|
|
34
|
+
|
|
35
|
+
private:
|
|
36
|
+
static constexpr size_t kMaxFrameBytes = 1024 * 1024;
|
|
37
|
+
|
|
38
|
+
// Reports `reason` through onExit once, then tears the runtime down unless
|
|
39
|
+
// the exit was itself the lock report (the runtime is already gone).
|
|
40
|
+
void exit(KernelExitReason reason);
|
|
41
|
+
|
|
42
|
+
std::mutex mutex_;
|
|
43
|
+
std::shared_ptr<qntx::KernelRuntime> runtime_;
|
|
44
|
+
std::function<void(const std::shared_ptr<ArrayBuffer>&)> messageListener_;
|
|
45
|
+
std::function<void(KernelExitReason)> exitListener_;
|
|
46
|
+
bool exited_ = false;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
} // namespace margelo::nitro::qntxwallet
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#include "KernelFile.hpp"
|
|
2
|
+
|
|
3
|
+
#include <cerrno>
|
|
4
|
+
#include <cstring>
|
|
5
|
+
#include <stdexcept>
|
|
6
|
+
|
|
7
|
+
#if defined(_WIN32)
|
|
8
|
+
#error "KernelFile has no Windows implementation"
|
|
9
|
+
#else
|
|
10
|
+
#include <fcntl.h>
|
|
11
|
+
#include <sys/stat.h>
|
|
12
|
+
#include <sys/types.h>
|
|
13
|
+
#include <unistd.h>
|
|
14
|
+
#endif
|
|
15
|
+
|
|
16
|
+
namespace qntx {
|
|
17
|
+
namespace {
|
|
18
|
+
|
|
19
|
+
[[noreturn]] void fail(const char* what) {
|
|
20
|
+
const int saved = errno;
|
|
21
|
+
throw std::runtime_error(
|
|
22
|
+
std::string(what) + ": " + std::strerror(saved) + " (errno " + std::to_string(saved) + ")");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
void fsyncOrFail(int fd, const char* what, bool full = false) {
|
|
26
|
+
// Files: on Apple the documented durable barrier is fcntl(F_FULLFSYNC).
|
|
27
|
+
// Directories: plain fsync everywhere (F_FULLFSYNC is for files only).
|
|
28
|
+
const int rc =
|
|
29
|
+
#if defined(__APPLE__)
|
|
30
|
+
full ? ::fcntl(fd, F_FULLFSYNC) : ::fsync(fd);
|
|
31
|
+
#else
|
|
32
|
+
(::fsync(fd));
|
|
33
|
+
#endif
|
|
34
|
+
if (rc != 0) {
|
|
35
|
+
fail(what);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
std::string pathFor(const std::string& directory, const char* leaf) {
|
|
40
|
+
return directory + "/" + leaf;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
} // namespace
|
|
44
|
+
|
|
45
|
+
std::optional<std::vector<uint8_t>> kernelFileRead(const std::string& directory) {
|
|
46
|
+
const std::string path = pathFor(directory, "vault.cbor");
|
|
47
|
+
const int fd = ::open(path.c_str(), O_RDONLY);
|
|
48
|
+
if (fd < 0) {
|
|
49
|
+
if (errno == ENOENT) {
|
|
50
|
+
return std::nullopt;
|
|
51
|
+
}
|
|
52
|
+
fail("open vault file");
|
|
53
|
+
}
|
|
54
|
+
std::vector<uint8_t> bytes;
|
|
55
|
+
uint8_t chunk[65536];
|
|
56
|
+
for (;;) {
|
|
57
|
+
const ssize_t n = ::read(fd, chunk, sizeof(chunk));
|
|
58
|
+
if (n < 0) {
|
|
59
|
+
const int saved = errno;
|
|
60
|
+
::close(fd);
|
|
61
|
+
errno = saved;
|
|
62
|
+
fail("read vault file");
|
|
63
|
+
}
|
|
64
|
+
if (n == 0) {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
bytes.insert(bytes.end(), chunk, chunk + n);
|
|
68
|
+
}
|
|
69
|
+
::close(fd);
|
|
70
|
+
return bytes;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
void kernelFileWrite(const std::string& directory, const std::vector<uint8_t>& bytes) {
|
|
74
|
+
const std::string tmp = pathFor(directory, "vault.cbor.tmp");
|
|
75
|
+
const std::string final = pathFor(directory, "vault.cbor");
|
|
76
|
+
const int fd = ::open(tmp.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
|
77
|
+
if (fd < 0) {
|
|
78
|
+
fail("open temp vault file");
|
|
79
|
+
}
|
|
80
|
+
size_t off = 0;
|
|
81
|
+
while (off < bytes.size()) {
|
|
82
|
+
const ssize_t n = ::write(fd, bytes.data() + off, bytes.size() - off);
|
|
83
|
+
if (n <= 0) {
|
|
84
|
+
const int saved = errno;
|
|
85
|
+
::close(fd);
|
|
86
|
+
::unlink(tmp.c_str());
|
|
87
|
+
errno = saved;
|
|
88
|
+
fail("write temp vault file");
|
|
89
|
+
}
|
|
90
|
+
off += static_cast<size_t>(n);
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
fsyncOrFail(fd, "fsync temp vault file", /*full=*/true);
|
|
94
|
+
} catch (...) {
|
|
95
|
+
::close(fd);
|
|
96
|
+
::unlink(tmp.c_str());
|
|
97
|
+
throw;
|
|
98
|
+
}
|
|
99
|
+
if (::close(fd) != 0) {
|
|
100
|
+
::unlink(tmp.c_str());
|
|
101
|
+
fail("close temp vault file");
|
|
102
|
+
}
|
|
103
|
+
if (::rename(tmp.c_str(), final.c_str()) != 0) {
|
|
104
|
+
::unlink(tmp.c_str());
|
|
105
|
+
fail("rename vault file");
|
|
106
|
+
}
|
|
107
|
+
const int dirFd = ::open(directory.c_str(), O_RDONLY);
|
|
108
|
+
if (dirFd < 0) {
|
|
109
|
+
fail("open vault directory");
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
fsyncOrFail(dirFd, "fsync vault directory");
|
|
113
|
+
} catch (...) {
|
|
114
|
+
::close(dirFd);
|
|
115
|
+
throw;
|
|
116
|
+
}
|
|
117
|
+
::close(dirFd);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
} // namespace qntx
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <optional>
|
|
4
|
+
#include <string>
|
|
5
|
+
#include <vector>
|
|
6
|
+
|
|
7
|
+
namespace qntx {
|
|
8
|
+
|
|
9
|
+
// The vault image file lives in a platform-provided directory
|
|
10
|
+
// (platformVaultDirectory()): <directory>/vault.cbor.
|
|
11
|
+
//
|
|
12
|
+
// The whole file, or nullopt when it does not exist. Throws std::runtime_error
|
|
13
|
+
// naming the failing step and errno — the file store turns that into a typed
|
|
14
|
+
// rejection and never silently treats it as an empty vault.
|
|
15
|
+
std::optional<std::vector<uint8_t>> kernelFileRead(const std::string& directory);
|
|
16
|
+
|
|
17
|
+
// Atomic replace of vault.cbor in `directory`: write vault.cbor.tmp in the
|
|
18
|
+
// same directory, fsync it, rename it over the target, then fsync the
|
|
19
|
+
// directory so the rename itself is durable. Throws std::runtime_error naming
|
|
20
|
+
// the failing step and errno; a failed write leaves the previous file intact
|
|
21
|
+
// and unlinks the temp file.
|
|
22
|
+
void kernelFileWrite(const std::string& directory, const std::vector<uint8_t>& bytes);
|
|
23
|
+
|
|
24
|
+
} // namespace qntx
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <functional>
|
|
4
|
+
#include <string>
|
|
5
|
+
#include <vector>
|
|
6
|
+
|
|
7
|
+
#include "../runtime/KernelBuffer.hpp"
|
|
8
|
+
|
|
9
|
+
namespace qntx {
|
|
10
|
+
|
|
11
|
+
// The platform keystore behind `__qntx.keystore` (W5): a hardware key per
|
|
12
|
+
// vault factor that seals and opens the factor's KEK behind a system user
|
|
13
|
+
// verification. iOS: Secure Enclave P-256 + ECIES; Android: Keystore
|
|
14
|
+
// AES-256-GCM behind BiometricPrompt. Each function has exactly one strong
|
|
15
|
+
// definition per platform (KeystoreIos.mm, KeystoreAndroid.cpp); host tests
|
|
16
|
+
// link a fake.
|
|
17
|
+
|
|
18
|
+
struct KeystorePrompt {
|
|
19
|
+
std::string title;
|
|
20
|
+
std::string description;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// How a keystore call ended. The binding maps each status to the kernel's
|
|
24
|
+
// error code: cancelled → "cancelled", invalidated → "factor-invalidated",
|
|
25
|
+
// unavailable → "factor-unavailable", failed → "unavailable". The values are
|
|
26
|
+
// part of the Android JNI contract (Keystore.kt).
|
|
27
|
+
enum class KeystoreStatus { ok = 0, cancelled = 1, invalidated = 2, unavailable = 3, failed = 4 };
|
|
28
|
+
|
|
29
|
+
struct KeystoreResult {
|
|
30
|
+
KeystoreStatus status = KeystoreStatus::failed;
|
|
31
|
+
// seal: the platform ciphertext; open: the KEK; empty otherwise.
|
|
32
|
+
SecretBytes bytes;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Completion callbacks run exactly once, on any thread.
|
|
36
|
+
using KeystoreDone = std::function<void(KeystoreResult)>;
|
|
37
|
+
|
|
38
|
+
// Creates the hardware key for vault factor `id` and encrypts `secret` under
|
|
39
|
+
// it. Android prompts (encryption needs the authenticated cipher); iOS does
|
|
40
|
+
// not (ECIES encrypts with the public key).
|
|
41
|
+
void keystoreSeal(
|
|
42
|
+
const std::string& id,
|
|
43
|
+
SecretBytes secret,
|
|
44
|
+
KeystorePrompt prompt,
|
|
45
|
+
KeystoreDone done);
|
|
46
|
+
|
|
47
|
+
// Decrypts `sealed` with factor `id`'s key after a fresh system verification
|
|
48
|
+
// that shows `prompt`. A key the system removed (lock screen disabled,
|
|
49
|
+
// device restored) completes `invalidated`.
|
|
50
|
+
void keystoreOpen(
|
|
51
|
+
const std::string& id,
|
|
52
|
+
std::vector<uint8_t> sealed,
|
|
53
|
+
KeystorePrompt prompt,
|
|
54
|
+
KeystoreDone done);
|
|
55
|
+
|
|
56
|
+
// A device-owner check with no key involved (vault reset, WD-055).
|
|
57
|
+
void keystoreAuthenticate(KeystorePrompt prompt, KeystoreDone done);
|
|
58
|
+
|
|
59
|
+
// Deletes every vault key this app created.
|
|
60
|
+
void keystoreClear(KeystoreDone done);
|
|
61
|
+
|
|
62
|
+
// Dismisses the system prompt of an in-flight call, if any; that call still
|
|
63
|
+
// completes (normally `cancelled`). Called when a kernel runtime tears down.
|
|
64
|
+
void keystoreCancel();
|
|
65
|
+
|
|
66
|
+
// The platform's preferred language as a BCP 47 tag, read natively so the
|
|
67
|
+
// main runtime cannot choose the prompt language.
|
|
68
|
+
std::string keystoreLocale();
|
|
69
|
+
|
|
70
|
+
} // namespace qntx
|