@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
package/README.md CHANGED
@@ -1,3 +1,124 @@
1
- # Temporary Holding Version
1
+ # @qntx/wallet-native
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ React Native host for [`@qntx/wallet`](https://www.npmjs.com/package/@qntx/wallet): runs the
4
+ wallet kernel on its own Hermes runtime on a separate thread (the Nitro `KernelHost` object
5
+ shuttles CBOR messages to it across JSI), keeps the
6
+ encrypted vault under the platform's never-backed-up directory, and makes the platform keystore
7
+ (Secure Enclave on iOS, Android Keystore) the vault factor — every unlock and every signing
8
+ request is gated by a system user-presence prompt rendered with kernel-owned text.
9
+
10
+ ```bash
11
+ npm install @qntx/wallet-native @qntx/wallet @qntx/nostr-native @qntx/spark-native
12
+ ```
13
+
14
+ Peer dependencies (exact `@qntx/*` pins are required — the host/kernel protocol and the bundled
15
+ kernel artifacts are built against those releases):
16
+
17
+ | package | range |
18
+ | ---------------------------- | ---------------- |
19
+ | `@qntx/wallet` | `0.4.0` (exact) |
20
+ | `@qntx/nostr-native` | `0.15.3` (exact) |
21
+ | `@qntx/spark-native` | `0.5.2` (exact) |
22
+ | `react-native` | `~0.88.0` |
23
+ | `react-native-nitro-modules` | `~0.37.1` |
24
+
25
+ The package registers one Nitro hybrid object, `KernelHost`. Everything else the kernel needs —
26
+ keystore, passkey ceremonies, lifecycle, pbkdf2, the vault directory — is injected as JSI host
27
+ objects inside the kernel runtime and is never registered with Nitro. **No Nitro patch or
28
+ `nitro::install` call is needed in the consuming app.**
29
+
30
+ ## Expo configuration
31
+
32
+ ```ts
33
+ // app.config.ts
34
+ export default {
35
+ ios: {
36
+ bundleIdentifier: "com.example.app",
37
+ infoPlist: { NSFaceIDUsageDescription: "Verify your identity to unlock your wallet" },
38
+ },
39
+ android: { package: "com.example.app" },
40
+ plugins: [
41
+ ["expo-build-properties", { ios: { useFrameworks: "static" }, android: { minSdkVersion: 30 } }],
42
+ ],
43
+ };
44
+ ```
45
+
46
+ - `ios.infoPlist.NSFaceIDUsageDescription` — required: the keystore evaluates device-owner
47
+ presence through `LAContext`, which needs the Face ID purpose string.
48
+ - `ios.useFrameworks: "static"` is supported.
49
+ - `android.minSdkVersion: 30` — required: per-use Keystore keys that accept a biometric _or_ the
50
+ device credential need API 30 (`setUserAuthenticationParameters` semantics).
51
+
52
+ ## Usage
53
+
54
+ ```ts
55
+ import { createNativeWallet } from "@qntx/wallet-native";
56
+
57
+ const wallet = await createNativeWallet({
58
+ passkey: {
59
+ rpId: "example.com",
60
+ rpName: "Example",
61
+ walletSalt: "example/wallet",
62
+ vaultSalt: "example/vault",
63
+ backupSalt: "example/backup",
64
+ },
65
+ autoLock: { platform: "native", backgroundSeconds: 60 },
66
+ });
67
+ ```
68
+
69
+ - `passkey` — relying-party config for the passkey _wallet_ flow (create/restore with a synced
70
+ passkey). `rpId`, `walletSalt` and `vaultSalt` are permanent identifiers: changing them makes
71
+ existing passkey factors and passkey-derived wallets unrecoverable. `backupSalt` is the PRF
72
+ salt for the cloud-backup factor — a third, distinct permanent label.
73
+ - `autoLock` — must be `{ platform: "native", backgroundSeconds }`. The native host owns the
74
+ signal: it calls `host.lock()` when the app has spent `backgroundSeconds` in the background.
75
+ A platform authentication prompt on screen does **not** count as background time.
76
+
77
+ ## Passkey domains
78
+
79
+ Passkey wallets need the platform's domain association:
80
+
81
+ - **iOS** — `webcredentials:<rpId>` associated domain (requires a paid developer account; free
82
+ signing cannot hold the entitlement). **Native iOS passkeys are not implemented yet** — the
83
+ ceremony returns `factor-unavailable`, so `createWithPasskey`/`restoreWithPasskey` cannot be
84
+ offered on iOS; ship mnemonic restore.
85
+ - **Android** — Credential Manager resolves the RP against
86
+ `https://<rpId>/.well-known/assetlinks.json` with
87
+ `delegate_permission/common.get_login_creds` naming the app's package and SHA-256 signing
88
+ certificate. Android passkeys are implemented via `androidx.credentials`.
89
+
90
+ ## Vault storage and backup
91
+
92
+ - **iOS** — the vault image lives under `Application Support/qntx-wallet/`, created with
93
+ `NSFileProtectionComplete` and `NSURLIsExcludedFromBackupKey`. Device-to-device migration and
94
+ iCloud backups must not carry the vault: it is bound to this device's hardware factor, which
95
+ cannot leave the Secure Enclave.
96
+ - **Android** — the vault image lives under `noBackupFilesDir/qntx-wallet`, excluded from
97
+ Auto Backup by construction.
98
+
99
+ ## Security notes
100
+
101
+ - The hardware vault factor is the only factor kind on this platform. Unlock, vault creation,
102
+ mnemonic/key export, wallet deletion, cloud-backup enable, reset and every `chain.sign` /
103
+ `spark.approve` request open a platform authentication ceremony with kernel-owned prompt text
104
+ (Simplified Chinese under a `zh*` locale, English otherwise).
105
+ - `vault.reset` authenticates the device owner first, then erases the vault and all hardware
106
+ keys. Devices without a lock screen return `factor-unavailable` — the host should send the
107
+ user to set one up.
108
+ - `chain.sign` prompts _before_ any secret derivation: the kernel normalizes and decodes the
109
+ intent, shows the prompt, and only then touches key material.
110
+ - The kernel serializes ceremonies — two concurrent signing requests cannot race two system
111
+ prompts.
112
+ - `kernel/generated/kernel.manifest.json` inside the package records the kernel bundle's
113
+ SHA-256 and every pinned protocol dependency; `scripts/compile-kernel.mjs` verifies it when
114
+ the host is natively built and fails the build on a mismatch. At kernel start the host
115
+ checks each backend's `abiVersion`.
116
+ - On the iOS simulator the Secure Enclave token and the `.privateKeyUsage` flag are compiled
117
+ out, so ceremonies run on software keys through the same code path. The app must be
118
+ code-signed (the default ad-hoc "Sign to Run Locally" identity suffices) — an unsigned
119
+ binary fails every `SecItem` call with `errSecMissingEntitlement` (-34018), surfaced as
120
+ `unavailable`.
121
+
122
+ ## License
123
+
124
+ MIT OR Apache-2.0
@@ -0,0 +1,74 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "WalletNative"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = "https://github.com/qntx/wallet"
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => min_ios_version_supported }
14
+ s.source = { :git => "https://github.com/qntx/wallet.git" }
15
+ # Swift pods (V1Probe's Nitrogen bridge) depend on this — a module map is required.
16
+ s.static_framework = true
17
+
18
+ s.source_files = [
19
+ "cpp/runtime/**/*.{h,hpp,c,cpp,mm}",
20
+ "cpp/host/**/*.{h,hpp,c,cpp,mm}",
21
+ "cpp/platform/**/*.{h,hpp,c,cpp,mm}",
22
+ ]
23
+ s.public_header_files = "cpp/**/*.hpp"
24
+ s.header_mappings_dir = "cpp"
25
+
26
+ s.pod_target_xcconfig = {
27
+ "DEFINES_MODULE" => "YES",
28
+ "CLANG_CXX_LANGUAGE_STANDARD" => "c++20",
29
+ "HEADER_SEARCH_PATHS" => [
30
+ '"$(PODS_TARGET_SRCROOT)/cpp"',
31
+ '"$(PODS_ROOT)/hermes-engine/destroot/include"',
32
+ '"$(PODS_ROOT)/Headers/Public/SparkNative"',
33
+ '"$(DERIVED_FILE_DIR)/qntx-kernel"',
34
+ ].join(' '),
35
+ }
36
+
37
+ # Compile the shipped kernel-entry.js to bytecode with the app's own hermesc —
38
+ # Hermes only loads its own bytecode version, so precompiled hbc cannot ship.
39
+ # kernel.hpp lands in DERIVED_FILE_DIR/qntx-kernel (a header search path).
40
+ s.script_phase = {
41
+ :name => "Compile Kernel Bytecode",
42
+ :script => <<~SH,
43
+ if [ -z "$REACT_NATIVE_PATH" ]; then
44
+ echo "REACT_NATIVE_PATH is not set"
45
+ exit 1
46
+ fi
47
+ . "$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh"
48
+ "$NODE_BINARY" "$PODS_TARGET_SRCROOT/scripts/compile-kernel.mjs" \
49
+ "$DERIVED_FILE_DIR/qntx-kernel" "$REACT_NATIVE_PATH"
50
+ SH
51
+ :execution_position => :before_compile,
52
+ :input_files => [
53
+ "$(PODS_TARGET_SRCROOT)/kernel/generated/kernel-entry.js",
54
+ "$(PODS_TARGET_SRCROOT)/kernel/generated/kernel.manifest.json",
55
+ "$(PODS_TARGET_SRCROOT)/scripts/compile-kernel.mjs",
56
+ ],
57
+ :output_files => ["$(DERIVED_FILE_DIR)/qntx-kernel/kernel.hpp"],
58
+ }
59
+
60
+ # The kernel's backends.spark binds the qs_* C ABI; the prebuilt
61
+ # QntxSpark.xcframework ships inside the SparkNative pod.
62
+ s.frameworks = "LocalAuthentication", "Security"
63
+
64
+ s.dependency 'NostrNative'
65
+ s.dependency 'SparkNative'
66
+ s.dependency 'React-jsi'
67
+ s.dependency 'React-callinvoker'
68
+ s.dependency 'React-hermes'
69
+ s.dependency 'hermes-engine'
70
+
71
+ load 'nitrogen/generated/ios/WalletNative+autolinking.rb'
72
+ add_nitrogen_files(s)
73
+ install_modules_dependencies(s)
74
+ end
@@ -0,0 +1,99 @@
1
+ project(WalletNative)
2
+ cmake_minimum_required(VERSION 3.16)
3
+
4
+ set(PACKAGE_NAME WalletNative)
5
+ set(CMAKE_VERBOSE_MAKEFILE ON)
6
+ set(CMAKE_CXX_STANDARD 20)
7
+
8
+ file(GLOB_RECURSE runtime_files CONFIGURE_DEPENDS
9
+ "${CMAKE_CURRENT_SOURCE_DIR}/../cpp/runtime/*.cpp"
10
+ "${CMAKE_CURRENT_SOURCE_DIR}/../cpp/host/*.cpp"
11
+ "${CMAKE_CURRENT_SOURCE_DIR}/../cpp/platform/*.cpp"
12
+ "${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp/*.cpp"
13
+ )
14
+
15
+ add_library(${PACKAGE_NAME} SHARED ${runtime_files})
16
+ target_include_directories(${PACKAGE_NAME} PUBLIC
17
+ "${CMAKE_CURRENT_SOURCE_DIR}/../cpp"
18
+ "${CMAKE_CURRENT_SOURCE_DIR}/../cpp/host"
19
+ "${CMAKE_CURRENT_SOURCE_DIR}/../cpp/platform"
20
+ "${CMAKE_CURRENT_BINARY_DIR}/qntx-kernel"
21
+ )
22
+
23
+ # Compile the shipped kernel bundle to bytecode with the app's own hermesc —
24
+ # Hermes only loads its own bytecode version, so precompiled hbc cannot ship.
25
+ # The app's react-native root is passed explicitly (-DQNTX_REACT_NATIVE_PATH)
26
+ # so the script resolves the app's own hermesc version.
27
+ find_program(NODE_EXECUTABLE node REQUIRED)
28
+ if(NOT QNTX_REACT_NATIVE_PATH)
29
+ message(FATAL_ERROR "QNTX_REACT_NATIVE_PATH is not set — the consuming app must pass -DQNTX_REACT_NATIVE_PATH=<react-native dir>")
30
+ endif()
31
+ set(KERNEL_HEADER "${CMAKE_CURRENT_BINARY_DIR}/qntx-kernel/kernel.hpp")
32
+ add_custom_command(
33
+ OUTPUT ${KERNEL_HEADER}
34
+ COMMAND ${NODE_EXECUTABLE}
35
+ "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/compile-kernel.mjs"
36
+ "${CMAKE_CURRENT_BINARY_DIR}/qntx-kernel"
37
+ "${QNTX_REACT_NATIVE_PATH}"
38
+ DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/../kernel/generated/kernel-entry.js"
39
+ "${CMAKE_CURRENT_SOURCE_DIR}/../kernel/generated/kernel.manifest.json"
40
+ "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/compile-kernel.mjs"
41
+ COMMENT "Compiling the kernel bytecode (hermesc)"
42
+ VERBATIM
43
+ )
44
+ add_custom_target(qntx-kernel-bytecode DEPENDS ${KERNEL_HEADER})
45
+ add_dependencies(${PACKAGE_NAME} qntx-kernel-bytecode)
46
+
47
+ # The kernel's backends.spark binds the qs_* C ABI: the prebuilt
48
+ # libqntx_spark.so + header ship inside the app's @qntx/spark-native package
49
+ # (dir passed as -DQNTX_SPARK_NATIVE_PATH, resolved like the react-native dir).
50
+ if(NOT QNTX_SPARK_NATIVE_PATH)
51
+ message(FATAL_ERROR "QNTX_SPARK_NATIVE_PATH is not set — the consuming app must pass -DQNTX_SPARK_NATIVE_PATH=<@qntx/spark-native dir>")
52
+ endif()
53
+ set(QNTX_SPARK_LIBDIR "${QNTX_SPARK_NATIVE_PATH}/android/src/main/jniLibs/${ANDROID_ABI}")
54
+ if(NOT EXISTS "${QNTX_SPARK_LIBDIR}/libqntx_spark.so")
55
+ message(FATAL_ERROR "libqntx_spark.so for ${ANDROID_ABI} not found in ${QNTX_SPARK_LIBDIR}")
56
+ endif()
57
+ # The .so carries SONAME since 0.5.1, so the path link records the soname in
58
+ # DT_NEEDED — the loader resolves it from the packaged jniLibs. GLOBAL: this
59
+ # target's link libraries are transitive (plain signature), so
60
+ # add_subdirectory consumers (v1-probe) see and link the same imported target.
61
+ add_library(qntx_spark SHARED IMPORTED GLOBAL)
62
+ set_target_properties(qntx_spark PROPERTIES
63
+ IMPORTED_LOCATION "${QNTX_SPARK_LIBDIR}/libqntx_spark.so")
64
+ target_include_directories(${PACKAGE_NAME} PRIVATE "${QNTX_SPARK_NATIVE_PATH}/include")
65
+
66
+ find_package(qntx_nostr-native REQUIRED CONFIG)
67
+
68
+ # Adds the nitrogen-generated sources, the BUILDING_..._GENERATED_CMAKE_PROJECT
69
+ # define they require, and the fbjni/ReactAndroid/NitroModules prefab links.
70
+ include(${CMAKE_CURRENT_SOURCE_DIR}/../nitrogen/generated/android/WalletNative+autolinking.cmake)
71
+
72
+ # Same Hermes link pattern as react-native-worklets 0.10.1: the hermes-engine
73
+ # prefab target is hermesvm on RN >= 0.82 and libhermes before that. The version
74
+ # minor comes from the ReactAndroid prefab config.
75
+ find_package(hermes-engine REQUIRED CONFIG)
76
+
77
+ # The generated autolinking.cmake already defines the FOLLY flags this module
78
+ # needs; the runtime only adds RN_SERIALIZABLE_STATE. (folly-flags.cmake itself
79
+ # is only reachable relative to the consuming app's node_modules, which this
80
+ # module cannot assume when it is built standalone.)
81
+ target_compile_definitions(${PACKAGE_NAME} PRIVATE -DRN_SERIALIZABLE_STATE)
82
+
83
+ find_library(LOG_LIB log)
84
+
85
+ target_link_libraries(
86
+ ${PACKAGE_NAME}
87
+ fbjni::fbjni
88
+ ReactAndroid::jsi
89
+ qntx_spark
90
+ qntx_nostr-native::NostrNative
91
+ ${LOG_LIB}
92
+ android
93
+ )
94
+
95
+ if(ReactAndroid_VERSION_MINOR GREATER_EQUAL 82)
96
+ target_link_libraries(${PACKAGE_NAME} hermes-engine::hermesvm)
97
+ else()
98
+ target_link_libraries(${PACKAGE_NAME} hermes-engine::libhermes)
99
+ endif()
@@ -0,0 +1,177 @@
1
+ buildscript {
2
+ repositories {
3
+ google()
4
+ mavenCentral()
5
+ }
6
+ dependencies {
7
+
8
+ classpath "com.android.tools.build:gradle:9.2.1"
9
+ }
10
+ }
11
+
12
+ def reactNativeArchitectures() {
13
+ def value = rootProject.getProperties().get("reactNativeArchitectures")
14
+ return value ? value.split(",") : ["x86", "x86_64", "armeabi-v7a", "arm64-v8a"]
15
+ }
16
+
17
+ def isNewArchitectureEnabled() {
18
+ return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
19
+ }
20
+
21
+ apply plugin: "com.android.library"
22
+ apply plugin: 'org.jetbrains.kotlin.android'
23
+ apply from: '../nitrogen/generated/android/WalletNative+autolinking.gradle'
24
+
25
+ if (isNewArchitectureEnabled()) {
26
+ apply plugin: "com.facebook.react"
27
+
28
+
29
+ }
30
+
31
+ def getExtOrDefault(name) {
32
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["WalletNative_" + name]
33
+ }
34
+
35
+ def getExtOrIntegerDefault(name) {
36
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["WalletNative_" + name]).toInteger()
37
+ }
38
+
39
+ android {
40
+ namespace = "com.margelo.nitro.qntxwallet"
41
+
42
+ ndkVersion = getExtOrDefault("ndkVersion")
43
+ compileSdkVersion = getExtOrIntegerDefault("compileSdkVersion")
44
+
45
+ defaultConfig {
46
+ // WD-024: per-use Keystore keys that accept a biometric or the device
47
+ // credential through BiometricPrompt need API 30.
48
+ minSdkVersion = 30
49
+ targetSdkVersion = getExtOrIntegerDefault("targetSdkVersion")
50
+
51
+ consumerProguardFiles "consumer-rules.pro"
52
+
53
+ externalNativeBuild {
54
+ cmake {
55
+ cppFlags "-frtti -fexceptions -Wall -Wextra -fstack-protector-all"
56
+ // The consuming app's react-native root — resolved through node module
57
+ // resolution from the app project (as RN's own settings.gradle does);
58
+ // compile-kernel.mjs then resolves hermesc through it.
59
+ arguments "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON",
60
+ // The consuming app's react-native dir, resolved through node
61
+ // module resolution from the app project — a library cannot reach
62
+ // it by walking up from its own tree.
63
+ "-DQNTX_REACT_NATIVE_PATH=" + providers.exec {
64
+ workingDir = rootProject.projectDir
65
+ commandLine "node", "-p", "require('path').dirname(require.resolve('react-native/package.json'))"
66
+ }.standardOutput.asText.get().trim(),
67
+ // The app's @qntx/spark-native dir (header + per-ABI libqntx_spark.so),
68
+ // resolved the same way.
69
+ "-DQNTX_SPARK_NATIVE_PATH=" + providers.exec {
70
+ workingDir = rootProject.projectDir
71
+ commandLine "node", "-p", "require('path').dirname(require.resolve('@qntx/spark-native/package.json'))"
72
+ }.standardOutput.asText.get().trim()
73
+ abiFilters (*reactNativeArchitectures())
74
+ buildTypes {
75
+ debug {
76
+ cppFlags "-O1 -g"
77
+ }
78
+ release {
79
+ cppFlags "-O2"
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
85
+
86
+ externalNativeBuild {
87
+ cmake {
88
+ path = file("CMakeLists.txt")
89
+ }
90
+ }
91
+
92
+ packagingOptions {
93
+ excludes = [
94
+ "META-INF",
95
+ "META-INF/**",
96
+ "**/libNitroModules.so",
97
+ "**/libc++_shared.so",
98
+ "**/libfbjni.so",
99
+ "**/libjsi.so",
100
+ "**/libfolly_json.so",
101
+ "**/libfolly_runtime.so",
102
+ "**/libglog.so",
103
+ "**/libhermes.so",
104
+ "**/libhermes-executor-debug.so",
105
+ "**/libhermes_executor.so",
106
+ "**/libreactnative.so",
107
+ "**/libreactnativejni.so",
108
+ "**/libturbomodulejsijni.so",
109
+ "**/libreact_nativemodule_core.so",
110
+ "**/libjscexecutor.so",
111
+ // Linked, not bundled — @qntx/spark-native packages it from its jniLibs.
112
+ "**/libqntx_spark.so"
113
+ ]
114
+ }
115
+
116
+ buildFeatures {
117
+ buildConfig = true
118
+ prefab = true
119
+ }
120
+
121
+ buildTypes {
122
+ release {
123
+ minifyEnabled = false
124
+ }
125
+ }
126
+
127
+ lintOptions {
128
+ disable "GradleCompatible"
129
+ }
130
+
131
+ compileOptions {
132
+ sourceCompatibility JavaVersion.VERSION_1_8
133
+ targetCompatibility JavaVersion.VERSION_1_8
134
+ }
135
+
136
+ sourceSets {
137
+ main {
138
+ if (isNewArchitectureEnabled()) {
139
+ java.srcDirs += [
140
+ "${project.buildDir}/generated/source/codegen/java"
141
+ ]
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ dependencies {
148
+ // For > 0.71, this is replaced by `com.facebook.react:react-android:$version` by the React
149
+ // gradle plugin — it supplies the fbjni/ReactAndroid prefab packages the generated CMake
150
+ // autolinking file finds with find_package.
151
+ //noinspection GradleDynamicVersion
152
+ implementation "com.facebook.react:react-native:+"
153
+
154
+ // Hermes engine prefab — the kernel runtime links hermesvm/libhermes
155
+ implementation "com.facebook.react:hermes-android"
156
+
157
+ // NitroModules core (provides the react-native-nitro-modules prefab + HybridObject runtime)
158
+ implementation project(":react-native-nitro-modules")
159
+ implementation project(":qntx_nostr-native")
160
+
161
+ // ProcessLifecycleOwner for the background lock (W7) — the version the app
162
+ // already resolves for androidx.lifecycle.
163
+ implementation "androidx.lifecycle:lifecycle-process:2.6.2"
164
+
165
+ // BiometricPrompt + CryptoObject for the hardware vault factor (W5).
166
+ implementation "androidx.biometric:biometric:1.1.0"
167
+
168
+ // Credential Manager for native passkeys (W6); the Play services provider
169
+ // backs Google Password Manager.
170
+ implementation "androidx.credentials:credentials:1.6.0"
171
+ implementation "androidx.credentials:credentials-play-services-auth:1.6.0"
172
+ }
173
+
174
+ repositories {
175
+ mavenCentral()
176
+ google()
177
+ }
@@ -0,0 +1,4 @@
1
+ # Called from native code over JNI or bound to JNI by method-name mangling;
2
+ # R8 would otherwise strip or rename them (the classes have no direct Java
3
+ # call sites inside the app).
4
+ -keep class com.margelo.nitro.qntxwallet.** { *; }
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,29 @@
1
+ #include "WalletNativeOnLoad.hpp"
2
+ #include "host/VaultDirectory.hpp"
3
+ #include "runtime/platform/KernelPbkdf2.hpp"
4
+ #include <fbjni/fbjni.h>
5
+ #include <jni.h>
6
+
7
+ namespace qntx {
8
+ void kernelVaultDirectoryInit(facebook::jni::alias_ref<jclass> clazz);
9
+ void kernelLifecycleInit(facebook::jni::alias_ref<jclass> clazz);
10
+ void kernelKeystoreInit(facebook::jni::alias_ref<jclass> clazz);
11
+ void kernelPasskeyInit(facebook::jni::alias_ref<jclass> clazz);
12
+ }
13
+
14
+ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
15
+ return facebook::jni::initialize(vm, []() {
16
+ qntx::kernelVaultDirectoryInit(
17
+ facebook::jni::findClassLocal("com/margelo/nitro/qntxwallet/VaultDirectory"));
18
+ qntx::kernelLifecycleInit(
19
+ facebook::jni::findClassLocal("com/margelo/nitro/qntxwallet/AppLifecycle"));
20
+ qntx::kernelKeystoreInit(
21
+ facebook::jni::findClassLocal("com/margelo/nitro/qntxwallet/Keystore"));
22
+ qntx::kernelPasskeyInit(
23
+ facebook::jni::findClassLocal("com/margelo/nitro/qntxwallet/Passkey"));
24
+ margelo::nitro::qntxwallet::registerAllNatives();
25
+ qntx::kernelPbkdf2OnLoad(
26
+ facebook::jni::Environment::current(),
27
+ "com/margelo/nitro/qntxwallet/KernelPbkdf2");
28
+ });
29
+ }
@@ -0,0 +1,35 @@
1
+ package com.margelo.nitro.qntxwallet
2
+
3
+ import android.os.Handler
4
+ import android.os.Looper
5
+ import androidx.annotation.Keep
6
+ import androidx.lifecycle.DefaultLifecycleObserver
7
+ import androidx.lifecycle.LifecycleOwner
8
+ import androidx.lifecycle.ProcessLifecycleOwner
9
+ import com.facebook.proguard.annotations.DoNotStrip
10
+
11
+ /**
12
+ * Process-wide lifecycle: the C++ host calls [install] once (fbjni, first
13
+ * KernelHost.start); the observer forwards app background/foreground
14
+ * transitions to the native dispatcher on the main looper.
15
+ */
16
+ @DoNotStrip
17
+ @Keep
18
+ object AppLifecycle {
19
+
20
+ @JvmStatic
21
+ fun install() {
22
+ Handler(Looper.getMainLooper()).post {
23
+ ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
24
+ override fun onStop(owner: LifecycleOwner) = onAppBackground()
25
+ override fun onStart(owner: LifecycleOwner) = onAppForeground()
26
+ })
27
+ }
28
+ }
29
+
30
+ @JvmStatic
31
+ private external fun onAppBackground()
32
+
33
+ @JvmStatic
34
+ private external fun onAppForeground()
35
+ }
@@ -0,0 +1,59 @@
1
+ package com.margelo.nitro.qntxwallet;
2
+
3
+ import java.security.GeneralSecurityException;
4
+ import java.util.Arrays;
5
+ import javax.crypto.Mac;
6
+ import javax.crypto.spec.SecretKeySpec;
7
+
8
+ /**
9
+ * PBKDF2 for the kernel runtime, called from C++ over JNI on the dedicated KDF worker thread.
10
+ *
11
+ * Byte-exact RFC 8018 derivation over javax.crypto.Mac — there is no SecretKeyFactory/PBEKeySpec
12
+ * path, because chars decoded from the password's UTF-8 bytes are provider-specific. All
13
+ * intermediate arrays the VM hands back (u, t, out) are wiped. Note that SecretKeySpec and JCA
14
+ * provider internals retain their own non-wipeable copies of the key material; the caller-side
15
+ * buffers are still zeroed by JniByteArray after return.
16
+ */
17
+ public final class KernelPbkdf2 {
18
+ private KernelPbkdf2() {}
19
+
20
+ public static byte[] derive(String hmacAlgorithm, byte[] password, byte[] salt, int iterations, int length)
21
+ throws GeneralSecurityException {
22
+ SecretKeySpec key = new SecretKeySpec(password, hmacAlgorithm);
23
+ Mac mac = Mac.getInstance(hmacAlgorithm);
24
+ mac.init(key);
25
+ int blockSize = mac.getMacLength();
26
+ int blocks = (length + blockSize - 1) / blockSize;
27
+ byte[] out = new byte[blocks * blockSize];
28
+ byte[] index = new byte[4];
29
+ int offset = 0;
30
+ try {
31
+ for (int block = 1; block <= blocks; block++) {
32
+ index[0] = (byte) (block >>> 24);
33
+ index[1] = (byte) (block >>> 16);
34
+ index[2] = (byte) (block >>> 8);
35
+ index[3] = (byte) block;
36
+ mac.update(salt);
37
+ byte[] u = mac.doFinal(index);
38
+ byte[] t = u.clone();
39
+ for (int j = 1; j < iterations; j++) {
40
+ byte[] next = mac.doFinal(u);
41
+ Arrays.fill(u, (byte) 0);
42
+ u = next;
43
+ for (int k = 0; k < u.length; k++) {
44
+ t[k] ^= u[k];
45
+ }
46
+ }
47
+ System.arraycopy(t, 0, out, offset, blockSize);
48
+ offset += blockSize;
49
+ Arrays.fill(u, (byte) 0);
50
+ Arrays.fill(t, (byte) 0);
51
+ }
52
+ return Arrays.copyOf(out, length);
53
+ } finally {
54
+ Arrays.fill(out, (byte) 0);
55
+ Arrays.fill(index, (byte) 0);
56
+ Arrays.fill(password, (byte) 0);
57
+ }
58
+ }
59
+ }