@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,77 @@
|
|
|
1
|
+
#include "Lifecycle.hpp"
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
namespace qntx {
|
|
6
|
+
|
|
7
|
+
KernelLifecycleDispatcher& KernelLifecycleDispatcher::shared() {
|
|
8
|
+
static KernelLifecycleDispatcher dispatcher;
|
|
9
|
+
return dispatcher;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
void KernelLifecycleDispatcher::registerHost(
|
|
13
|
+
const std::weak_ptr<LifecycleLockTarget>& host, int seconds) {
|
|
14
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
15
|
+
entries_.push_back(Entry{host, seconds, std::nullopt});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
void KernelLifecycleDispatcher::unregister(const LifecycleLockTarget* host) {
|
|
19
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
20
|
+
std::erase_if(entries_, [&](const Entry& entry) {
|
|
21
|
+
const auto target = entry.host.lock();
|
|
22
|
+
return !target || target.get() == host;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
void KernelLifecycleDispatcher::onBackground() {
|
|
27
|
+
std::vector<std::shared_ptr<LifecycleLockTarget>> immediate;
|
|
28
|
+
{
|
|
29
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
30
|
+
const auto now = clock_();
|
|
31
|
+
for (auto& entry : entries_) {
|
|
32
|
+
const auto host = entry.host.lock();
|
|
33
|
+
if (!host) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (entry.seconds == 0) {
|
|
37
|
+
immediate.push_back(host);
|
|
38
|
+
} else {
|
|
39
|
+
entry.backgroundAtMs = now;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Lock outside the registry mutex — the target's own lock may re-enter here
|
|
44
|
+
// (unregister on exit).
|
|
45
|
+
for (const auto& host : immediate) {
|
|
46
|
+
host->lockForBackground();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
void KernelLifecycleDispatcher::onForeground() {
|
|
51
|
+
std::vector<std::shared_ptr<LifecycleLockTarget>> expired;
|
|
52
|
+
{
|
|
53
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
54
|
+
const auto now = clock_();
|
|
55
|
+
for (auto& entry : entries_) {
|
|
56
|
+
const auto host = entry.host.lock();
|
|
57
|
+
if (!host) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (entry.backgroundAtMs.has_value() &&
|
|
61
|
+
backgroundExpired(*entry.backgroundAtMs, now, entry.seconds)) {
|
|
62
|
+
expired.push_back(host);
|
|
63
|
+
}
|
|
64
|
+
entry.backgroundAtMs.reset();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const auto& host : expired) {
|
|
68
|
+
host->lockForBackground();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
void KernelLifecycleDispatcher::setClock(std::function<int64_t()> clock) {
|
|
73
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
74
|
+
clock_ = std::move(clock);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
} // namespace qntx
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <cstdint>
|
|
4
|
+
#include <functional>
|
|
5
|
+
#include <memory>
|
|
6
|
+
#include <optional>
|
|
7
|
+
#include <mutex>
|
|
8
|
+
#include <vector>
|
|
9
|
+
|
|
10
|
+
namespace qntx {
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The background-expiry decision: a host whose lock deadline was
|
|
14
|
+
* `backgroundAtMs + seconds*1000` on the sleep-inclusive monotonic clock is
|
|
15
|
+
* expired when nowMs is past it. seconds==0 locks the moment the app
|
|
16
|
+
* backgrounds — the caller never reaches this function for it.
|
|
17
|
+
*/
|
|
18
|
+
inline bool backgroundExpired(int64_t backgroundAtMs, int64_t nowMs, int seconds) {
|
|
19
|
+
return nowMs >= backgroundAtMs + static_cast<int64_t>(seconds) * 1000;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Milliseconds on the clock that keeps running while the device sleeps —
|
|
23
|
+
* Apple CLOCK_MONOTONIC, Android CLOCK_BOOTTIME. */
|
|
24
|
+
int64_t sleepInclusiveMonotonicMs();
|
|
25
|
+
|
|
26
|
+
/** What the platform's lifecycle hooks lock; HybridKernelHost implements it
|
|
27
|
+
* (exit(BACKGROUND)). The interface keeps the dispatcher testable without a
|
|
28
|
+
* Nitro runtime. */
|
|
29
|
+
class LifecycleLockTarget {
|
|
30
|
+
public:
|
|
31
|
+
virtual ~LifecycleLockTarget() = default;
|
|
32
|
+
virtual void lockForBackground() = 0;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* One process-wide registry of live kernel hosts with their
|
|
37
|
+
* backgroundLockSeconds. `onBackground` stamps every registered host (or locks
|
|
38
|
+
* it immediately when its seconds is 0); `onForeground` locks the hosts whose
|
|
39
|
+
* deadline passed while backgrounded. All calls are main-thread safe and take
|
|
40
|
+
* weak_ptr so a destroyed host is simply skipped.
|
|
41
|
+
*/
|
|
42
|
+
class KernelLifecycleDispatcher {
|
|
43
|
+
public:
|
|
44
|
+
static KernelLifecycleDispatcher& shared();
|
|
45
|
+
|
|
46
|
+
void registerHost(const std::weak_ptr<LifecycleLockTarget>& host, int seconds);
|
|
47
|
+
/** Drop the registration — called on every host exit. */
|
|
48
|
+
void unregister(const LifecycleLockTarget* host);
|
|
49
|
+
|
|
50
|
+
void onBackground();
|
|
51
|
+
void onForeground();
|
|
52
|
+
|
|
53
|
+
/** Test seam: the clock defaults to sleepInclusiveMonotonicMs. */
|
|
54
|
+
void setClock(std::function<int64_t()> clock);
|
|
55
|
+
|
|
56
|
+
private:
|
|
57
|
+
struct Entry {
|
|
58
|
+
std::weak_ptr<LifecycleLockTarget> host;
|
|
59
|
+
int seconds;
|
|
60
|
+
std::optional<int64_t> backgroundAtMs;
|
|
61
|
+
};
|
|
62
|
+
std::mutex mutex_;
|
|
63
|
+
std::vector<Entry> entries_;
|
|
64
|
+
std::function<int64_t()> clock_ = sleepInclusiveMonotonicMs;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Install the platform's app-lifecycle observer exactly once — NSNotificationCenter
|
|
68
|
+
* on Apple, ProcessLifecycleOwner on Android. Called from KernelHost.start. */
|
|
69
|
+
void installLifecycleObserver();
|
|
70
|
+
|
|
71
|
+
} // namespace qntx
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <functional>
|
|
4
|
+
#include <string>
|
|
5
|
+
|
|
6
|
+
namespace qntx {
|
|
7
|
+
|
|
8
|
+
// The platform passkey authenticator behind `__qntx.passkey` (W6). The kernel
|
|
9
|
+
// builds the WebAuthn request JSON and parses the response JSON itself; the
|
|
10
|
+
// platform only runs the system ceremony. Android: Credential Manager. iOS:
|
|
11
|
+
// not yet available (W6c needs an associated domain) — every call completes
|
|
12
|
+
// `unavailable`. Each function has exactly one strong definition per platform
|
|
13
|
+
// (PasskeyAndroid.cpp, PasskeyIos.mm); host tests link a fake.
|
|
14
|
+
|
|
15
|
+
// The binding maps each status to the kernel's error code: cancelled →
|
|
16
|
+
// "cancelled", unavailable → "factor-unavailable", failed → "unavailable".
|
|
17
|
+
// The values are part of the Android JNI contract (Passkey.kt).
|
|
18
|
+
enum class PasskeyStatus { ok = 0, cancelled = 1, unavailable = 3, failed = 4 };
|
|
19
|
+
|
|
20
|
+
struct PasskeyResult {
|
|
21
|
+
PasskeyStatus status = PasskeyStatus::failed;
|
|
22
|
+
// The WebAuthn registration or authentication response JSON on success.
|
|
23
|
+
std::string responseJson;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Completion callbacks run exactly once, on any thread.
|
|
27
|
+
using PasskeyDone = std::function<void(PasskeyResult)>;
|
|
28
|
+
|
|
29
|
+
// Registers a discoverable credential from PublicKeyCredentialCreationOptions JSON.
|
|
30
|
+
void passkeyCreate(std::string requestJson, PasskeyDone done);
|
|
31
|
+
|
|
32
|
+
// Asserts a credential from PublicKeyCredentialRequestOptions JSON.
|
|
33
|
+
void passkeyGet(std::string requestJson, PasskeyDone done);
|
|
34
|
+
|
|
35
|
+
// Dismisses the system UI of an in-flight ceremony, if any; that call still
|
|
36
|
+
// completes (normally `cancelled`). Called when a kernel runtime tears down.
|
|
37
|
+
void passkeyCancel();
|
|
38
|
+
|
|
39
|
+
} // namespace qntx
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <string>
|
|
4
|
+
|
|
5
|
+
namespace qntx {
|
|
6
|
+
|
|
7
|
+
// The directory the vault image file lives in — resolved once per
|
|
8
|
+
// KernelRuntime::start and passed explicitly to kernelFileRead/kernelFileWrite:
|
|
9
|
+
// iOS <Application Support>/qntx-wallet
|
|
10
|
+
// Android <noBackupFilesDir>/qntx-wallet
|
|
11
|
+
// Defined only by a platform translation unit (platform/VaultDirectory.mm on iOS,
|
|
12
|
+
// platform/VaultDirectoryAndroid.cpp on Android): a host that links neither fails
|
|
13
|
+
// at link time instead of misbehaving at runtime.
|
|
14
|
+
std::string platformVaultDirectory();
|
|
15
|
+
|
|
16
|
+
} // namespace qntx
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Android keystore: the Kotlin Keystore object owns the AndroidKeyStore key
|
|
2
|
+
// and the BiometricPrompt; this file forwards each call 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/Keystore.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_keystoreClass;
|
|
24
|
+
|
|
25
|
+
std::mutex g_mutex;
|
|
26
|
+
int64_t g_nextToken = 1;
|
|
27
|
+
std::unordered_map<int64_t, KeystoreDone> g_pending;
|
|
28
|
+
|
|
29
|
+
int64_t park(KeystoreDone 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
|
+
KeystoreDone 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> keystoreClass() {
|
|
48
|
+
if (!g_keystoreClass) {
|
|
49
|
+
throw std::runtime_error("keystore: JNI_OnLoad init did not run");
|
|
50
|
+
}
|
|
51
|
+
return g_keystoreClass;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
jni::local_ref<jni::JArrayByte> byteArray(const uint8_t* data, size_t size) {
|
|
55
|
+
auto array = jni::JArrayByte::newArray(size);
|
|
56
|
+
array->setRegion(0, static_cast<jsize>(size), reinterpret_cast<const jbyte*>(data));
|
|
57
|
+
return array;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Runs one Kotlin entry point; a JNI failure completes the call as `failed`
|
|
61
|
+
// instead of leaving it parked forever.
|
|
62
|
+
template <typename Call>
|
|
63
|
+
void dispatch(KeystoreDone done, Call&& call) {
|
|
64
|
+
const int64_t token = park(std::move(done));
|
|
65
|
+
try {
|
|
66
|
+
jni::ThreadScope threadScope;
|
|
67
|
+
call(static_cast<jlong>(token));
|
|
68
|
+
} catch (...) {
|
|
69
|
+
if (auto parked = take(token)) {
|
|
70
|
+
parked(KeystoreResult{KeystoreStatus::failed, {}});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
} // namespace
|
|
76
|
+
|
|
77
|
+
void kernelKeystoreInit(jni::alias_ref<jclass> clazz) {
|
|
78
|
+
g_keystoreClass = jni::make_global(clazz);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
void keystoreSeal(
|
|
82
|
+
const std::string& id,
|
|
83
|
+
SecretBytes secret,
|
|
84
|
+
KeystorePrompt prompt,
|
|
85
|
+
KeystoreDone done) {
|
|
86
|
+
dispatch(std::move(done), [&](jlong token) {
|
|
87
|
+
auto method = keystoreClass()->getStaticMethod<void(
|
|
88
|
+
jlong, jni::alias_ref<jstring>, jni::alias_ref<jni::JArrayByte>, jni::alias_ref<jstring>,
|
|
89
|
+
jni::alias_ref<jstring>)>("seal");
|
|
90
|
+
method(
|
|
91
|
+
keystoreClass(),
|
|
92
|
+
token,
|
|
93
|
+
jni::make_jstring(id),
|
|
94
|
+
byteArray(secret.data(), secret.size()),
|
|
95
|
+
jni::make_jstring(prompt.title),
|
|
96
|
+
jni::make_jstring(prompt.description));
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
void keystoreOpen(
|
|
101
|
+
const std::string& id,
|
|
102
|
+
std::vector<uint8_t> sealed,
|
|
103
|
+
KeystorePrompt prompt,
|
|
104
|
+
KeystoreDone done) {
|
|
105
|
+
dispatch(std::move(done), [&](jlong token) {
|
|
106
|
+
auto method = keystoreClass()->getStaticMethod<void(
|
|
107
|
+
jlong, jni::alias_ref<jstring>, jni::alias_ref<jni::JArrayByte>, jni::alias_ref<jstring>,
|
|
108
|
+
jni::alias_ref<jstring>)>("open");
|
|
109
|
+
method(
|
|
110
|
+
keystoreClass(),
|
|
111
|
+
token,
|
|
112
|
+
jni::make_jstring(id),
|
|
113
|
+
byteArray(sealed.data(), sealed.size()),
|
|
114
|
+
jni::make_jstring(prompt.title),
|
|
115
|
+
jni::make_jstring(prompt.description));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
void keystoreAuthenticate(KeystorePrompt prompt, KeystoreDone done) {
|
|
120
|
+
dispatch(std::move(done), [&](jlong token) {
|
|
121
|
+
auto method = keystoreClass()->getStaticMethod<void(
|
|
122
|
+
jlong, jni::alias_ref<jstring>, jni::alias_ref<jstring>)>("authenticate");
|
|
123
|
+
method(
|
|
124
|
+
keystoreClass(),
|
|
125
|
+
token,
|
|
126
|
+
jni::make_jstring(prompt.title),
|
|
127
|
+
jni::make_jstring(prompt.description));
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
void keystoreClear(KeystoreDone done) {
|
|
132
|
+
dispatch(std::move(done), [&](jlong token) {
|
|
133
|
+
auto method = keystoreClass()->getStaticMethod<void(jlong)>("clear");
|
|
134
|
+
method(keystoreClass(), token);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
void keystoreCancel() {
|
|
139
|
+
jni::ThreadScope threadScope;
|
|
140
|
+
auto method = keystoreClass()->getStaticMethod<void()>("cancel");
|
|
141
|
+
method(keystoreClass());
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
std::string keystoreLocale() {
|
|
145
|
+
jni::ThreadScope threadScope;
|
|
146
|
+
auto method = keystoreClass()->getStaticMethod<jni::JString::javaobject()>("locale");
|
|
147
|
+
return method(keystoreClass())->toStdString();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
} // namespace qntx
|
|
151
|
+
|
|
152
|
+
// Kotlin's Keystore.nativeComplete: resolves the parked call. The byte array
|
|
153
|
+
// is copied into wiping storage; Kotlin wipes its own copy afterwards.
|
|
154
|
+
extern "C" JNIEXPORT void JNICALL
|
|
155
|
+
Java_com_margelo_nitro_qntxwallet_Keystore_nativeComplete(
|
|
156
|
+
JNIEnv* env, jclass, jlong token, jint status, jbyteArray bytes) {
|
|
157
|
+
auto done = qntx::take(static_cast<int64_t>(token));
|
|
158
|
+
if (!done) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
qntx::KeystoreResult result;
|
|
162
|
+
result.status = static_cast<qntx::KeystoreStatus>(status);
|
|
163
|
+
if (status < 0 || status > static_cast<jint>(qntx::KeystoreStatus::failed)) {
|
|
164
|
+
result.status = qntx::KeystoreStatus::failed;
|
|
165
|
+
}
|
|
166
|
+
if (bytes != nullptr && result.status == qntx::KeystoreStatus::ok) {
|
|
167
|
+
const jsize size = env->GetArrayLength(bytes);
|
|
168
|
+
qntx::SecretBytes copy(static_cast<size_t>(size));
|
|
169
|
+
env->GetByteArrayRegion(bytes, 0, size, reinterpret_cast<jbyte*>(copy.data()));
|
|
170
|
+
result.bytes = std::move(copy);
|
|
171
|
+
}
|
|
172
|
+
done(std::move(result));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#endif // __ANDROID__
|
|
@@ -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
|