@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.
- 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 +61 -4
- package/react-native.config.js +14 -0
- package/scripts/compile-kernel.mjs +118 -0
package/README.md
CHANGED
|
@@ -1,3 +1,124 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @qntx/wallet-native
|
|
2
2
|
|
|
3
|
-
|
|
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,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
|
+
}
|