@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,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
|
+
}
|
|
@@ -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
|
+
}
|