@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.
Files changed (69) hide show
  1. package/README.md +123 -2
  2. package/WalletNative.podspec +74 -0
  3. package/android/CMakeLists.txt +99 -0
  4. package/android/build.gradle +177 -0
  5. package/android/consumer-rules.pro +4 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/cpp-adapter.cpp +29 -0
  8. package/android/src/main/java/com/margelo/nitro/qntxwallet/AppLifecycle.kt +35 -0
  9. package/android/src/main/java/com/margelo/nitro/qntxwallet/KernelPbkdf2.java +59 -0
  10. package/android/src/main/java/com/margelo/nitro/qntxwallet/Keystore.kt +280 -0
  11. package/android/src/main/java/com/margelo/nitro/qntxwallet/Passkey.kt +147 -0
  12. package/android/src/main/java/com/margelo/nitro/qntxwallet/VaultDirectory.kt +28 -0
  13. package/android/src/main/java/com/margelo/nitro/qntxwallet/WalletNativePackage.java +30 -0
  14. package/cpp/host/HybridKernelHost.cpp +162 -0
  15. package/cpp/host/HybridKernelHost.hpp +49 -0
  16. package/cpp/host/KernelFile.cpp +120 -0
  17. package/cpp/host/KernelFile.hpp +24 -0
  18. package/cpp/host/Keystore.hpp +70 -0
  19. package/cpp/host/Lifecycle.cpp +77 -0
  20. package/cpp/host/Lifecycle.hpp +71 -0
  21. package/cpp/host/Passkey.hpp +39 -0
  22. package/cpp/host/VaultDirectory.hpp +16 -0
  23. package/cpp/platform/KeystoreAndroid.cpp +175 -0
  24. package/cpp/platform/KeystoreIos.mm +300 -0
  25. package/cpp/platform/LifecycleAndroid.cpp +55 -0
  26. package/cpp/platform/LifecycleIos.mm +36 -0
  27. package/cpp/platform/PasskeyAndroid.cpp +118 -0
  28. package/cpp/platform/PasskeyIos.mm +20 -0
  29. package/cpp/platform/VaultDirectory.mm +58 -0
  30. package/cpp/platform/VaultDirectoryAndroid.cpp +35 -0
  31. package/cpp/runtime/KernelBackends.cpp +297 -0
  32. package/cpp/runtime/KernelBackends.hpp +20 -0
  33. package/cpp/runtime/KernelBuffer.cpp +12 -0
  34. package/cpp/runtime/KernelBuffer.hpp +96 -0
  35. package/cpp/runtime/KernelGlobals.cpp +535 -0
  36. package/cpp/runtime/KernelGlobals.hpp +107 -0
  37. package/cpp/runtime/KernelJs.cpp +155 -0
  38. package/cpp/runtime/KernelJs.hpp +81 -0
  39. package/cpp/runtime/KernelKeystore.cpp +224 -0
  40. package/cpp/runtime/KernelKeystore.hpp +28 -0
  41. package/cpp/runtime/KernelPasskey.cpp +137 -0
  42. package/cpp/runtime/KernelPasskey.hpp +23 -0
  43. package/cpp/runtime/KernelRuntime.cpp +496 -0
  44. package/cpp/runtime/KernelRuntime.hpp +94 -0
  45. package/cpp/runtime/platform/KernelPbkdf2.cpp +198 -0
  46. package/cpp/runtime/platform/KernelPbkdf2.hpp +38 -0
  47. package/dist/index.d.mts +44 -0
  48. package/dist/index.mjs +170 -0
  49. package/dist/index.mjs.map +1 -0
  50. package/kernel/generated/kernel-entry.js +42967 -0
  51. package/kernel/generated/kernel.manifest.json +80 -0
  52. package/nitrogen/generated/.gitattributes +1 -0
  53. package/nitrogen/generated/android/WalletNative+autolinking.cmake +81 -0
  54. package/nitrogen/generated/android/WalletNative+autolinking.gradle +27 -0
  55. package/nitrogen/generated/android/WalletNativeOnLoad.cpp +49 -0
  56. package/nitrogen/generated/android/WalletNativeOnLoad.hpp +34 -0
  57. package/nitrogen/generated/android/kotlin/com/margelo/nitro/qntxwallet/WalletNativeOnLoad.kt +35 -0
  58. package/nitrogen/generated/ios/WalletNative+autolinking.rb +62 -0
  59. package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Bridge.cpp +17 -0
  60. package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Bridge.hpp +27 -0
  61. package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Umbrella.hpp +38 -0
  62. package/nitrogen/generated/ios/WalletNativeAutolinking.mm +35 -0
  63. package/nitrogen/generated/ios/WalletNativeAutolinking.swift +16 -0
  64. package/nitrogen/generated/shared/c++/HybridKernelHostSpec.cpp +25 -0
  65. package/nitrogen/generated/shared/c++/HybridKernelHostSpec.hpp +69 -0
  66. package/nitrogen/generated/shared/c++/KernelExitReason.hpp +84 -0
  67. package/package.json +61 -4
  68. package/react-native.config.js +14 -0
  69. package/scripts/compile-kernel.mjs +118 -0
