@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
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// iOS keystore (W5): one Secure Enclave P-256 key per vault factor, tagged
|
|
2
|
+
// org.qntx.wallet.vault.<factor id>, readable only while a device passcode is
|
|
3
|
+
// set and usable only after a fresh user-presence check. Sealing is ECIES with
|
|
4
|
+
// the public key (no prompt); opening decrypts with the private key behind a
|
|
5
|
+
// new LAContext whose reason is the kernel's prompt text, so every unlock,
|
|
6
|
+
// re-verification and per-operation verification is its own system prompt.
|
|
7
|
+
// The simulator has no Secure Enclave: there the key lives in the software
|
|
8
|
+
// keychain with the same user-presence access control.
|
|
9
|
+
#include "../host/Keystore.hpp"
|
|
10
|
+
|
|
11
|
+
#import <Foundation/Foundation.h>
|
|
12
|
+
#import <LocalAuthentication/LocalAuthentication.h>
|
|
13
|
+
#import <Security/Security.h>
|
|
14
|
+
#include <TargetConditionals.h>
|
|
15
|
+
|
|
16
|
+
#include <cstring>
|
|
17
|
+
#include <memory>
|
|
18
|
+
#include <mutex>
|
|
19
|
+
#include <utility>
|
|
20
|
+
|
|
21
|
+
namespace qntx {
|
|
22
|
+
namespace {
|
|
23
|
+
|
|
24
|
+
NSString* const kTagPrefix = @"org.qntx.wallet.vault.";
|
|
25
|
+
|
|
26
|
+
SecKeyAlgorithm algorithm() {
|
|
27
|
+
return kSecKeyAlgorithmECIESEncryptionCofactorVariableIVX963SHA256AESGCM;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
dispatch_queue_t keystoreQueue() {
|
|
31
|
+
static dispatch_queue_t queue =
|
|
32
|
+
dispatch_queue_create("org.qntx.wallet.keystore", DISPATCH_QUEUE_SERIAL);
|
|
33
|
+
return queue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// The context of the prompt currently on screen, for keystoreCancel.
|
|
37
|
+
std::mutex g_contextMutex;
|
|
38
|
+
LAContext* g_context = nil;
|
|
39
|
+
|
|
40
|
+
void setCurrentContext(LAContext* context) {
|
|
41
|
+
std::lock_guard<std::mutex> lock(g_contextMutex);
|
|
42
|
+
g_context = context;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
NSData* tagFor(const std::string& factorId) {
|
|
46
|
+
NSString* suffix = [NSString stringWithUTF8String:factorId.c_str()];
|
|
47
|
+
return [[kTagPrefix stringByAppendingString:suffix ?: @""]
|
|
48
|
+
dataUsingEncoding:NSUTF8StringEncoding];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
KeystoreStatus statusOf(NSError* error) {
|
|
52
|
+
if (error == nil) {
|
|
53
|
+
return KeystoreStatus::failed;
|
|
54
|
+
}
|
|
55
|
+
if ([error.domain isEqualToString:LAErrorDomain]) {
|
|
56
|
+
switch (error.code) {
|
|
57
|
+
case LAErrorUserCancel:
|
|
58
|
+
case LAErrorAppCancel:
|
|
59
|
+
case LAErrorSystemCancel:
|
|
60
|
+
case LAErrorUserFallback:
|
|
61
|
+
return KeystoreStatus::cancelled;
|
|
62
|
+
case LAErrorPasscodeNotSet:
|
|
63
|
+
return KeystoreStatus::unavailable;
|
|
64
|
+
default:
|
|
65
|
+
return KeystoreStatus::failed;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if ([error.domain isEqualToString:NSOSStatusErrorDomain]) {
|
|
69
|
+
switch (error.code) {
|
|
70
|
+
case errSecUserCanceled:
|
|
71
|
+
return KeystoreStatus::cancelled;
|
|
72
|
+
case errSecItemNotFound:
|
|
73
|
+
return KeystoreStatus::invalidated;
|
|
74
|
+
default:
|
|
75
|
+
return KeystoreStatus::failed;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return KeystoreStatus::failed;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
KeystoreStatus statusOf(CFErrorRef error) {
|
|
82
|
+
return statusOf((__bridge NSError*)error);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
void deleteKey(NSData* tag) {
|
|
86
|
+
SecItemDelete((__bridge CFDictionaryRef) @{
|
|
87
|
+
(id)kSecClass : (id)kSecClassKey,
|
|
88
|
+
(id)kSecAttrApplicationTag : tag,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
bool deviceOwnerAuthenticationAvailable() {
|
|
93
|
+
LAContext* probe = [LAContext new];
|
|
94
|
+
return [probe canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:nil];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
KeystoreResult sealNow(const std::string& factorId, const SecretBytes& secret) {
|
|
98
|
+
if (!deviceOwnerAuthenticationAvailable()) {
|
|
99
|
+
return {KeystoreStatus::unavailable, {}};
|
|
100
|
+
}
|
|
101
|
+
NSData* tag = tagFor(factorId);
|
|
102
|
+
deleteKey(tag);
|
|
103
|
+
SecAccessControlCreateFlags flags = kSecAccessControlUserPresence;
|
|
104
|
+
#if !TARGET_OS_SIMULATOR
|
|
105
|
+
flags |= kSecAccessControlPrivateKeyUsage;
|
|
106
|
+
#endif
|
|
107
|
+
CFErrorRef error = nullptr;
|
|
108
|
+
SecAccessControlRef access = SecAccessControlCreateWithFlags(
|
|
109
|
+
kCFAllocatorDefault, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, flags, &error);
|
|
110
|
+
if (access == nullptr) {
|
|
111
|
+
if (error != nullptr) {
|
|
112
|
+
CFRelease(error);
|
|
113
|
+
}
|
|
114
|
+
return {KeystoreStatus::unavailable, {}};
|
|
115
|
+
}
|
|
116
|
+
NSMutableDictionary* attributes = [@{
|
|
117
|
+
(id)kSecAttrKeyType : (id)kSecAttrKeyTypeECSECPrimeRandom,
|
|
118
|
+
(id)kSecAttrKeySizeInBits : @256,
|
|
119
|
+
(id)kSecPrivateKeyAttrs : @{
|
|
120
|
+
(id)kSecAttrIsPermanent : @YES,
|
|
121
|
+
(id)kSecAttrApplicationTag : tag,
|
|
122
|
+
(id)kSecAttrAccessControl : (__bridge id)access,
|
|
123
|
+
},
|
|
124
|
+
} mutableCopy];
|
|
125
|
+
#if !TARGET_OS_SIMULATOR
|
|
126
|
+
attributes[(id)kSecAttrTokenID] = (id)kSecAttrTokenIDSecureEnclave;
|
|
127
|
+
#endif
|
|
128
|
+
SecKeyRef privateKey = SecKeyCreateRandomKey((__bridge CFDictionaryRef)attributes, &error);
|
|
129
|
+
CFRelease(access);
|
|
130
|
+
if (privateKey == nullptr) {
|
|
131
|
+
if (error != nullptr) {
|
|
132
|
+
CFRelease(error);
|
|
133
|
+
}
|
|
134
|
+
return {KeystoreStatus::unavailable, {}};
|
|
135
|
+
}
|
|
136
|
+
SecKeyRef publicKey = SecKeyCopyPublicKey(privateKey);
|
|
137
|
+
CFRelease(privateKey);
|
|
138
|
+
if (publicKey == nullptr) {
|
|
139
|
+
deleteKey(tag);
|
|
140
|
+
return {KeystoreStatus::failed, {}};
|
|
141
|
+
}
|
|
142
|
+
CFDataRef plain = CFDataCreateWithBytesNoCopy(
|
|
143
|
+
kCFAllocatorDefault, secret.data(), static_cast<CFIndex>(secret.size()), kCFAllocatorNull);
|
|
144
|
+
CFDataRef sealed = SecKeyCreateEncryptedData(publicKey, algorithm(), plain, &error);
|
|
145
|
+
CFRelease(plain);
|
|
146
|
+
CFRelease(publicKey);
|
|
147
|
+
if (sealed == nullptr) {
|
|
148
|
+
if (error != nullptr) {
|
|
149
|
+
CFRelease(error);
|
|
150
|
+
}
|
|
151
|
+
deleteKey(tag);
|
|
152
|
+
return {KeystoreStatus::failed, {}};
|
|
153
|
+
}
|
|
154
|
+
SecretBytes out(CFDataGetBytePtr(sealed), static_cast<size_t>(CFDataGetLength(sealed)));
|
|
155
|
+
CFRelease(sealed);
|
|
156
|
+
return {KeystoreStatus::ok, std::move(out)};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
KeystoreResult openNow(
|
|
160
|
+
const std::string& factorId,
|
|
161
|
+
const std::vector<uint8_t>& sealed,
|
|
162
|
+
const KeystorePrompt& prompt) {
|
|
163
|
+
LAContext* context = [LAContext new];
|
|
164
|
+
context.touchIDAuthenticationAllowableReuseDuration = 0;
|
|
165
|
+
context.localizedReason = @(prompt.description.c_str());
|
|
166
|
+
CFTypeRef keyRef = nullptr;
|
|
167
|
+
const OSStatus found = SecItemCopyMatching(
|
|
168
|
+
(__bridge CFDictionaryRef) @{
|
|
169
|
+
(id)kSecClass : (id)kSecClassKey,
|
|
170
|
+
(id)kSecAttrKeyType : (id)kSecAttrKeyTypeECSECPrimeRandom,
|
|
171
|
+
(id)kSecAttrApplicationTag : tagFor(factorId),
|
|
172
|
+
(id)kSecReturnRef : @YES,
|
|
173
|
+
(id)kSecUseAuthenticationContext : context,
|
|
174
|
+
},
|
|
175
|
+
&keyRef);
|
|
176
|
+
auto privateKey = static_cast<SecKeyRef>(const_cast<void*>(keyRef));
|
|
177
|
+
if (found == errSecItemNotFound) {
|
|
178
|
+
return {KeystoreStatus::invalidated, {}};
|
|
179
|
+
}
|
|
180
|
+
if (found != errSecSuccess || privateKey == nullptr) {
|
|
181
|
+
return {KeystoreStatus::failed, {}};
|
|
182
|
+
}
|
|
183
|
+
setCurrentContext(context);
|
|
184
|
+
CFDataRef cipher = CFDataCreate(
|
|
185
|
+
kCFAllocatorDefault, sealed.data(), static_cast<CFIndex>(sealed.size()));
|
|
186
|
+
CFErrorRef error = nullptr;
|
|
187
|
+
// Blocks this queue while the system verification is on screen.
|
|
188
|
+
CFDataRef plain = SecKeyCreateDecryptedData(privateKey, algorithm(), cipher, &error);
|
|
189
|
+
setCurrentContext(nil);
|
|
190
|
+
CFRelease(cipher);
|
|
191
|
+
CFRelease(privateKey);
|
|
192
|
+
if (plain == nullptr) {
|
|
193
|
+
const KeystoreStatus status = statusOf(error);
|
|
194
|
+
if (error != nullptr) {
|
|
195
|
+
CFRelease(error);
|
|
196
|
+
}
|
|
197
|
+
return {status, {}};
|
|
198
|
+
}
|
|
199
|
+
const auto length = static_cast<size_t>(CFDataGetLength(plain));
|
|
200
|
+
SecretBytes out(CFDataGetBytePtr(plain), length);
|
|
201
|
+
// The KEK must not outlive this copy: wipe the framework's buffer before
|
|
202
|
+
// releasing it (it is our sole reference).
|
|
203
|
+
std::memset(const_cast<UInt8*>(CFDataGetBytePtr(plain)), 0, length);
|
|
204
|
+
CFRelease(plain);
|
|
205
|
+
return {KeystoreStatus::ok, std::move(out)};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
} // namespace
|
|
209
|
+
|
|
210
|
+
void keystoreSeal(
|
|
211
|
+
const std::string& factorId,
|
|
212
|
+
SecretBytes secret,
|
|
213
|
+
KeystorePrompt,
|
|
214
|
+
KeystoreDone done) {
|
|
215
|
+
// Blocks capture C++ references as references: copy everything the
|
|
216
|
+
// asynchronous work needs into values first.
|
|
217
|
+
const std::string alias = factorId;
|
|
218
|
+
auto input = std::make_shared<SecretBytes>(std::move(secret));
|
|
219
|
+
auto finish = std::make_shared<KeystoreDone>(std::move(done));
|
|
220
|
+
dispatch_async(keystoreQueue(), ^{
|
|
221
|
+
(*finish)(sealNow(alias, *input));
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
void keystoreOpen(
|
|
226
|
+
const std::string& factorId,
|
|
227
|
+
std::vector<uint8_t> sealed,
|
|
228
|
+
KeystorePrompt prompt,
|
|
229
|
+
KeystoreDone done) {
|
|
230
|
+
const std::string alias = factorId;
|
|
231
|
+
auto finish = std::make_shared<KeystoreDone>(std::move(done));
|
|
232
|
+
dispatch_async(keystoreQueue(), ^{
|
|
233
|
+
(*finish)(openNow(alias, sealed, prompt));
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
void keystoreAuthenticate(KeystorePrompt prompt, KeystoreDone done) {
|
|
238
|
+
auto finish = std::make_shared<KeystoreDone>(std::move(done));
|
|
239
|
+
dispatch_async(keystoreQueue(), ^{
|
|
240
|
+
LAContext* context = [LAContext new];
|
|
241
|
+
context.touchIDAuthenticationAllowableReuseDuration = 0;
|
|
242
|
+
NSError* unavailable = nil;
|
|
243
|
+
if (![context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&unavailable]) {
|
|
244
|
+
(*finish)(KeystoreResult{KeystoreStatus::unavailable, {}});
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
setCurrentContext(context);
|
|
248
|
+
// Keep the serial queue busy until the reply: one prompt at a time.
|
|
249
|
+
dispatch_semaphore_t replied = dispatch_semaphore_create(0);
|
|
250
|
+
__block KeystoreStatus status = KeystoreStatus::failed;
|
|
251
|
+
[context evaluatePolicy:LAPolicyDeviceOwnerAuthentication
|
|
252
|
+
localizedReason:@(prompt.description.c_str())
|
|
253
|
+
reply:^(BOOL success, NSError* error) {
|
|
254
|
+
status = success ? KeystoreStatus::ok : statusOf(error);
|
|
255
|
+
dispatch_semaphore_signal(replied);
|
|
256
|
+
}];
|
|
257
|
+
dispatch_semaphore_wait(replied, DISPATCH_TIME_FOREVER);
|
|
258
|
+
setCurrentContext(nil);
|
|
259
|
+
(*finish)(KeystoreResult{status, {}});
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
void keystoreClear(KeystoreDone done) {
|
|
264
|
+
auto finish = std::make_shared<KeystoreDone>(std::move(done));
|
|
265
|
+
dispatch_async(keystoreQueue(), ^{
|
|
266
|
+
CFTypeRef found = nullptr;
|
|
267
|
+
const OSStatus status = SecItemCopyMatching(
|
|
268
|
+
(__bridge CFDictionaryRef) @{
|
|
269
|
+
(id)kSecClass : (id)kSecClassKey,
|
|
270
|
+
(id)kSecMatchLimit : (id)kSecMatchLimitAll,
|
|
271
|
+
(id)kSecReturnAttributes : @YES,
|
|
272
|
+
},
|
|
273
|
+
&found);
|
|
274
|
+
if (status == errSecSuccess && found != nullptr) {
|
|
275
|
+
NSArray* items = (__bridge_transfer NSArray*)found;
|
|
276
|
+
NSData* prefix = [kTagPrefix dataUsingEncoding:NSUTF8StringEncoding];
|
|
277
|
+
for (NSDictionary* item in items) {
|
|
278
|
+
NSData* tag = item[(id)kSecAttrApplicationTag];
|
|
279
|
+
if ([tag isKindOfClass:[NSData class]] && tag.length > prefix.length &&
|
|
280
|
+
[[tag subdataWithRange:NSMakeRange(0, prefix.length)] isEqualToData:prefix]) {
|
|
281
|
+
deleteKey(tag);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const bool ok = status == errSecSuccess || status == errSecItemNotFound;
|
|
286
|
+
(*finish)(KeystoreResult{ok ? KeystoreStatus::ok : KeystoreStatus::failed, {}});
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
void keystoreCancel() {
|
|
291
|
+
std::lock_guard<std::mutex> lock(g_contextMutex);
|
|
292
|
+
[g_context invalidate];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
std::string keystoreLocale() {
|
|
296
|
+
NSString* language = NSLocale.preferredLanguages.firstObject;
|
|
297
|
+
return language == nil ? "en" : std::string(language.UTF8String);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
} // namespace qntx
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Android background-lock lifecycle: the Kotlin AppLifecycle object registers
|
|
2
|
+
// a ProcessLifecycleOwner observer on the main looper; its DefaultLifecycleObserver
|
|
3
|
+
// callbacks reach the dispatcher through the JNI exports below.
|
|
4
|
+
#include "../host/Lifecycle.hpp"
|
|
5
|
+
|
|
6
|
+
#ifdef __ANDROID__
|
|
7
|
+
|
|
8
|
+
#include <fbjni/fbjni.h>
|
|
9
|
+
#include <jni.h>
|
|
10
|
+
#include <stdexcept>
|
|
11
|
+
|
|
12
|
+
namespace qntx {
|
|
13
|
+
namespace {
|
|
14
|
+
facebook::jni::global_ref<jclass> g_appLifecycleClass;
|
|
15
|
+
} // namespace
|
|
16
|
+
|
|
17
|
+
void kernelLifecycleInit(facebook::jni::alias_ref<jclass> clazz) {
|
|
18
|
+
g_appLifecycleClass = facebook::jni::make_global(clazz);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
int64_t sleepInclusiveMonotonicMs() {
|
|
22
|
+
// CLOCK_BOOTTIME counts suspend; CLOCK_MONOTONIC does not.
|
|
23
|
+
struct timespec ts;
|
|
24
|
+
::clock_gettime(CLOCK_BOOTTIME, &ts);
|
|
25
|
+
return static_cast<int64_t>(ts.tv_sec) * 1000 + ts.tv_nsec / 1000000;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
void installLifecycleObserver() {
|
|
29
|
+
static std::once_flag once;
|
|
30
|
+
std::call_once(once, [] {
|
|
31
|
+
facebook::jni::ThreadScope threadScope;
|
|
32
|
+
if (!g_appLifecycleClass) {
|
|
33
|
+
throw std::runtime_error("lifecycle: JNI_OnLoad init did not run");
|
|
34
|
+
}
|
|
35
|
+
auto install = g_appLifecycleClass->getStaticMethod<void()>("install");
|
|
36
|
+
install(g_appLifecycleClass.get());
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
} // namespace qntx
|
|
41
|
+
|
|
42
|
+
// The Kotlin object's DefaultLifecycleObserver dispatches here.
|
|
43
|
+
extern "C" JNIEXPORT void JNICALL
|
|
44
|
+
Java_com_margelo_nitro_qntxwallet_AppLifecycle_onAppBackground(
|
|
45
|
+
JNIEnv*, jclass) {
|
|
46
|
+
qntx::KernelLifecycleDispatcher::shared().onBackground();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
extern "C" JNIEXPORT void JNICALL
|
|
50
|
+
Java_com_margelo_nitro_qntxwallet_AppLifecycle_onAppForeground(
|
|
51
|
+
JNIEnv*, jclass) {
|
|
52
|
+
qntx::KernelLifecycleDispatcher::shared().onForeground();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#endif // __ANDROID__
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
#import <UIKit/UIKit.h>
|
|
3
|
+
|
|
4
|
+
#include <mutex>
|
|
5
|
+
#include <time.h>
|
|
6
|
+
|
|
7
|
+
#include "../host/Lifecycle.hpp"
|
|
8
|
+
|
|
9
|
+
namespace qntx {
|
|
10
|
+
|
|
11
|
+
int64_t sleepInclusiveMonotonicMs() {
|
|
12
|
+
// CLOCK_MONOTONIC counts sleep (steady_clock/mach_absolute_time does not).
|
|
13
|
+
const uint64_t ns = clock_gettime_nsec_np(CLOCK_MONOTONIC);
|
|
14
|
+
return static_cast<int64_t>(ns / 1000000);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
void installLifecycleObserver() {
|
|
18
|
+
static std::once_flag once;
|
|
19
|
+
std::call_once(once, [] {
|
|
20
|
+
auto* center = NSNotificationCenter.defaultCenter;
|
|
21
|
+
[center addObserverForName:UIApplicationDidEnterBackgroundNotification
|
|
22
|
+
object:nil
|
|
23
|
+
queue:nil
|
|
24
|
+
usingBlock:^(NSNotification*) {
|
|
25
|
+
KernelLifecycleDispatcher::shared().onBackground();
|
|
26
|
+
}];
|
|
27
|
+
[center addObserverForName:UIApplicationWillEnterForegroundNotification
|
|
28
|
+
object:nil
|
|
29
|
+
queue:nil
|
|
30
|
+
usingBlock:^(NSNotification*) {
|
|
31
|
+
KernelLifecycleDispatcher::shared().onForeground();
|
|
32
|
+
}];
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
} // namespace qntx
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Android passkey authenticator: the Kotlin Passkey object runs Credential
|
|
2
|
+
// Manager; this file forwards each request JSON through fbjni with a
|
|
3
|
+
// completion token and resolves the parked completion when Kotlin calls back
|
|
4
|
+
// through nativeComplete (on the main thread).
|
|
5
|
+
#include "../host/Passkey.hpp"
|
|
6
|
+
|
|
7
|
+
#ifdef __ANDROID__
|
|
8
|
+
|
|
9
|
+
#include <fbjni/fbjni.h>
|
|
10
|
+
#include <jni.h>
|
|
11
|
+
|
|
12
|
+
#include <cstdint>
|
|
13
|
+
#include <mutex>
|
|
14
|
+
#include <stdexcept>
|
|
15
|
+
#include <unordered_map>
|
|
16
|
+
#include <utility>
|
|
17
|
+
|
|
18
|
+
namespace qntx {
|
|
19
|
+
namespace {
|
|
20
|
+
|
|
21
|
+
namespace jni = facebook::jni;
|
|
22
|
+
|
|
23
|
+
jni::global_ref<jclass> g_passkeyClass;
|
|
24
|
+
|
|
25
|
+
std::mutex g_mutex;
|
|
26
|
+
int64_t g_nextToken = 1;
|
|
27
|
+
std::unordered_map<int64_t, PasskeyDone> g_pending;
|
|
28
|
+
|
|
29
|
+
int64_t park(PasskeyDone done) {
|
|
30
|
+
std::lock_guard<std::mutex> lock(g_mutex);
|
|
31
|
+
const int64_t token = g_nextToken++;
|
|
32
|
+
g_pending.emplace(token, std::move(done));
|
|
33
|
+
return token;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
PasskeyDone take(int64_t token) {
|
|
37
|
+
std::lock_guard<std::mutex> lock(g_mutex);
|
|
38
|
+
auto found = g_pending.find(token);
|
|
39
|
+
if (found == g_pending.end()) {
|
|
40
|
+
return nullptr;
|
|
41
|
+
}
|
|
42
|
+
auto done = std::move(found->second);
|
|
43
|
+
g_pending.erase(found);
|
|
44
|
+
return done;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
jni::alias_ref<jclass> passkeyClass() {
|
|
48
|
+
if (!g_passkeyClass) {
|
|
49
|
+
throw std::runtime_error("passkey: JNI_OnLoad init did not run");
|
|
50
|
+
}
|
|
51
|
+
return g_passkeyClass;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Runs one Kotlin entry point; a JNI failure completes the call as `failed`
|
|
55
|
+
// instead of leaving it parked forever.
|
|
56
|
+
void dispatch(const char* method, const std::string& requestJson, PasskeyDone done) {
|
|
57
|
+
const int64_t token = park(std::move(done));
|
|
58
|
+
try {
|
|
59
|
+
jni::ThreadScope threadScope;
|
|
60
|
+
auto entry = passkeyClass()->getStaticMethod<void(jlong, jni::alias_ref<jstring>)>(method);
|
|
61
|
+
entry(passkeyClass(), static_cast<jlong>(token), jni::make_jstring(requestJson));
|
|
62
|
+
} catch (...) {
|
|
63
|
+
if (auto parked = take(token)) {
|
|
64
|
+
parked(PasskeyResult{PasskeyStatus::failed, {}});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
} // namespace
|
|
70
|
+
|
|
71
|
+
void kernelPasskeyInit(jni::alias_ref<jclass> clazz) {
|
|
72
|
+
g_passkeyClass = jni::make_global(clazz);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
void passkeyCreate(std::string requestJson, PasskeyDone done) {
|
|
76
|
+
dispatch("create", requestJson, std::move(done));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
void passkeyGet(std::string requestJson, PasskeyDone done) {
|
|
80
|
+
dispatch("get", requestJson, std::move(done));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
void passkeyCancel() {
|
|
84
|
+
jni::ThreadScope threadScope;
|
|
85
|
+
auto method = passkeyClass()->getStaticMethod<void()>("cancel");
|
|
86
|
+
method(passkeyClass());
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
} // namespace qntx
|
|
90
|
+
|
|
91
|
+
extern "C" JNIEXPORT void JNICALL
|
|
92
|
+
Java_com_margelo_nitro_qntxwallet_Passkey_nativeComplete(
|
|
93
|
+
JNIEnv* env, jclass, jlong token, jint status, jstring responseJson) {
|
|
94
|
+
auto done = qntx::take(static_cast<int64_t>(token));
|
|
95
|
+
if (!done) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
qntx::PasskeyResult result;
|
|
99
|
+
switch (status) {
|
|
100
|
+
case static_cast<jint>(qntx::PasskeyStatus::ok):
|
|
101
|
+
case static_cast<jint>(qntx::PasskeyStatus::cancelled):
|
|
102
|
+
case static_cast<jint>(qntx::PasskeyStatus::unavailable):
|
|
103
|
+
result.status = static_cast<qntx::PasskeyStatus>(status);
|
|
104
|
+
break;
|
|
105
|
+
default:
|
|
106
|
+
result.status = qntx::PasskeyStatus::failed;
|
|
107
|
+
}
|
|
108
|
+
if (responseJson != nullptr && result.status == qntx::PasskeyStatus::ok) {
|
|
109
|
+
const char* utf = env->GetStringUTFChars(responseJson, nullptr);
|
|
110
|
+
if (utf != nullptr) {
|
|
111
|
+
result.responseJson = utf;
|
|
112
|
+
env->ReleaseStringUTFChars(responseJson, utf);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
done(std::move(result));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#endif // __ANDROID__
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// iOS passkey authenticator: not available yet. Native passkeys need the
|
|
2
|
+
// app's associated domain (webcredentials), which the current free signing
|
|
3
|
+
// team cannot provision; W6c implements ASAuthorization with PRF once a paid
|
|
4
|
+
// team exists. Until then every ceremony reports `unavailable`, which the
|
|
5
|
+
// kernel surfaces as factor-unavailable (no passkey wallet on this platform).
|
|
6
|
+
#include "../host/Passkey.hpp"
|
|
7
|
+
|
|
8
|
+
namespace qntx {
|
|
9
|
+
|
|
10
|
+
void passkeyCreate(std::string, PasskeyDone done) {
|
|
11
|
+
done(PasskeyResult{PasskeyStatus::unavailable, {}});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
void passkeyGet(std::string, PasskeyDone done) {
|
|
15
|
+
done(PasskeyResult{PasskeyStatus::unavailable, {}});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
void passkeyCancel() {}
|
|
19
|
+
|
|
20
|
+
} // namespace qntx
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// iOS vault directory: Application Support/qntx-wallet/ created with
|
|
2
|
+
// NSFileProtectionComplete and excluded from backup — the vault image must
|
|
3
|
+
// never ride into iCloud or unencrypted backups.
|
|
4
|
+
#include "../host/VaultDirectory.hpp"
|
|
5
|
+
|
|
6
|
+
#import <Foundation/Foundation.h>
|
|
7
|
+
|
|
8
|
+
#include <stdexcept>
|
|
9
|
+
|
|
10
|
+
namespace qntx {
|
|
11
|
+
namespace {
|
|
12
|
+
|
|
13
|
+
// NSError detail is reduced to its numeric code: failure messages propagate
|
|
14
|
+
// through the kernel's error path and must never carry user-data paths.
|
|
15
|
+
[[noreturn]] void failStep(const char* step, NSError* error) {
|
|
16
|
+
if (error == nil) {
|
|
17
|
+
throw std::runtime_error(step);
|
|
18
|
+
}
|
|
19
|
+
throw std::runtime_error(
|
|
20
|
+
std::string(step) + " (Cocoa error " + std::to_string(error.code) + ")");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
} // namespace
|
|
24
|
+
|
|
25
|
+
std::string platformVaultDirectory() {
|
|
26
|
+
NSFileManager* fileManager = [NSFileManager defaultManager];
|
|
27
|
+
NSError* error = nil;
|
|
28
|
+
NSURL* appSupport = [fileManager URLForDirectory:NSApplicationSupportDirectory
|
|
29
|
+
inDomain:NSUserDomainMask
|
|
30
|
+
appropriateForURL:nil
|
|
31
|
+
create:YES
|
|
32
|
+
error:&error];
|
|
33
|
+
if (appSupport == nil) {
|
|
34
|
+
failStep("resolve Application Support directory", error);
|
|
35
|
+
}
|
|
36
|
+
NSURL* dir = [appSupport URLByAppendingPathComponent:@"qntx-wallet" isDirectory:YES];
|
|
37
|
+
if (dir == nil) {
|
|
38
|
+
throw std::runtime_error("resolve vault directory");
|
|
39
|
+
}
|
|
40
|
+
error = nil;
|
|
41
|
+
if (![fileManager createDirectoryAtURL:dir
|
|
42
|
+
withIntermediateDirectories:YES
|
|
43
|
+
attributes:@{NSFileProtectionKey : NSFileProtectionComplete}
|
|
44
|
+
error:&error]) {
|
|
45
|
+
failStep("create vault directory", error);
|
|
46
|
+
}
|
|
47
|
+
error = nil;
|
|
48
|
+
if (![dir setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:&error]) {
|
|
49
|
+
failStep("exclude vault directory from backup", error);
|
|
50
|
+
}
|
|
51
|
+
const char* path = dir.fileSystemRepresentation;
|
|
52
|
+
if (path == nullptr) {
|
|
53
|
+
throw std::runtime_error("resolve vault directory path");
|
|
54
|
+
}
|
|
55
|
+
return path;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
} // namespace qntx
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Android vault directory: <noBackupFilesDir>/qntx-wallet, pulled from the
|
|
2
|
+
// Kotlin VaultDirectory.resolve() through fbjni — one supported-API path, no
|
|
3
|
+
// C++-side caching of the path.
|
|
4
|
+
#include "../host/VaultDirectory.hpp"
|
|
5
|
+
|
|
6
|
+
#ifdef __ANDROID__
|
|
7
|
+
|
|
8
|
+
#include <fbjni/fbjni.h>
|
|
9
|
+
#include <jni.h>
|
|
10
|
+
#include <stdexcept>
|
|
11
|
+
#include <string>
|
|
12
|
+
|
|
13
|
+
namespace qntx {
|
|
14
|
+
namespace {
|
|
15
|
+
facebook::jni::global_ref<jclass> g_vaultDirectoryClass;
|
|
16
|
+
} // namespace
|
|
17
|
+
|
|
18
|
+
void kernelVaultDirectoryInit(facebook::jni::alias_ref<jclass> clazz) {
|
|
19
|
+
g_vaultDirectoryClass = facebook::jni::make_global(clazz);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
std::string platformVaultDirectory() {
|
|
23
|
+
facebook::jni::ThreadScope threadScope;
|
|
24
|
+
if (!g_vaultDirectoryClass) {
|
|
25
|
+
throw std::runtime_error("vault directory: JNI_OnLoad init did not run");
|
|
26
|
+
}
|
|
27
|
+
auto method = g_vaultDirectoryClass->getStaticMethod<facebook::jni::JString::javaobject()>(
|
|
28
|
+
"resolve");
|
|
29
|
+
auto path = method(g_vaultDirectoryClass.get());
|
|
30
|
+
return path->toStdString();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
} // namespace qntx
|
|
34
|
+
|
|
35
|
+
#endif // __ANDROID__
|