379 lines
11 KiB
Text
379 lines
11 KiB
Text
/*
|
|
This file is part of Telegram Desktop,
|
|
the official desktop application for the Telegram messaging service.
|
|
|
|
For license and copyright information please follow this link:
|
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|
*/
|
|
|
|
#include "platform/platform_webauthn.h"
|
|
|
|
#if 0
|
|
|
|
#include "data/data_passkey_deserialize.h"
|
|
#include "core/application.h"
|
|
#include "window/window_controller.h"
|
|
#include "window/main_window.h"
|
|
|
|
#import <AuthenticationServices/AuthenticationServices.h>
|
|
#import <AppKit/AppKit.h>
|
|
#import <Foundation/Foundation.h>
|
|
|
|
@interface WebAuthnDelegate : NSObject<
|
|
ASAuthorizationControllerDelegate,
|
|
ASAuthorizationControllerPresentationContextProviding>
|
|
@property (nonatomic, copy) void (^completionRegister)(
|
|
const Platform::WebAuthn::RegisterResult&);
|
|
@property (nonatomic, copy) void (^completionLogin)(
|
|
const Platform::WebAuthn::LoginResult&);
|
|
@property (nonatomic, strong) ASAuthorizationController *controller API_AVAILABLE(macos(10.15));
|
|
@property (nonatomic, strong)
|
|
ASAuthorizationPlatformPublicKeyCredentialProvider *provider API_AVAILABLE(macos(12.0));
|
|
@property (nonatomic, strong) NSWindow *anchorWindow;
|
|
@end
|
|
|
|
namespace {
|
|
|
|
// Strong owners of the in-flight controller flow; main thread only.
|
|
NSMutableArray *WebAuthnAliveDelegates() {
|
|
static NSMutableArray *result = [[NSMutableArray alloc] init];
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] NSWindow *ResolveAnchorWindow() {
|
|
if (Core::IsAppLaunched()) {
|
|
const auto controller = Core::App().activeWindow()
|
|
? Core::App().activeWindow()
|
|
: Core::App().activePrimaryWindow();
|
|
if (controller) {
|
|
const auto view = reinterpret_cast<NSView*>(
|
|
controller->widget()->winId());
|
|
if (NSWindow *window = [view window]) {
|
|
return window;
|
|
}
|
|
}
|
|
}
|
|
if (NSWindow *window = [NSApp keyWindow]) {
|
|
return window;
|
|
}
|
|
if (NSWindow *window = [NSApp mainWindow]) {
|
|
return window;
|
|
}
|
|
for (NSWindow *window in [NSApp windows]) {
|
|
if (window.isVisible) {
|
|
return window;
|
|
}
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
@implementation WebAuthnDelegate
|
|
|
|
- (void)authorizationController:(ASAuthorizationController *)controller
|
|
didCompleteWithAuthorization:(ASAuthorization *)authorization
|
|
API_AVAILABLE(macos(12.0)) {
|
|
if (self.completionRegister) {
|
|
auto result = Platform::WebAuthn::RegisterResult();
|
|
if ([authorization.credential conformsToProtocol:
|
|
@protocol(ASAuthorizationPublicKeyCredentialRegistration)]) {
|
|
auto credential
|
|
= (id<ASAuthorizationPublicKeyCredentialRegistration>)
|
|
authorization.credential;
|
|
result.success = true;
|
|
result.credentialId = QByteArray::fromNSData(
|
|
credential.credentialID);
|
|
result.attestationObject = QByteArray::fromNSData(
|
|
credential.rawAttestationObject);
|
|
result.clientDataJSON = QByteArray::fromNSData(
|
|
credential.rawClientDataJSON);
|
|
}
|
|
[self deliverRegister:result];
|
|
} else if (self.completionLogin) {
|
|
auto result = Platform::WebAuthn::LoginResult();
|
|
if ([authorization.credential conformsToProtocol:
|
|
@protocol(ASAuthorizationPublicKeyCredentialAssertion)]) {
|
|
auto credential
|
|
= (id<ASAuthorizationPublicKeyCredentialAssertion>)
|
|
authorization.credential;
|
|
result.credentialId = QByteArray::fromNSData(
|
|
credential.credentialID);
|
|
result.authenticatorData = QByteArray::fromNSData(
|
|
credential.rawAuthenticatorData);
|
|
result.signature = QByteArray::fromNSData(
|
|
credential.signature);
|
|
result.clientDataJSON = QByteArray::fromNSData(
|
|
credential.rawClientDataJSON);
|
|
result.userHandle = QByteArray::fromNSData(credential.userID);
|
|
}
|
|
[self deliverLogin:result];
|
|
}
|
|
}
|
|
|
|
- (void)authorizationController:(ASAuthorizationController *)controller
|
|
didCompleteWithError:(NSError *)error
|
|
API_AVAILABLE(macos(10.15)) {
|
|
const auto isCancelled = (error.code == ASAuthorizationErrorCanceled);
|
|
const auto isUnsigned = (error.code == 1004 || error.code == 1009);
|
|
if (!isCancelled) {
|
|
NSLog(@"WebAuthn error: %@ (code: %ld)",
|
|
error.localizedDescription, (long)error.code);
|
|
}
|
|
const auto value = isUnsigned
|
|
? Platform::WebAuthn::Error::UnsignedBuild
|
|
: (isCancelled
|
|
? Platform::WebAuthn::Error::Cancelled
|
|
: Platform::WebAuthn::Error::Other);
|
|
if (self.completionRegister) {
|
|
auto result = Platform::WebAuthn::RegisterResult();
|
|
result.success = false;
|
|
result.error = value;
|
|
[self deliverRegister:result];
|
|
} else if (self.completionLogin) {
|
|
auto result = Platform::WebAuthn::LoginResult();
|
|
result.error = value;
|
|
[self deliverLogin:result];
|
|
}
|
|
}
|
|
|
|
- (ASPresentationAnchor)presentationAnchorForAuthorizationController:
|
|
(ASAuthorizationController *)controller
|
|
API_AVAILABLE(macos(10.15)) {
|
|
return self.anchorWindow ? self.anchorWindow : ResolveAnchorWindow();
|
|
}
|
|
|
|
- (void)deliverRegister:(Platform::WebAuthn::RegisterResult)result {
|
|
crl::on_main([self, result] {
|
|
if (self.completionRegister) {
|
|
self.completionRegister(result);
|
|
self.completionRegister = nil;
|
|
}
|
|
[self finishAndRelease];
|
|
});
|
|
}
|
|
|
|
- (void)deliverLogin:(Platform::WebAuthn::LoginResult)result {
|
|
crl::on_main([self, result] {
|
|
if (self.completionLogin) {
|
|
self.completionLogin(result);
|
|
self.completionLogin = nil;
|
|
}
|
|
[self finishAndRelease];
|
|
});
|
|
}
|
|
|
|
- (void)finishAndRelease {
|
|
if (@available(macOS 10.15, *)) {
|
|
self.controller = nil;
|
|
}
|
|
if (@available(macOS 12.0, *)) {
|
|
self.provider = nil;
|
|
}
|
|
self.anchorWindow = nil;
|
|
// Last statement: releases the delegate's final owner; self may die on return.
|
|
[WebAuthnAliveDelegates() removeObject:self];
|
|
}
|
|
|
|
@end
|
|
|
|
namespace Platform::WebAuthn {
|
|
|
|
bool IsSupported() {
|
|
if (@available(macOS 12.0, *)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void RegisterKey(
|
|
const Data::Passkey::RegisterData &data,
|
|
Fn<void(RegisterResult result)> callback) {
|
|
if (@available(macOS 12.0, *)) {
|
|
NSWindow *anchor = ResolveAnchorWindow();
|
|
if (!anchor) {
|
|
auto result = RegisterResult();
|
|
result.success = false;
|
|
result.error = Error::Other;
|
|
callback(result);
|
|
return;
|
|
}
|
|
|
|
auto rpId = data.rp.id.toNSString();
|
|
auto userName = data.user.name.toNSString();
|
|
auto userDisplayName = data.user.displayName.toNSString();
|
|
auto userId = [NSData dataWithBytes:data.user.id.constData()
|
|
length:data.user.id.size()];
|
|
auto challenge = [NSData dataWithBytes:data.challenge.constData()
|
|
length:data.challenge.size()];
|
|
|
|
if (!userId || !challenge) {
|
|
auto result = RegisterResult();
|
|
result.success = false;
|
|
callback(result);
|
|
return;
|
|
}
|
|
|
|
auto provider = [[ASAuthorizationPlatformPublicKeyCredentialProvider
|
|
alloc] initWithRelyingPartyIdentifier:rpId];
|
|
|
|
auto request = [provider
|
|
createCredentialRegistrationRequestWithChallenge:challenge
|
|
name:userName
|
|
userID:userId];
|
|
|
|
if (userDisplayName && userDisplayName.length > 0) {
|
|
request.displayName = userDisplayName;
|
|
}
|
|
|
|
if (@available(macOS 13.5, *)) {
|
|
request.attestationPreference =
|
|
ASAuthorizationPublicKeyCredentialAttestationKindNone;
|
|
}
|
|
|
|
auto controller = [[ASAuthorizationController alloc]
|
|
initWithAuthorizationRequests:@[request]];
|
|
|
|
auto delegate = [[WebAuthnDelegate alloc] init];
|
|
delegate.provider = provider;
|
|
delegate.controller = controller;
|
|
delegate.anchorWindow = anchor;
|
|
delegate.completionRegister = ^(const RegisterResult &result) {
|
|
callback(result);
|
|
};
|
|
|
|
controller.delegate = delegate;
|
|
controller.presentationContextProvider = delegate;
|
|
|
|
// Alive container becomes the delegate's sole owner; balance local +1s.
|
|
[WebAuthnAliveDelegates() addObject:delegate];
|
|
[controller performRequests];
|
|
[provider release];
|
|
[controller release];
|
|
[delegate release];
|
|
} else {
|
|
auto result = RegisterResult();
|
|
result.success = false;
|
|
callback(result);
|
|
}
|
|
}
|
|
|
|
void Login(
|
|
const Data::Passkey::LoginData &data,
|
|
Fn<void(LoginResult result)> callback) {
|
|
if (@available(macOS 12.0, *)) {
|
|
NSWindow *anchor = ResolveAnchorWindow();
|
|
if (!anchor) {
|
|
auto result = LoginResult();
|
|
result.error = Error::Other;
|
|
callback(result);
|
|
return;
|
|
}
|
|
|
|
auto challenge = [NSData dataWithBytes:data.challenge.constData()
|
|
length:data.challenge.size()];
|
|
|
|
if (!challenge) {
|
|
auto result = LoginResult();
|
|
callback(result);
|
|
return;
|
|
}
|
|
|
|
auto provider = [[ASAuthorizationPlatformPublicKeyCredentialProvider
|
|
alloc] initWithRelyingPartyIdentifier:data.rpId.toNSString()];
|
|
|
|
auto request = [provider
|
|
createCredentialAssertionRequestWithChallenge:challenge];
|
|
|
|
if (!data.allowCredentials.empty()) {
|
|
NSMutableArray *credentialIds = [NSMutableArray array];
|
|
for (const auto &cred : data.allowCredentials) {
|
|
auto credId = [NSData dataWithBytes:cred.id.constData()
|
|
length:cred.id.size()];
|
|
if (credId) {
|
|
[credentialIds addObject:credId];
|
|
}
|
|
}
|
|
if (credentialIds.count > 0) {
|
|
request.allowedCredentials = credentialIds;
|
|
}
|
|
}
|
|
|
|
if (data.userVerification == "required") {
|
|
request.userVerificationPreference =
|
|
ASAuthorizationPublicKeyCredentialUserVerificationPreferenceRequired;
|
|
} else if (data.userVerification == "preferred") {
|
|
request.userVerificationPreference =
|
|
ASAuthorizationPublicKeyCredentialUserVerificationPreferencePreferred;
|
|
} else if (data.userVerification == "discouraged") {
|
|
request.userVerificationPreference =
|
|
ASAuthorizationPublicKeyCredentialUserVerificationPreferenceDiscouraged;
|
|
}
|
|
|
|
auto controller = [[ASAuthorizationController alloc]
|
|
initWithAuthorizationRequests:@[request]];
|
|
|
|
auto delegate = [[WebAuthnDelegate alloc] init];
|
|
delegate.provider = provider;
|
|
delegate.controller = controller;
|
|
delegate.anchorWindow = anchor;
|
|
delegate.completionLogin = ^(const LoginResult &result) {
|
|
callback(result);
|
|
};
|
|
|
|
controller.delegate = delegate;
|
|
controller.presentationContextProvider = delegate;
|
|
|
|
// Alive container becomes the delegate's sole owner; balance local +1s.
|
|
[WebAuthnAliveDelegates() addObject:delegate];
|
|
[controller performRequests];
|
|
[provider release];
|
|
[controller release];
|
|
[delegate release];
|
|
} else {
|
|
auto result = LoginResult();
|
|
callback(result);
|
|
}
|
|
}
|
|
|
|
} // namespace Platform::WebAuthn
|
|
|
|
#endif
|
|
|
|
#include "webauthn/webauthn_common.h"
|
|
|
|
namespace Platform::WebAuthn {
|
|
|
|
bool IsSupported() {
|
|
return true;
|
|
}
|
|
|
|
void RegisterKey(
|
|
const Data::Passkey::RegisterData &data,
|
|
Fn<void(RegisterResult result)> callback) {
|
|
RegisterViaCable(data, std::move(callback));
|
|
}
|
|
|
|
void Login(
|
|
const Data::Passkey::LoginData &data,
|
|
Fn<void(LoginResult result)> callback) {
|
|
LoginViaCable(data, std::move(callback));
|
|
}
|
|
|
|
bool SecurityKeyPresent() {
|
|
return Libfido2DevicePresent();
|
|
}
|
|
|
|
void RegisterViaSecurityKey(
|
|
const Data::Passkey::RegisterData &data,
|
|
Fn<void(RegisterResult)> callback) {
|
|
RegisterViaLibfido2(data, std::move(callback));
|
|
}
|
|
|
|
void LoginViaSecurityKey(
|
|
const Data::Passkey::LoginData &data,
|
|
Fn<void(LoginResult)> callback) {
|
|
LoginViaLibfido2(data, std::move(callback));
|
|
}
|
|
|
|
} // namespace Platform::WebAuthn
|