@@ -0,0 +1,280 @@
1
+ package com.margelo.nitro.qntxwallet
2
+
3
+ import android.app.KeyguardManager
4
+ import android.content.Context
5
+ import android.content.pm.PackageManager
6
+ import android.os.LocaleList
7
+ import android.security.keystore.KeyGenParameterSpec
8
+ import android.security.keystore.KeyPermanentlyInvalidatedException
9
+ import android.security.keystore.KeyProperties
10
+ import android.security.keystore.StrongBoxUnavailableException
11
+ import androidx.annotation.Keep
12
+ import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
13
+ import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
14
+ import androidx.biometric.BiometricPrompt
15
+ import androidx.core.content.ContextCompat
16
+ import androidx.fragment.app.FragmentActivity
17
+ import androidx.lifecycle.DefaultLifecycleObserver
18
+ import androidx.lifecycle.Lifecycle
19
+ import androidx.lifecycle.LifecycleOwner
20
+ import com.facebook.proguard.annotations.DoNotStrip
21
+ import com.margelo.nitro.NitroModules
22
+ import java.security.KeyStore
23
+ import java.util.concurrent.Executor
24
+ import java.security.UnrecoverableKeyException
25
+ import javax.crypto.Cipher
26
+ import javax.crypto.KeyGenerator
27
+ import javax.crypto.SecretKey
28
+ import javax.crypto.spec.GCMParameterSpec
29
+
30
+ /**
31
+ * Android keystore (W5): one AndroidKeyStore AES-256-GCM key per vault factor
32
+ * (`qntx-wallet.vault.<factor id>`), StrongBox-backed when present. Every use
33
+ * needs a fresh BiometricPrompt (strong biometric or device credential) bound
34
+ * to the cipher through a CryptoObject; the key survives biometric enrollment
35
+ * changes and dies with the lock screen. The C++ host calls these entry points
36
+ * with a completion token and receives the result through [nativeComplete].
37
+ * Sealed bytes are `iv(12) ‖ ciphertext ‖ tag(16)`.
38
+ */
39
+ @DoNotStrip
40
+ @Keep
41
+ object Keystore {
42
+ private const val PREFIX = "qntx-wallet.vault."
43
+ private const val PROVIDER = "AndroidKeyStore"
44
+ private const val TRANSFORMATION = "AES/GCM/NoPadding"
45
+ private const val IV_BYTES = 12
46
+ private const val TAG_BITS = 128
47
+
48
+ // Status codes shared with KeystoreAndroid.cpp.
49
+ private const val OK = 0
50
+ private const val CANCELLED = 1
51
+ private const val INVALIDATED = 2
52
+ private const val UNAVAILABLE = 3
53
+ private const val FAILED = 4
54
+
55
+ @Volatile private var current: BiometricPrompt? = null
56
+
57
+ @JvmStatic
58
+ fun locale(): String = LocaleList.getDefault()[0]?.toLanguageTag() ?: "en"
59
+
60
+ @JvmStatic
61
+ fun seal(token: Long, id: String, secret: ByteArray, title: String, description: String) {
62
+ val activity = foregroundActivity()
63
+ if (activity == null || !deviceSecure(activity)) {
64
+ secret.fill(0)
65
+ return nativeComplete(token, UNAVAILABLE, null)
66
+ }
67
+ val alias = PREFIX + id
68
+ val cipher = try {
69
+ keyStore().deleteEntry(alias)
70
+ Cipher.getInstance(TRANSFORMATION).apply { init(Cipher.ENCRYPT_MODE, generate(alias, activity)) }
71
+ } catch (e: Exception) {
72
+ secret.fill(0)
73
+ return nativeComplete(token, UNAVAILABLE, null)
74
+ }
75
+ prompt(activity, title, description, BiometricPrompt.CryptoObject(cipher)) { status, crypto ->
76
+ try {
77
+ val authenticated = crypto?.cipher
78
+ if (status != OK || authenticated == null) {
79
+ keyStore().deleteEntry(alias)
80
+ nativeComplete(token, if (status == OK) FAILED else status, null)
81
+ } else {
82
+ nativeComplete(token, OK, authenticated.iv + authenticated.doFinal(secret))
83
+ }
84
+ } catch (e: Exception) {
85
+ keyStore().deleteEntry(alias)
86
+ nativeComplete(token, FAILED, null)
87
+ } finally {
88
+ secret.fill(0)
89
+ }
90
+ }
91
+ }
92
+
93
+ @JvmStatic
94
+ fun open(token: Long, id: String, sealed: ByteArray, title: String, description: String) {
95
+ val activity = foregroundActivity() ?: return nativeComplete(token, UNAVAILABLE, null)
96
+ if (sealed.size <= IV_BYTES + TAG_BITS / 8) {
97
+ return nativeComplete(token, FAILED, null)
98
+ }
99
+ val alias = PREFIX + id
100
+ val key = try {
101
+ keyStore().getKey(alias, null) as? SecretKey
102
+ } catch (e: UnrecoverableKeyException) {
103
+ null
104
+ } ?: return nativeComplete(token, INVALIDATED, null)
105
+ val cipher = try {
106
+ Cipher.getInstance(TRANSFORMATION).apply {
107
+ init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(TAG_BITS, sealed, 0, IV_BYTES))
108
+ }
109
+ } catch (e: KeyPermanentlyInvalidatedException) {
110
+ keyStore().deleteEntry(alias)
111
+ return nativeComplete(token, INVALIDATED, null)
112
+ } catch (e: Exception) {
113
+ return nativeComplete(token, FAILED, null)
114
+ }
115
+ prompt(activity, title, description, BiometricPrompt.CryptoObject(cipher)) { status, crypto ->
116
+ val authenticated = crypto?.cipher
117
+ if (status != OK || authenticated == null) {
118
+ nativeComplete(token, if (status == OK) FAILED else status, null)
119
+ return@prompt
120
+ }
121
+ val plain = try {
122
+ authenticated.doFinal(sealed, IV_BYTES, sealed.size - IV_BYTES)
123
+ } catch (e: Exception) {
124
+ return@prompt nativeComplete(token, FAILED, null)
125
+ }
126
+ try {
127
+ nativeComplete(token, OK, plain)
128
+ } finally {
129
+ plain.fill(0)
130
+ }
131
+ }
132
+ }
133
+
134
+ @JvmStatic
135
+ fun authenticate(token: Long, title: String, description: String) {
136
+ val activity = foregroundActivity()
137
+ if (activity == null || !deviceSecure(activity)) {
138
+ return nativeComplete(token, UNAVAILABLE, null)
139
+ }
140
+ prompt(activity, title, description, null) { status, _ -> nativeComplete(token, status, null) }
141
+ }
142
+
143
+ @JvmStatic
144
+ fun clear(token: Long) {
145
+ val status = try {
146
+ val store = keyStore()
147
+ store.aliases().toList().filter { it.startsWith(PREFIX) }.forEach(store::deleteEntry)
148
+ OK
149
+ } catch (e: Exception) {
150
+ FAILED
151
+ }
152
+ nativeComplete(token, status, null)
153
+ }
154
+
155
+ @JvmStatic
156
+ fun cancel() {
157
+ val activity = foregroundActivity() ?: return
158
+ ContextCompat.getMainExecutor(activity).execute { current?.cancelAuthentication() }
159
+ }
160
+
161
+ private fun foregroundActivity(): FragmentActivity? =
162
+ NitroModules.applicationContext?.currentActivity as? FragmentActivity
163
+
164
+ private fun deviceSecure(context: Context): Boolean =
165
+ context.getSystemService(KeyguardManager::class.java)?.isDeviceSecure == true
166
+
167
+ private fun keyStore(): KeyStore = KeyStore.getInstance(PROVIDER).apply { load(null) }
168
+
169
+ private fun generate(alias: String, context: Context): SecretKey {
170
+ val strongBox = context.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)
171
+ return try {
172
+ generate(alias, strongBox)
173
+ } catch (e: StrongBoxUnavailableException) {
174
+ generate(alias, false)
175
+ }
176
+ }
177
+
178
+ private fun generate(alias: String, strongBox: Boolean): SecretKey {
179
+ val spec = KeyGenParameterSpec.Builder(
180
+ alias,
181
+ KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
182
+ )
183
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
184
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
185
+ .setKeySize(256)
186
+ .setUserAuthenticationRequired(true)
187
+ .setUserAuthenticationParameters(
188
+ 0,
189
+ KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL,
190
+ )
191
+ .setUnlockedDeviceRequired(true)
192
+ .setInvalidatedByBiometricEnrollment(false)
193
+ .setIsStrongBoxBacked(strongBox)
194
+ .build()
195
+ return KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, PROVIDER)
196
+ .apply { init(spec) }
197
+ .generateKey()
198
+ }
199
+
200
+ /** Shows one system verification on the main thread; [done] runs there with the outcome. */
201
+ private fun prompt(
202
+ activity: FragmentActivity,
203
+ title: String,
204
+ description: String,
205
+ crypto: BiometricPrompt.CryptoObject?,
206
+ done: (Int, BiometricPrompt.CryptoObject?) -> Unit,
207
+ ) {
208
+ val executor = ContextCompat.getMainExecutor(activity)
209
+ executor.execute { whenResumed(activity) { show(activity, executor, title, description, crypto, done) } }
210
+ }
211
+
212
+ // A prompt started while the activity is not resumed — e.g. right after a
213
+ // Credential Manager sheet finishes and before our activity is back on top —
214
+ // is cancelled by the framework at once. Start it on the next ON_RESUME.
215
+ private fun whenResumed(activity: FragmentActivity, action: () -> Unit) {
216
+ val lifecycle = activity.lifecycle
217
+ if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
218
+ action()
219
+ return
220
+ }
221
+ lifecycle.addObserver(object : DefaultLifecycleObserver {
222
+ override fun onResume(owner: LifecycleOwner) {
223
+ lifecycle.removeObserver(this)
224
+ action()
225
+ }
226
+ })
227
+ }
228
+
229
+ private fun show(
230
+ activity: FragmentActivity,
231
+ executor: Executor,
232
+ title: String,
233
+ description: String,
234
+ crypto: BiometricPrompt.CryptoObject?,
235
+ done: (Int, BiometricPrompt.CryptoObject?) -> Unit,
236
+ ) {
237
+ val info = BiometricPrompt.PromptInfo.Builder()
238
+ .setTitle(title)
239
+ .setDescription(description)
240
+ .setAllowedAuthenticators(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)
241
+ .build()
242
+ val biometricPrompt = BiometricPrompt(
243
+ activity,
244
+ executor,
245
+ object : BiometricPrompt.AuthenticationCallback() {
246
+ override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
247
+ current = null
248
+ done(OK, result.cryptoObject)
249
+ }
250
+
251
+ override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
252
+ current = null
253
+ done(statusOf(errorCode), null)
254
+ }
255
+ },
256
+ )
257
+ current = biometricPrompt
258
+ try {
259
+ if (crypto == null) biometricPrompt.authenticate(info) else biometricPrompt.authenticate(info, crypto)
260
+ } catch (e: Exception) {
261
+ current = null
262
+ done(FAILED, null)
263
+ }
264
+ }
265
+
266
+ private fun statusOf(errorCode: Int): Int = when (errorCode) {
267
+ BiometricPrompt.ERROR_USER_CANCELED,
268
+ BiometricPrompt.ERROR_NEGATIVE_BUTTON,
269
+ BiometricPrompt.ERROR_CANCELED,
270
+ BiometricPrompt.ERROR_TIMEOUT -> CANCELLED
271
+ BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL,
272
+ BiometricPrompt.ERROR_HW_UNAVAILABLE,
273
+ BiometricPrompt.ERROR_HW_NOT_PRESENT,
274
+ BiometricPrompt.ERROR_NO_BIOMETRICS -> UNAVAILABLE
275
+ else -> FAILED
276
+ }
277
+
278
+ @JvmStatic
279
+ private external fun nativeComplete(token: Long, status: Int, bytes: ByteArray?)
280
+ }
@@ -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