@qntx/wallet-native 0.5.2 → 0.7.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/WalletNative.podspec +1 -1
- package/android/build.gradle +3 -24
- package/cpp/host/Passkey.hpp +5 -4
- package/cpp/platform/PasskeyIos.mm +477 -10
- package/dist/index.d.mts +0 -6
- package/dist/index.mjs +3 -6
- package/dist/index.mjs.map +1 -1
- package/kernel/generated/kernel-entry.js +1862 -2351
- package/kernel/generated/kernel.manifest.json +3 -3
- package/package.json +3 -3
package/WalletNative.podspec
CHANGED
|
@@ -59,7 +59,7 @@ Pod::Spec.new do |s|
|
|
|
59
59
|
|
|
60
60
|
# The kernel's backends.spark binds the qs_* C ABI; the prebuilt
|
|
61
61
|
# QntxSpark.xcframework ships inside the SparkNative pod.
|
|
62
|
-
s.frameworks = "LocalAuthentication", "Security"
|
|
62
|
+
s.frameworks = "LocalAuthentication", "Security", "AuthenticationServices", "UIKit"
|
|
63
63
|
|
|
64
64
|
s.dependency 'NostrNative'
|
|
65
65
|
s.dependency 'SparkNative'
|
package/android/build.gradle
CHANGED
|
@@ -14,20 +14,10 @@ def reactNativeArchitectures() {
|
|
|
14
14
|
return value ? value.split(",") : ["x86", "x86_64", "armeabi-v7a", "arm64-v8a"]
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
def isNewArchitectureEnabled() {
|
|
18
|
-
return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
|
|
19
|
-
}
|
|
20
|
-
|
|
21
17
|
apply plugin: "com.android.library"
|
|
22
18
|
apply plugin: 'org.jetbrains.kotlin.android'
|
|
23
19
|
apply from: '../nitrogen/generated/android/WalletNative+autolinking.gradle'
|
|
24
20
|
|
|
25
|
-
if (isNewArchitectureEnabled()) {
|
|
26
|
-
apply plugin: "com.facebook.react"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
21
|
def getExtOrDefault(name) {
|
|
32
22
|
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["WalletNative_" + name]
|
|
33
23
|
}
|
|
@@ -133,23 +123,12 @@ android {
|
|
|
133
123
|
targetCompatibility JavaVersion.VERSION_1_8
|
|
134
124
|
}
|
|
135
125
|
|
|
136
|
-
sourceSets {
|
|
137
|
-
main {
|
|
138
|
-
if (isNewArchitectureEnabled()) {
|
|
139
|
-
java.srcDirs += [
|
|
140
|
-
"${project.buildDir}/generated/source/codegen/java"
|
|
141
|
-
]
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
126
|
}
|
|
146
127
|
|
|
147
128
|
dependencies {
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
//noinspection GradleDynamicVersion
|
|
152
|
-
implementation "com.facebook.react:react-native:+"
|
|
129
|
+
// The app's React Native Gradle plugin pins the version; the prefab packages (fbjni, ReactAndroid)
|
|
130
|
+
// are what the generated CMake autolinking file finds with find_package.
|
|
131
|
+
implementation "com.facebook.react:react-android"
|
|
153
132
|
|
|
154
133
|
// Hermes engine prefab — the kernel runtime links hermesvm/libhermes
|
|
155
134
|
implementation "com.facebook.react:hermes-android"
|
package/cpp/host/Passkey.hpp
CHANGED
|
@@ -7,10 +7,11 @@ namespace qntx {
|
|
|
7
7
|
|
|
8
8
|
// The platform passkey authenticator behind `__qntx.passkey` (W6). The kernel
|
|
9
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
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
10
|
+
// platform only runs the system ceremony. Android: Credential Manager. iOS
|
|
11
|
+
// (W6c): ASAuthorization platform passkeys with the PRF extension on iOS 18+,
|
|
12
|
+
// unavailable below or without the associated domain. Each function has
|
|
13
|
+
// exactly one strong definition per platform (PasskeyAndroid.cpp,
|
|
14
|
+
// PasskeyIos.mm); host tests link a fake.
|
|
14
15
|
|
|
15
16
|
// The binding maps each status to the kernel's error code: cancelled →
|
|
16
17
|
// "cancelled", unavailable → "factor-unavailable", failed → "unavailable".
|
|
@@ -1,20 +1,487 @@
|
|
|
1
|
-
// iOS passkey authenticator:
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// iOS passkey authenticator (W6c): ASAuthorization platform passkeys with the
|
|
2
|
+
// PRF extension (iOS 18+). The kernel builds the WebAuthn request JSON and
|
|
3
|
+
// parses the response JSON itself; this file only reads the fields the system
|
|
4
|
+
// ceremony needs, runs one ASAuthorizationController at a time and repacks the
|
|
5
|
+
// credential into the browser `toJSON()` shape the kernel parses. A ceremony
|
|
6
|
+
// requested while the scene is still activating — e.g. right after a system
|
|
7
|
+
// Face ID prompt resigns it — waits for UISceneDidActivateNotification instead
|
|
8
|
+
// of failing before any sheet appears. PRF outputs are encoded straight into
|
|
9
|
+
// the response JSON — no copies are kept and they are never logged.
|
|
6
10
|
#include "../host/Passkey.hpp"
|
|
7
11
|
|
|
12
|
+
#import <AuthenticationServices/AuthenticationServices.h>
|
|
13
|
+
#import <Foundation/Foundation.h>
|
|
14
|
+
#import <UIKit/UIKit.h>
|
|
15
|
+
#import <os/log.h>
|
|
16
|
+
|
|
17
|
+
#include <mutex>
|
|
18
|
+
#include <string>
|
|
19
|
+
#include <utility>
|
|
20
|
+
|
|
21
|
+
@class QntxPasskeySession;
|
|
22
|
+
|
|
23
|
+
namespace {
|
|
24
|
+
|
|
25
|
+
os_log_t passkeyLog() {
|
|
26
|
+
static os_log_t log = os_log_create("org.qntx.wallet", "passkey");
|
|
27
|
+
return log;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// The session — and the controller it owns once the ceremony actually runs —
|
|
31
|
+
// stays alive only while a call is in flight (controller.delegate is weak).
|
|
32
|
+
// It is touched on the main thread only; the mutex pairs the one-in-flight
|
|
33
|
+
// check with the clear on completion. A parked session (scene not yet active)
|
|
34
|
+
// has no controller yet, so the busy check looks at the session.
|
|
35
|
+
std::mutex g_mutex;
|
|
36
|
+
QntxPasskeySession* g_session = nil;
|
|
37
|
+
|
|
38
|
+
// Strict base64url decode (padding restored); nil on any invalid character.
|
|
39
|
+
NSData* _Nullable base64urlDecode(NSString* _Nullable value) {
|
|
40
|
+
if (value == nil) {
|
|
41
|
+
return nil;
|
|
42
|
+
}
|
|
43
|
+
NSMutableString* standard = [NSMutableString stringWithCapacity:value.length + 3];
|
|
44
|
+
for (NSUInteger i = 0; i < value.length; i += 1) {
|
|
45
|
+
const unichar c = [value characterAtIndex:i];
|
|
46
|
+
if (c == '-') {
|
|
47
|
+
[standard appendString:@"+"];
|
|
48
|
+
} else if (c == '_') {
|
|
49
|
+
[standard appendString:@"/"];
|
|
50
|
+
} else if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) {
|
|
51
|
+
[standard appendFormat:@"%C", c];
|
|
52
|
+
} else {
|
|
53
|
+
return nil;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (standard.length % 4 == 1) {
|
|
57
|
+
return nil;
|
|
58
|
+
}
|
|
59
|
+
while (standard.length % 4 != 0) {
|
|
60
|
+
[standard appendString:@"="];
|
|
61
|
+
}
|
|
62
|
+
return [[NSData alloc] initWithBase64EncodedString:standard options:0];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
NSString* base64urlEncode(NSData* data) {
|
|
66
|
+
NSString* standard = [data base64EncodedStringWithOptions:0];
|
|
67
|
+
standard = [standard stringByReplacingOccurrencesOfString:@"+" withString:@"-"];
|
|
68
|
+
standard = [standard stringByReplacingOccurrencesOfString:@"/" withString:@"_"];
|
|
69
|
+
while ([standard hasSuffix:@"="]) {
|
|
70
|
+
standard = [standard substringToIndex:standard.length - 1];
|
|
71
|
+
}
|
|
72
|
+
return standard;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
NSDictionary<NSString*, id>* _Nullable dictOf(_Nullable id value) {
|
|
76
|
+
return [value isKindOfClass:NSDictionary.class] ? (NSDictionary<NSString*, id>*)value : nil;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
NSString* _Nullable stringOf(_Nullable id value) {
|
|
80
|
+
return [value isKindOfClass:NSString.class] ? (NSString*)value : nil;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
NSArray* _Nullable arrayOf(_Nullable id value) {
|
|
84
|
+
return [value isKindOfClass:NSArray.class] ? (NSArray*)value : nil;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
NSDictionary<NSString*, id>* _Nullable jsonOf(const std::string& json) {
|
|
88
|
+
NSData* data = [NSData dataWithBytes:json.data() length:json.size()];
|
|
89
|
+
return dictOf([NSJSONSerialization JSONObjectWithData:data options:0 error:nil]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The extensions.prf.eval block the kernel always sends; nil when missing or
|
|
93
|
+
// malformed (eval.second is optional).
|
|
94
|
+
ASAuthorizationPublicKeyCredentialPRFAssertionInputValues* _Nullable prfEval(
|
|
95
|
+
NSDictionary<NSString*, id>* request) API_AVAILABLE(ios(18.0)) {
|
|
96
|
+
NSDictionary* eval = dictOf(dictOf(dictOf(request[@"extensions"])[@"prf"])[@"eval"]);
|
|
97
|
+
NSData* first = base64urlDecode(stringOf(eval[@"first"]));
|
|
98
|
+
if (first == nil) {
|
|
99
|
+
return nil;
|
|
100
|
+
}
|
|
101
|
+
NSData* second = nil;
|
|
102
|
+
if (eval[@"second"] != nil) {
|
|
103
|
+
second = base64urlDecode(stringOf(eval[@"second"]));
|
|
104
|
+
if (second == nil) {
|
|
105
|
+
return nil;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return [[ASAuthorizationPublicKeyCredentialPRFAssertionInputValues alloc]
|
|
109
|
+
initWithSaltInput1:first
|
|
110
|
+
saltInput2:second];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
ASPresentationAnchor _Nullable activeKeyWindow() {
|
|
114
|
+
for (UIScene* scene in UIApplication.sharedApplication.connectedScenes) {
|
|
115
|
+
if ([scene isKindOfClass:UIWindowScene.class] &&
|
|
116
|
+
scene.activationState == UISceneActivationStateForegroundActive) {
|
|
117
|
+
UIWindow* window = ((UIWindowScene*)scene).keyWindow;
|
|
118
|
+
if (window != nil) {
|
|
119
|
+
return window;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return nil;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// A window scene on its way to the foreground — e.g. right after a system
|
|
127
|
+
// Face ID prompt resigns the app's scene. Once it activates, its key window
|
|
128
|
+
// can anchor the passkey sheet.
|
|
129
|
+
BOOL hasInactiveWindowScene() {
|
|
130
|
+
for (UIScene* scene in UIApplication.sharedApplication.connectedScenes) {
|
|
131
|
+
if ([scene isKindOfClass:UIWindowScene.class] &&
|
|
132
|
+
scene.activationState == UISceneActivationStateForegroundInactive &&
|
|
133
|
+
((UIWindowScene*)scene).keyWindow != nil) {
|
|
134
|
+
return YES;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return NO;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
std::string jsonString(NSDictionary<NSString*, id>* json) {
|
|
141
|
+
NSData* data = [NSJSONSerialization dataWithJSONObject:json options:0 error:nil];
|
|
142
|
+
if (data == nil) {
|
|
143
|
+
return {};
|
|
144
|
+
}
|
|
145
|
+
return std::string(static_cast<const char*>(data.bytes), data.length);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
} // namespace
|
|
149
|
+
|
|
150
|
+
API_AVAILABLE(ios(18.0))
|
|
151
|
+
@interface QntxPasskeySession : NSObject <ASAuthorizationControllerDelegate,
|
|
152
|
+
ASAuthorizationControllerPresentationContextProviding>
|
|
153
|
+
- (instancetype)initWithDone:(qntx::PasskeyDone)done
|
|
154
|
+
request:(ASAuthorizationRequest*)request;
|
|
155
|
+
// Runs the ceremony now, or parks it until the window scene activates.
|
|
156
|
+
- (void)start;
|
|
157
|
+
// Cancels a parked wait or the running ceremony; completes through finish:.
|
|
158
|
+
- (void)cancelCeremony;
|
|
159
|
+
- (void)finish:(qntx::PasskeyResult)result;
|
|
160
|
+
@end
|
|
161
|
+
|
|
162
|
+
@implementation QntxPasskeySession {
|
|
163
|
+
qntx::PasskeyDone _done;
|
|
164
|
+
ASAuthorizationRequest* _request;
|
|
165
|
+
ASPresentationAnchor _anchor;
|
|
166
|
+
ASAuthorizationController* _controller;
|
|
167
|
+
NSObject* _observer;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
- (instancetype)initWithDone:(qntx::PasskeyDone)done
|
|
171
|
+
request:(ASAuthorizationRequest*)request {
|
|
172
|
+
self = [super init];
|
|
173
|
+
if (self != nil) {
|
|
174
|
+
_done = std::move(done);
|
|
175
|
+
_request = request;
|
|
176
|
+
}
|
|
177
|
+
return self;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// A ceremony started while the scene is still activating fails before any
|
|
181
|
+
// sheet appears — like Keystore.kt's whenResumed, park on an inactive window
|
|
182
|
+
// scene and run it at UISceneDidActivateNotification. A scene that is fully
|
|
183
|
+
// absent still reports `unavailable`.
|
|
184
|
+
- (void)start {
|
|
185
|
+
ASPresentationAnchor anchor = activeKeyWindow();
|
|
186
|
+
if (anchor != nil) {
|
|
187
|
+
[self performWithAnchor:anchor];
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (hasInactiveWindowScene()) {
|
|
191
|
+
_observer = [NSNotificationCenter.defaultCenter
|
|
192
|
+
addObserverForName:UISceneDidActivateNotification
|
|
193
|
+
object:nil
|
|
194
|
+
queue:NSOperationQueue.mainQueue
|
|
195
|
+
usingBlock:^(NSNotification*) {
|
|
196
|
+
[self sceneActivated];
|
|
197
|
+
}];
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
[self finish:qntx::PasskeyResult{qntx::PasskeyStatus::unavailable, {}}];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
- (void)sceneActivated {
|
|
204
|
+
[NSNotificationCenter.defaultCenter removeObserver:_observer];
|
|
205
|
+
_observer = nil;
|
|
206
|
+
ASPresentationAnchor anchor = activeKeyWindow();
|
|
207
|
+
if (anchor == nil) {
|
|
208
|
+
[self finish:qntx::PasskeyResult{qntx::PasskeyStatus::unavailable, {}}];
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
[self performWithAnchor:anchor];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
- (void)performWithAnchor:(ASPresentationAnchor)anchor {
|
|
215
|
+
_anchor = anchor;
|
|
216
|
+
_controller =
|
|
217
|
+
[[ASAuthorizationController alloc] initWithAuthorizationRequests:@[ _request ]];
|
|
218
|
+
_controller.delegate = self;
|
|
219
|
+
_controller.presentationContextProvider = self;
|
|
220
|
+
[_controller performRequests];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
- (void)cancelCeremony {
|
|
224
|
+
if (_observer != nil) {
|
|
225
|
+
[NSNotificationCenter.defaultCenter removeObserver:_observer];
|
|
226
|
+
_observer = nil;
|
|
227
|
+
[self finish:qntx::PasskeyResult{qntx::PasskeyStatus::cancelled, {}}];
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
[_controller cancel];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Runs exactly once per session; clears the in-flight session before
|
|
234
|
+
// invoking the kernel completion.
|
|
235
|
+
- (void)finish:(qntx::PasskeyResult)result {
|
|
236
|
+
qntx::PasskeyDone done;
|
|
237
|
+
{
|
|
238
|
+
std::lock_guard<std::mutex> lock(g_mutex);
|
|
239
|
+
if (g_session != self) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
g_session = nil;
|
|
243
|
+
done = std::move(_done);
|
|
244
|
+
_done = nullptr;
|
|
245
|
+
}
|
|
246
|
+
if (_observer != nil) {
|
|
247
|
+
[NSNotificationCenter.defaultCenter removeObserver:_observer];
|
|
248
|
+
_observer = nil;
|
|
249
|
+
}
|
|
250
|
+
done(std::move(result));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
- (ASPresentationAnchor)presentationAnchorForAuthorizationController:
|
|
254
|
+
(ASAuthorizationController*)controller {
|
|
255
|
+
return _anchor;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
- (void)authorizationController:(ASAuthorizationController*)controller
|
|
259
|
+
didCompleteWithAuthorization:(ASAuthorization*)authorization {
|
|
260
|
+
NSMutableDictionary<NSString*, id>* extensions = [NSMutableDictionary new];
|
|
261
|
+
NSMutableDictionary<NSString*, id>* response = [NSMutableDictionary new];
|
|
262
|
+
std::string credentialId;
|
|
263
|
+
if ([authorization.credential
|
|
264
|
+
isKindOfClass:ASAuthorizationPlatformPublicKeyCredentialRegistration.class]) {
|
|
265
|
+
auto* credential = (ASAuthorizationPlatformPublicKeyCredentialRegistration*)
|
|
266
|
+
authorization.credential;
|
|
267
|
+
NSString* id64 = base64urlEncode(credential.credentialID);
|
|
268
|
+
credentialId = std::string(id64.UTF8String);
|
|
269
|
+
response[@"clientDataJSON"] = base64urlEncode(credential.rawClientDataJSON);
|
|
270
|
+
if (credential.rawAttestationObject != nil) {
|
|
271
|
+
response[@"attestationObject"] = base64urlEncode(credential.rawAttestationObject);
|
|
272
|
+
}
|
|
273
|
+
if (credential.prf != nil) {
|
|
274
|
+
NSMutableDictionary<NSString*, id>* prf =
|
|
275
|
+
[@{ @"enabled" : @(credential.prf.isSupported) } mutableCopy];
|
|
276
|
+
if (credential.prf.first != nil) {
|
|
277
|
+
NSMutableDictionary<NSString*, id>* results =
|
|
278
|
+
[@{ @"first" : base64urlEncode(credential.prf.first) } mutableCopy];
|
|
279
|
+
if (credential.prf.second != nil) {
|
|
280
|
+
results[@"second"] = base64urlEncode(credential.prf.second);
|
|
281
|
+
}
|
|
282
|
+
prf[@"results"] = results;
|
|
283
|
+
}
|
|
284
|
+
extensions[@"prf"] = prf;
|
|
285
|
+
}
|
|
286
|
+
} else if ([authorization.credential
|
|
287
|
+
isKindOfClass:ASAuthorizationPlatformPublicKeyCredentialAssertion.class]) {
|
|
288
|
+
auto* credential = (ASAuthorizationPlatformPublicKeyCredentialAssertion*)
|
|
289
|
+
authorization.credential;
|
|
290
|
+
NSString* id64 = base64urlEncode(credential.credentialID);
|
|
291
|
+
credentialId = std::string(id64.UTF8String);
|
|
292
|
+
response[@"clientDataJSON"] = base64urlEncode(credential.rawClientDataJSON);
|
|
293
|
+
response[@"authenticatorData"] = base64urlEncode(credential.rawAuthenticatorData);
|
|
294
|
+
response[@"signature"] = base64urlEncode(credential.signature);
|
|
295
|
+
response[@"userHandle"] = base64urlEncode(credential.userID);
|
|
296
|
+
if (credential.prf != nil) {
|
|
297
|
+
NSMutableDictionary<NSString*, id>* results =
|
|
298
|
+
[@{ @"first" : base64urlEncode(credential.prf.first) } mutableCopy];
|
|
299
|
+
if (credential.prf.second != nil) {
|
|
300
|
+
results[@"second"] = base64urlEncode(credential.prf.second);
|
|
301
|
+
}
|
|
302
|
+
extensions[@"prf"] = @{ @"results" : results };
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
[self finish:qntx::PasskeyResult{qntx::PasskeyStatus::failed, {}}];
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
std::string json = jsonString(@{
|
|
309
|
+
@"id" : @(credentialId.c_str()),
|
|
310
|
+
@"rawId" : @(credentialId.c_str()),
|
|
311
|
+
@"type" : @"public-key",
|
|
312
|
+
@"authenticatorAttachment" : @"platform",
|
|
313
|
+
@"response" : response,
|
|
314
|
+
@"clientExtensionResults" : extensions,
|
|
315
|
+
});
|
|
316
|
+
if (json.empty()) {
|
|
317
|
+
[self finish:qntx::PasskeyResult{qntx::PasskeyStatus::failed, {}}];
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
[self finish:qntx::PasskeyResult{qntx::PasskeyStatus::ok, std::move(json)}];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
- (void)authorizationController:(ASAuthorizationController*)controller
|
|
324
|
+
didCompleteWithError:(NSError*)error {
|
|
325
|
+
qntx::PasskeyStatus status = qntx::PasskeyStatus::failed;
|
|
326
|
+
if ([error.domain isEqualToString:ASAuthorizationErrorDomain]) {
|
|
327
|
+
switch (error.code) {
|
|
328
|
+
case ASAuthorizationErrorCanceled:
|
|
329
|
+
status = qntx::PasskeyStatus::cancelled;
|
|
330
|
+
break;
|
|
331
|
+
case ASAuthorizationErrorNotHandled:
|
|
332
|
+
case ASAuthorizationErrorFailed:
|
|
333
|
+
case ASAuthorizationErrorNotInteractive:
|
|
334
|
+
status = qntx::PasskeyStatus::unavailable;
|
|
335
|
+
break;
|
|
336
|
+
default:
|
|
337
|
+
// 1010 = ASAuthorizationErrorDeviceNotConfiguredForPasskeyCreation,
|
|
338
|
+
// compared raw so the iOS 19 symbol needs no availability guard.
|
|
339
|
+
status = error.code == 1010 ? qntx::PasskeyStatus::unavailable
|
|
340
|
+
: qntx::PasskeyStatus::failed;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// Domain and code only — userInfo/localizedDescription can name the account.
|
|
344
|
+
os_log_error(
|
|
345
|
+
passkeyLog(),
|
|
346
|
+
"passkey ceremony failed: %{public}@/%ld",
|
|
347
|
+
error.domain,
|
|
348
|
+
static_cast<long>(error.code));
|
|
349
|
+
[self finish:qntx::PasskeyResult{status, {}}];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
@end
|
|
353
|
+
|
|
354
|
+
namespace {
|
|
355
|
+
|
|
356
|
+
ASAuthorizationPlatformPublicKeyCredentialRegistrationRequest* _Nullable registrationRequest(
|
|
357
|
+
NSDictionary<NSString*, id>* request) API_AVAILABLE(ios(18.0)) {
|
|
358
|
+
NSDictionary* rp = dictOf(request[@"rp"]);
|
|
359
|
+
NSDictionary* user = dictOf(request[@"user"]);
|
|
360
|
+
NSString* rpId = stringOf(rp[@"id"]);
|
|
361
|
+
NSString* name = stringOf(user[@"name"]);
|
|
362
|
+
NSData* challenge = base64urlDecode(stringOf(request[@"challenge"]));
|
|
363
|
+
NSData* userId = base64urlDecode(stringOf(user[@"id"]));
|
|
364
|
+
ASAuthorizationPublicKeyCredentialPRFAssertionInputValues* eval = prfEval(request);
|
|
365
|
+
if (rpId == nil || name == nil || challenge == nil || userId == nil || eval == nil) {
|
|
366
|
+
return nil;
|
|
367
|
+
}
|
|
368
|
+
auto* provider = [[ASAuthorizationPlatformPublicKeyCredentialProvider alloc]
|
|
369
|
+
initWithRelyingPartyIdentifier:rpId];
|
|
370
|
+
auto* platform = [provider createCredentialRegistrationRequestWithChallenge:challenge
|
|
371
|
+
name:name
|
|
372
|
+
userID:userId];
|
|
373
|
+
platform.userVerificationPreference =
|
|
374
|
+
ASAuthorizationPublicKeyCredentialUserVerificationPreferenceRequired;
|
|
375
|
+
platform.attestationPreference = ASAuthorizationPublicKeyCredentialAttestationKindNone;
|
|
376
|
+
platform.prf = [[ASAuthorizationPublicKeyCredentialPRFRegistrationInput alloc]
|
|
377
|
+
initWithInputValues:eval];
|
|
378
|
+
return platform;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
ASAuthorizationPlatformPublicKeyCredentialAssertionRequest* _Nullable assertionRequest(
|
|
382
|
+
NSDictionary<NSString*, id>* request) API_AVAILABLE(ios(18.0)) {
|
|
383
|
+
NSString* rpId = stringOf(request[@"rpId"]);
|
|
384
|
+
NSData* challenge = base64urlDecode(stringOf(request[@"challenge"]));
|
|
385
|
+
ASAuthorizationPublicKeyCredentialPRFAssertionInputValues* eval = prfEval(request);
|
|
386
|
+
if (rpId == nil || challenge == nil || eval == nil) {
|
|
387
|
+
return nil;
|
|
388
|
+
}
|
|
389
|
+
auto* provider = [[ASAuthorizationPlatformPublicKeyCredentialProvider alloc]
|
|
390
|
+
initWithRelyingPartyIdentifier:rpId];
|
|
391
|
+
auto* platform = [provider createCredentialAssertionRequestWithChallenge:challenge];
|
|
392
|
+
if (request[@"allowCredentials"] != nil) {
|
|
393
|
+
NSArray* allow = arrayOf(request[@"allowCredentials"]);
|
|
394
|
+
if (allow == nil) {
|
|
395
|
+
return nil;
|
|
396
|
+
}
|
|
397
|
+
NSMutableArray<ASAuthorizationPlatformPublicKeyCredentialDescriptor*>* descriptors =
|
|
398
|
+
[NSMutableArray arrayWithCapacity:allow.count];
|
|
399
|
+
for (id item in allow) {
|
|
400
|
+
NSData* credentialId = base64urlDecode(stringOf(dictOf(item)[@"id"]));
|
|
401
|
+
if (credentialId == nil) {
|
|
402
|
+
return nil;
|
|
403
|
+
}
|
|
404
|
+
[descriptors addObject:[[ASAuthorizationPlatformPublicKeyCredentialDescriptor alloc]
|
|
405
|
+
initWithCredentialID:credentialId]];
|
|
406
|
+
}
|
|
407
|
+
platform.allowedCredentials = descriptors;
|
|
408
|
+
}
|
|
409
|
+
platform.userVerificationPreference =
|
|
410
|
+
ASAuthorizationPublicKeyCredentialUserVerificationPreferenceRequired;
|
|
411
|
+
platform.prf = [[ASAuthorizationPublicKeyCredentialPRFAssertionInput alloc]
|
|
412
|
+
initWithInputValues:eval
|
|
413
|
+
perCredentialInputValues:nil];
|
|
414
|
+
return platform;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Claims the single in-flight slot and runs the ceremony (main thread). A nil
|
|
418
|
+
// request means the JSON was malformed, a busy slot a second concurrent call.
|
|
419
|
+
// AuthenticationServices may deliver the delegate callback synchronously, so
|
|
420
|
+
// the mutex never spans a call into it — or into `done`.
|
|
421
|
+
void run(ASAuthorizationRequest* _Nullable request, qntx::PasskeyDone done)
|
|
422
|
+
API_AVAILABLE(ios(18.0)) {
|
|
423
|
+
if (request == nil) {
|
|
424
|
+
done(qntx::PasskeyResult{qntx::PasskeyStatus::failed, {}});
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
QntxPasskeySession* session;
|
|
428
|
+
{
|
|
429
|
+
std::lock_guard<std::mutex> lock(g_mutex);
|
|
430
|
+
if (g_session == nil) {
|
|
431
|
+
session = g_session = [[QntxPasskeySession alloc] initWithDone:std::move(done)
|
|
432
|
+
request:request];
|
|
433
|
+
} else {
|
|
434
|
+
session = nil;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (session == nil) {
|
|
438
|
+
done(qntx::PasskeyResult{qntx::PasskeyStatus::failed, {}});
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
[session start];
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
} // namespace
|
|
445
|
+
|
|
8
446
|
namespace qntx {
|
|
9
447
|
|
|
10
|
-
void passkeyCreate(std::string, PasskeyDone done) {
|
|
11
|
-
|
|
448
|
+
void passkeyCreate(std::string requestJson, PasskeyDone done) {
|
|
449
|
+
auto json = std::make_shared<std::string>(std::move(requestJson));
|
|
450
|
+
auto finish = std::make_shared<PasskeyDone>(std::move(done));
|
|
451
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
452
|
+
if (@available(iOS 18.0, *)) {
|
|
453
|
+
run(registrationRequest(jsonOf(*json)), std::move(*finish));
|
|
454
|
+
} else {
|
|
455
|
+
(*finish)(PasskeyResult{PasskeyStatus::unavailable, {}});
|
|
456
|
+
}
|
|
457
|
+
});
|
|
12
458
|
}
|
|
13
459
|
|
|
14
|
-
void passkeyGet(std::string, PasskeyDone done) {
|
|
15
|
-
|
|
460
|
+
void passkeyGet(std::string requestJson, PasskeyDone done) {
|
|
461
|
+
auto json = std::make_shared<std::string>(std::move(requestJson));
|
|
462
|
+
auto finish = std::make_shared<PasskeyDone>(std::move(done));
|
|
463
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
464
|
+
if (@available(iOS 18.0, *)) {
|
|
465
|
+
run(assertionRequest(jsonOf(*json)), std::move(*finish));
|
|
466
|
+
} else {
|
|
467
|
+
(*finish)(PasskeyResult{PasskeyStatus::unavailable, {}});
|
|
468
|
+
}
|
|
469
|
+
});
|
|
16
470
|
}
|
|
17
471
|
|
|
18
|
-
void passkeyCancel() {
|
|
472
|
+
void passkeyCancel() {
|
|
473
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
474
|
+
if (@available(iOS 18.0, *)) {
|
|
475
|
+
QntxPasskeySession* session;
|
|
476
|
+
{
|
|
477
|
+
std::lock_guard<std::mutex> lock(g_mutex);
|
|
478
|
+
session = g_session;
|
|
479
|
+
}
|
|
480
|
+
// A parked wait completes `cancelled`; a running ceremony is cancelled —
|
|
481
|
+
// both answer through finish:, so this runs off the lock.
|
|
482
|
+
[session cancelCeremony];
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
}
|
|
19
486
|
|
|
20
487
|
} // namespace qntx
|
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { AutoLockPolicy, PasskeyConfig, Wallet } from "@qntx/wallet";
|
|
2
2
|
import { MainPort } from "@qntx/wallet/host";
|
|
3
3
|
import { ProtocolValue } from "@qntx/wallet/protocol";
|
|
4
|
-
//#region src/host.d.ts
|
|
5
4
|
type KernelExitReason = "locked" | "crashed" | "integrity" | "background";
|
|
6
5
|
/**
|
|
7
6
|
* Structural mirror of the Nitro `KernelHost` spec (W2b registers the real hybrid object; tests
|
|
@@ -15,8 +14,6 @@ type KernelHostLike = {
|
|
|
15
14
|
readonly lock: () => void;
|
|
16
15
|
};
|
|
17
16
|
export declare function spawnFromHost(host: KernelHostLike, backgroundLockSeconds: number): MainPort;
|
|
18
|
-
//#endregion
|
|
19
|
-
//#region src/cbor.d.ts
|
|
20
17
|
export declare const FRAME_MAX_BYTES: number;
|
|
21
18
|
type CborErrorCode = "oversize" | "truncated" | "malformed" | "float" | "unsafe-int" | "bigint" | "undefined" | "tag" | "non-string-key" | "duplicate-key" | "indefinite" | "bad-type";
|
|
22
19
|
export declare class CborError extends Error {
|
|
@@ -27,8 +24,6 @@ export declare class CborError extends Error {
|
|
|
27
24
|
export declare function encodeFrame(value: ProtocolValue): ArrayBuffer;
|
|
28
25
|
/** Decode a CBOR frame back to a `ProtocolValue`, enforcing the 1 MiB cap and strictness. */
|
|
29
26
|
export declare function decodeFrame(frame: ArrayBuffer | Uint8Array): ProtocolValue;
|
|
30
|
-
//#endregion
|
|
31
|
-
//#region src/index.d.ts
|
|
32
27
|
export type NativeWalletOptions = {
|
|
33
28
|
readonly passkey: PasskeyConfig;
|
|
34
29
|
readonly autoLock: AutoLockPolicy;
|
|
@@ -39,6 +34,5 @@ export type NativeWalletOptions = {
|
|
|
39
34
|
readonly host?: KernelHostLike;
|
|
40
35
|
};
|
|
41
36
|
export declare function createNativeWallet(options: NativeWalletOptions): Promise<Wallet>;
|
|
42
|
-
//#endregion
|
|
43
37
|
export type { CborErrorCode, KernelExitReason, KernelHostLike };
|
|
44
38
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { WalletError } from "@qntx/wallet";
|
|
2
2
|
import { connectKernel } from "@qntx/wallet/host";
|
|
3
3
|
import { decode, encode } from "cborg";
|
|
4
|
-
|
|
4
|
+
// src/cbor.ts
|
|
5
5
|
/**
|
|
6
6
|
* CBOR framing for the kernel channel. `ProtocolValue` travels main-side ↔ kernel-side as CBOR; the
|
|
7
7
|
* C++ layer moves bytes only. Decode is deliberately strict: floats, unsafe integers, undefined,
|
|
@@ -83,8 +83,7 @@ function decodeFrame(frame) {
|
|
|
83
83
|
throw classify(error);
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
-
|
|
87
|
-
//#region src/host.ts
|
|
86
|
+
// src/host.ts
|
|
88
87
|
function spawnFromHost(host, backgroundLockSeconds) {
|
|
89
88
|
const exitHandlers = /* @__PURE__ */ new Set();
|
|
90
89
|
let exited = false;
|
|
@@ -135,8 +134,7 @@ function spawnFromHost(host, backgroundLockSeconds) {
|
|
|
135
134
|
}
|
|
136
135
|
};
|
|
137
136
|
}
|
|
138
|
-
|
|
139
|
-
//#region src/index.ts
|
|
137
|
+
// src/index.ts
|
|
140
138
|
/**
|
|
141
139
|
* `@qntx/wallet-native`: the RN host entry. `createNativeWallet` adapts a `KernelHost` (Nitro
|
|
142
140
|
* hybrid from W2b, or any object with the same shape) to the wallet `connectKernel` entry — the
|
|
@@ -164,7 +162,6 @@ async function createNativeWallet(options) {
|
|
|
164
162
|
setTimer
|
|
165
163
|
});
|
|
166
164
|
}
|
|
167
|
-
//#endregion
|
|
168
165
|
export { CborError, FRAME_MAX_BYTES, createNativeWallet, decodeFrame, encodeFrame, spawnFromHost };
|
|
169
166
|
|
|
170
167
|
//# sourceMappingURL=index.mjs.map
|