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 e8458fb..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,8 +56,19 @@ 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, + ); + + (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)); } @@ -87,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 b04b48a..b4d83a3 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]; @@ -124,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) { @@ -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,14 +158,15 @@ - (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]) { if (self.completionHandler) { self.completionHandler(nil, @"Invalid arguments to create"); - return; } + return; } NSError *error = nil; @@ -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,14 +205,15 @@ - (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", }; if (![NSJSONSerialization isValidJSONObject:publicKeyCredentialDict]) { if (self.completionHandler) { self.completionHandler(nil, @"Invalid arguments to get"); - return; } + return; } // Serialize the PublicKeyCredential object into JSON @@ -250,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..c116126 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,30 +1,31 @@ +export function parseBuffer(buffer: ArrayBuffer): string { + return String.fromCharCode(...new Uint8Array(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); + return btoa(parseBuffer(buffer)); +} + +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('=', ''); } -function toBase64url(buffer: ArrayBuffer): string { - const txt = btoa(parseBuffer(buffer)); - return txt.replaceAll('+', '-').replaceAll('/', '_'); +export function toBase64url(buffer: ArrayBuffer): string { + const base64 = arrayBufferToBase64(buffer); + return base64ToBase64Url(base64); +} + +export function sha256(buffer: ArrayBuffer): Promise { + return crypto.subtle.digest('SHA-256', buffer); } export function mapPublicKey( @@ -34,17 +35,19 @@ 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; if (isCreate) { mapped.response.clientDataJSON = base64ToArrayBuffer( - mapped.response.clientDataJSON, + response.clientDataJSON, ); mapped.response.attestationObject = base64ToArrayBuffer( - mapped.response.attestationObject, + response.attestationObject, ); mapped.response = { @@ -69,56 +72,57 @@ 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(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 = () => { 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(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.response.signature), + userHandle: mapped.response.userHandle + ? toBase64url(mapped.response.userHandle) : undefined, }, };