From 42b73722c42faf21ac681b2f35fa3c03ce9c23ba Mon Sep 17 00:00:00 2001 From: Wyatt Mufson Date: Tue, 27 Aug 2024 08:45:42 +0900 Subject: [PATCH 1/3] Fix some mappings in index.ts, some obj-c updates --- src/index.ts | 12 ++++++++++++ src/lib/passkey.mm | 25 +++++++++++++++---------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/index.ts b/src/index.ts index e8458fb..f34f00c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -62,6 +62,18 @@ class Passkey { } (options.publicKey as PublicKeyCredentialRequestOptions).rpId = this.domain; + options.publicKey.challenge = arrayBufferToBase64( + options.publicKey.challenge as ArrayBuffer, + ); + + (options.publicKey as PublicKeyCredentialRequestOptions).allowCredentials = + ( + options.publicKey as PublicKeyCredentialRequestOptions + ).allowCredentials?.filter((cred) => { + return ( + cred && cred.id && typeof cred.id === 'string' && cred.id.length > 0 + ); + }); return this.handler.HandlePasskeyGet(JSON.stringify(options)); } diff --git a/src/lib/passkey.mm b/src/lib/passkey.mm index b04b48a..effcae5 100644 --- a/src/lib/passkey.mm +++ b/src/lib/passkey.mm @@ -5,10 +5,6 @@ typedef void (^PasskeyCompletionHandler)(NSString *resultMessage, NSString *errorMessage); -NSData* ConvertBufferToNSData(Napi::Buffer buffer) { - return [NSData dataWithBytes:buffer.Data() length:buffer.Length()]; -} - @interface PasskeyHandlerObjC : NSObject @property (nonatomic, strong) PasskeyCompletionHandler completionHandler; @@ -28,7 +24,7 @@ - (instancetype)init { - (void)PerformCreateRequest:(NSDictionary *)options withCompletionHandler:(PasskeyCompletionHandler)completionHandler { self.completionHandler = completionHandler; - if (@available(macOS 12.0, *)) { + if (@available(macOS 13.5, *)) { NSDictionary *publicKeyOptions = options[@"publicKey"]; NSString *rpId = publicKeyOptions[@"rp"][@"id"]; NSString *userName = publicKeyOptions[@"user"][@"name"]; @@ -63,6 +59,10 @@ - (void)PerformCreateRequest:(NSDictionary *)options withCompletionHandler:(Pass request.userVerificationPreference = ASAuthorizationPublicKeyCredentialUserVerificationPreferenceDiscouraged; } } + // NSString *attestationPreference = publicKeyOptions[@"attestation"]; + // if (attestationPreference) { + // request.attestationPreference = attestationPreference; + // } ASAuthorizationController *controller = [[ASAuthorizationController alloc] initWithAuthorizationRequests:@[request]]; @@ -84,10 +84,11 @@ - (void)PerformCreateRequest:(NSDictionary *)options withCompletionHandler:(Pass - (void)PerformGetRequest:(NSDictionary *)options withCompletionHandler:(PasskeyCompletionHandler)completionHandler { self.completionHandler = completionHandler; - if (@available(macOS 12.0, *)) { + if (@available(macOS 13.5, *)) { NSDictionary *publicKeyOptions = options[@"publicKey"]; NSString *rpId = publicKeyOptions[@"rpId"]; - NSData *challenge = publicKeyOptions[@"challenge"]; + NSString *challengeString = publicKeyOptions[@"challenge"]; + NSData *challenge = [[NSData alloc] initWithBase64EncodedString:challengeString options:0]; ASAuthorizationPlatformPublicKeyCredentialProvider *provider = [[ASAuthorizationPlatformPublicKeyCredentialProvider alloc] initWithRelyingPartyIdentifier:rpId]; @@ -143,7 +144,8 @@ - (void)authorizationController:(ASAuthorizationController *)controller didCompl NSData *clientDataJSON = credential.rawClientDataJSON; NSData *attestationObject = credential.rawAttestationObject; NSString *credentialId = [credential.credentialID base64EncodedStringWithOptions:0]; - + ASAuthorizationPublicKeyCredentialAttachment attachment = credential.attachment; + NSDictionary *responseDict = @{ @"clientDataJSON": [clientDataJSON base64EncodedStringWithOptions:0], @"attestationObject": [attestationObject base64EncodedStringWithOptions:0] @@ -156,7 +158,8 @@ - (void)authorizationController:(ASAuthorizationController *)controller didCompl @"rawId": credentialId, // rawId is the raw NSData representing the credential ID @"response": responseDict, // The response object @"clientExtensionResults": @{}, // An empty dictionary, as no extensions are used in this example - @"transports": @[] // Transports are not directly available in ASAuthorizationPlatformPublicKeyCredentialRegistration + @"transports": @[], // Transports are not directly available in ASAuthorizationPlatformPublicKeyCredentialRegistration + @"authenticatorAttachment": attachment == ASAuthorizationPublicKeyCredentialAttachmentPlatform ? @'platform' : @'cross-platform', }; if (![NSJSONSerialization isValidJSONObject:publicKeyCredentialDict]) { @@ -185,6 +188,7 @@ - (void)authorizationController:(ASAuthorizationController *)controller didCompl ASAuthorizationPlatformPublicKeyCredentialAssertion *credential = (ASAuthorizationPlatformPublicKeyCredentialAssertion *)authorization.credential; NSString *credentialId = [credential.credentialID base64EncodedStringWithOptions:0]; + ASAuthorizationPublicKeyCredentialAttachment attachment = credential.attachment; // Create the "response" dictionary, simulating the AuthenticatorAssertionResponse NSDictionary *responseDict = @{ @@ -201,7 +205,8 @@ - (void)authorizationController:(ASAuthorizationController *)controller didCompl @"rawId": credentialId, // rawId is the base64-encoded credential ID @"response": responseDict, // The response object @"clientExtensionResults": @{}, // An empty dictionary, as no extensions are used in this example - @"transports": @[] // Transports are not directly available in ASAuthorizationPlatformPublicKeyCredentialAssertion + @"transports": @[], // Transports are not directly available in ASAuthorizationPlatformPublicKeyCredentialAssertion + @"authenticatorAttachment": attachment == ASAuthorizationPublicKeyCredentialAttachmentPlatform ? @'platform' : @'cross-platform', }; if (![NSJSONSerialization isValidJSONObject:publicKeyCredentialDict]) { From 9636500d5d71f8383fc3a77099554aef996eeec3 Mon Sep 17 00:00:00 2001 From: Wyatt Mufson Date: Tue, 27 Aug 2024 18:43:19 +0900 Subject: [PATCH 2/3] fixes --- src/lib/passkey.mm | 15 +++++++------- src/utils.ts | 51 ++++++++++++++++++++++++++++------------------ 2 files changed, 38 insertions(+), 28 deletions(-) diff --git a/src/lib/passkey.mm b/src/lib/passkey.mm index effcae5..fc8c65d 100644 --- a/src/lib/passkey.mm +++ b/src/lib/passkey.mm @@ -125,7 +125,7 @@ - (void)PerformGetRequest:(NSDictionary *)options withCompletionHandler:(Passkey NSLog(@"[PerformGetRequest]: Delegate and PresentationContextProvider set. Starting requests..."); - [controller performRequests]; + [controller performRequestsWithOptions:ASAuthorizationControllerRequestOptionPreferImmediatelyAvailableCredentials]; } else { NSLog(@"[PerformGetRequest]: Your macOS version does not support WebAuthn APIs."); if (completionHandler) { @@ -159,14 +159,14 @@ - (void)authorizationController:(ASAuthorizationController *)controller didCompl @"response": responseDict, // The response object @"clientExtensionResults": @{}, // An empty dictionary, as no extensions are used in this example @"transports": @[], // Transports are not directly available in ASAuthorizationPlatformPublicKeyCredentialRegistration - @"authenticatorAttachment": attachment == ASAuthorizationPublicKeyCredentialAttachmentPlatform ? @'platform' : @'cross-platform', + @"authenticatorAttachment": attachment == ASAuthorizationPublicKeyCredentialAttachmentPlatform ? @"platform" : @"cross-platform", }; if (![NSJSONSerialization isValidJSONObject:publicKeyCredentialDict]) { if (self.completionHandler) { self.completionHandler(nil, @"Invalid arguments to create"); - return; } + return; } NSError *error = nil; @@ -206,14 +206,14 @@ - (void)authorizationController:(ASAuthorizationController *)controller didCompl @"response": responseDict, // The response object @"clientExtensionResults": @{}, // An empty dictionary, as no extensions are used in this example @"transports": @[], // Transports are not directly available in ASAuthorizationPlatformPublicKeyCredentialAssertion - @"authenticatorAttachment": attachment == ASAuthorizationPublicKeyCredentialAttachmentPlatform ? @'platform' : @'cross-platform', + @"authenticatorAttachment": attachment == ASAuthorizationPublicKeyCredentialAttachmentPlatform ? @"platform" : @"cross-platform", }; if (![NSJSONSerialization isValidJSONObject:publicKeyCredentialDict]) { if (self.completionHandler) { self.completionHandler(nil, @"Invalid arguments to get"); - return; } + return; } // Serialize the PublicKeyCredential object into JSON @@ -255,10 +255,9 @@ - (ASPresentationAnchor)presentationAnchorForAuthorizationController:(ASAuthoriz if (mainWindow) { NSLog(@"[presentationAnchorForAuthorizationController]: Returning main window as presentation anchor."); return mainWindow; - } else { - NSLog(@"[presentationAnchorForAuthorizationController]: Error: No valid presentation anchor available."); - return nil; } + NSLog(@"[presentationAnchorForAuthorizationController]: Error: No valid presentation anchor available."); + return nil; } @end diff --git a/src/utils.ts b/src/utils.ts index 2beeb4b..1ca6c29 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,11 @@ +function btoa(arg: string): string { + return Buffer.from(arg, 'utf8').toString('base64'); +} + +function atob(arg: string): string { + return Buffer.from(arg, 'base64').toString('utf8'); +} + export function arrayBufferToBase64(buffer: ArrayBuffer): string { let binary = ''; const bytes = new Uint8Array(buffer); @@ -41,10 +49,10 @@ export function mapPublicKey( if (isCreate) { mapped.response.clientDataJSON = base64ToArrayBuffer( - mapped.response.clientDataJSON, + response.clientDataJSON, ); mapped.response.attestationObject = base64ToArrayBuffer( - mapped.response.attestationObject, + response.attestationObject, ); mapped.response = { @@ -82,27 +90,28 @@ export function mapPublicKey( raw.authenticatorAttachment as AuthenticatorAttachment, clientExtensionResults: raw.getClientExtensionResults(), response: { - attestationObject: toBase64url(response.attestationObject), - authenticatorData: toBase64url(response.getAuthenticatorData()), - clientDataJSON: toBase64url(response.clientDataJSON), - publicKey: toBase64url(response.getPublicKey()), - publicKeyAlgorithm: response.getPublicKeyAlgorithm(), - transports: response.getTransports() as AuthenticatorTransport[], + attestationObject: toBase64url(mapped.response.attestationObject), + authenticatorData: toBase64url( + mapped.response.getAuthenticatorData(), + ), + clientDataJSON: toBase64url(mapped.response.clientDataJSON), + publicKey: toBase64url(mapped.response.getPublicKey()), + publicKeyAlgorithm: mapped.response.getPublicKeyAlgorithm(), + transports: + mapped.response.getTransports() as AuthenticatorTransport[], }, }; }; } else { mapped.response.clientDataJSON = base64ToArrayBuffer( - mapped.response.clientDataJSON, + response.clientDataJSON, ); mapped.response.authenticatorData = base64ToArrayBuffer( - mapped.response.authenticatorData, + response.authenticatorData, ); - mapped.response.signature = base64ToArrayBuffer(mapped.response.signature); - if (mapped.response.userHandle) { - mapped.response.userHandle = base64ToArrayBuffer( - mapped.response.userHandle, - ); + mapped.response.signature = base64ToArrayBuffer(response.signature); + if (response.userHandle) { + mapped.response.userHandle = base64ToArrayBuffer(response.userHandle); } mapped.response.toJson = () => { @@ -114,17 +123,19 @@ export function mapPublicKey( authenticatorAttachment: raw.authenticatorAttachment as AuthenticatorAttachment, response: { - authenticatorData: toBase64url(response.authenticatorData), - clientDataJSON: toBase64url(response.clientDataJSON), - signature: toBase64url(response.signature), - userHandle: response.userHandle - ? toBase64url(response.userHandle) + authenticatorData: toBase64url(mapped.response.authenticatorData), + clientDataJSON: toBase64url(mapped.response.clientDataJSON), + signature: toBase64url(mapped.esponse.signature), + userHandle: mapped.response.userHandle + ? toBase64url(mapped.response.userHandle) : undefined, }, }; }; } + console.log(mapped); + return mapped; } From 80f9db8a461f8a87fc7513cd35c666fae275a599 Mon Sep 17 00:00:00 2001 From: Wyatt Mufson Date: Wed, 28 Aug 2024 12:06:41 +0900 Subject: [PATCH 3/3] Cleanup --- README.md | 5 ++- src/demo/electron-demo.ts | 2 +- src/index.ts | 9 +----- src/lib/passkey.mm | 2 +- src/utils.ts | 67 ++++++++++++++++++--------------------- 5 files changed, 35 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 8502fa0..d37438a 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ navigator.credentials.get = (options) => import { ipcMain } from 'electron'; import Passkey from 'electron-passkey'; -Passkey.getInstance().attachHandlersToMain('domain.com', ipcMain); +Passkey.getInstance().attachHandlersToMain(ipcMain); ``` ### Entitlements Setup @@ -45,8 +45,7 @@ Passkey.getInstance().attachHandlersToMain('domain.com', ipcMain); ``` 7) Check to see if your AASA is being cached by the Apple CDN at `https://app-site-association.cdn-apple.com/a/v1/DOMAIN` -8) Make sure to pass in your domain to `attachHandlersToMain()` -9) Build your electron application and sign it +8) Build your electron application and sign it ### Deployments diff --git a/src/demo/electron-demo.ts b/src/demo/electron-demo.ts index 099390e..ab02e54 100644 --- a/src/demo/electron-demo.ts +++ b/src/demo/electron-demo.ts @@ -5,7 +5,7 @@ import Passkey from '..'; // https://github.com/electron/electron/issues/25153 // app.disableHardwareAcceleration(); -Passkey.getInstance().attachHandlersToMain('google.com', ipcMain); +Passkey.getInstance().attachHandlersToMain(ipcMain); let window: BrowserWindow; diff --git a/src/index.ts b/src/index.ts index f34f00c..f18ced0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,8 +20,6 @@ class Passkey { private platform = os.platform(); - private domain: string = ''; - private constructor() { this.handler = new lib.PasskeyHandler(); // Create an instance of PasskeyHandler } @@ -43,8 +41,6 @@ class Passkey { options.publicKey.challenge = arrayBufferToBase64( options.publicKey.challenge as ArrayBuffer, ); - (options.publicKey as PublicKeyCredentialCreationOptions).rp.id = - this.domain; (options.publicKey as PublicKeyCredentialCreationOptions).user.id = arrayBufferToBase64( (options.publicKey as PublicKeyCredentialCreationOptions).user @@ -60,7 +56,6 @@ class Passkey { `electron-passkey is meant for macOS only and should NOT be run on ${this.platform}`, ); } - (options.publicKey as PublicKeyCredentialRequestOptions).rpId = this.domain; options.publicKey.challenge = arrayBufferToBase64( options.publicKey.challenge as ArrayBuffer, @@ -99,9 +94,7 @@ class Passkey { return mapPublicKey(rawString, false); } - attachHandlersToMain(domain: string, ipcMain: IpcMain): void { - this.domain = domain; - + attachHandlersToMain(ipcMain: IpcMain): void { ipcMain.handle(PassKeyMethods.createPasskey, (_event, options) => this.handlePasskeyCreate(options), ); diff --git a/src/lib/passkey.mm b/src/lib/passkey.mm index fc8c65d..b4d83a3 100644 --- a/src/lib/passkey.mm +++ b/src/lib/passkey.mm @@ -205,7 +205,7 @@ - (void)authorizationController:(ASAuthorizationController *)controller didCompl @"rawId": credentialId, // rawId is the base64-encoded credential ID @"response": responseDict, // The response object @"clientExtensionResults": @{}, // An empty dictionary, as no extensions are used in this example - @"transports": @[], // Transports are not directly available in ASAuthorizationPlatformPublicKeyCredentialAssertion + @"transports": @[@"hybrid"], // Transports are not directly available in ASAuthorizationPlatformPublicKeyCredentialAssertion @"authenticatorAttachment": attachment == ASAuthorizationPublicKeyCredentialAttachmentPlatform ? @"platform" : @"cross-platform", }; diff --git a/src/utils.ts b/src/utils.ts index 1ca6c29..c116126 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,38 +1,31 @@ -function btoa(arg: string): string { - return Buffer.from(arg, 'utf8').toString('base64'); +export function parseBuffer(buffer: ArrayBuffer): string { + return String.fromCharCode(...new Uint8Array(buffer)); } -function atob(arg: string): string { - return Buffer.from(arg, 'base64').toString('utf8'); +export function arrayBufferToBase64(buffer: ArrayBuffer): string { + return btoa(parseBuffer(buffer)); } -export function arrayBufferToBase64(buffer: ArrayBuffer): string { - let binary = ''; - const bytes = new Uint8Array(buffer); - const len = bytes.byteLength; - for (let i = 0; i < len; i += 1) { - binary += String.fromCharCode(bytes[i]); - } - return btoa(binary); +export function toBuffer(txt: string): ArrayBuffer { + return Uint8Array.from(txt, (c) => c.charCodeAt(0)).buffer; } -export function base64ToArrayBuffer(base64: string): ArrayBuffer { +function base64ToArrayBuffer(base64: string): ArrayBuffer { const binaryString = atob(base64); - const len = binaryString.length; - const bytes = new Uint8Array(len); - for (let i = 0; i < len; i += 1) { - bytes[i] = binaryString.charCodeAt(i); - } - return bytes.buffer; + return toBuffer(binaryString); } -function parseBuffer(buffer: ArrayBuffer): string { - return String.fromCharCode(...new Uint8Array(buffer)); +function base64ToBase64Url(base64: string): string { + return base64.replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', ''); +} + +export function toBase64url(buffer: ArrayBuffer): string { + const base64 = arrayBufferToBase64(buffer); + return base64ToBase64Url(base64); } -function toBase64url(buffer: ArrayBuffer): string { - const txt = btoa(parseBuffer(buffer)); - return txt.replaceAll('+', '-').replaceAll('/', '_'); +export function sha256(buffer: ArrayBuffer): Promise { + return crypto.subtle.digest('SHA-256', buffer); } export function mapPublicKey( @@ -42,7 +35,9 @@ export function mapPublicKey( const raw = JSON.parse(rawString); const mapped = { ...raw }; - mapped.rawId = base64ToArrayBuffer(raw.id); + mapped.id = base64ToBase64Url(raw.id); + mapped.rawId = base64ToArrayBuffer(raw.rawId); + mapped.getClientExtensionResults = () => raw.clientExtensionResults; const { response } = raw; @@ -77,18 +72,18 @@ export function mapPublicKey( }, getTransports(): string[] { // Return an empty array or fetch actual transports from rawJson if available - return raw.transports || []; + return mapped.transports || []; }, }; mapped.response.toJson = () => { return { - type: raw.type, - id: raw.id, + type: mapped.type, + id: mapped.id, rawId: mapped.rawId, // Same as ID, but useful in tests authenticatorAttachment: - raw.authenticatorAttachment as AuthenticatorAttachment, - clientExtensionResults: raw.getClientExtensionResults(), + mapped.authenticatorAttachment as AuthenticatorAttachment, + clientExtensionResults: mapped.getClientExtensionResults(), response: { attestationObject: toBase64url(mapped.response.attestationObject), authenticatorData: toBase64url( @@ -116,16 +111,16 @@ export function mapPublicKey( mapped.response.toJson = () => { return { - clientExtensionResults: raw.getClientExtensionResults(), - id: raw.id, + clientExtensionResults: mapped.getClientExtensionResults(), + id: mapped.id, rawId: mapped.rawId, - type: raw.type, + type: mapped.type, authenticatorAttachment: - raw.authenticatorAttachment as AuthenticatorAttachment, + mapped.authenticatorAttachment as AuthenticatorAttachment, response: { authenticatorData: toBase64url(mapped.response.authenticatorData), clientDataJSON: toBase64url(mapped.response.clientDataJSON), - signature: toBase64url(mapped.esponse.signature), + signature: toBase64url(mapped.response.signature), userHandle: mapped.response.userHandle ? toBase64url(mapped.response.userHandle) : undefined, @@ -134,8 +129,6 @@ export function mapPublicKey( }; } - console.log(mapped); - return mapped; }