Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixing get credentials #17

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -45,8 +45,7 @@ Passkey.getInstance().attachHandlersToMain('domain.com', ipcMain);
</array>
```
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

Expand Down
2 changes: 1 addition & 1 deletion src/demo/electron-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
21 changes: 13 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
Expand All @@ -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));
}

Expand All @@ -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),
);
Expand Down
36 changes: 20 additions & 16 deletions src/lib/passkey.mm
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@

typedef void (^PasskeyCompletionHandler)(NSString *resultMessage, NSString *errorMessage);

NSData* ConvertBufferToNSData(Napi::Buffer<uint8_t> buffer) {
return [NSData dataWithBytes:buffer.Data() length:buffer.Length()];
}

@interface PasskeyHandlerObjC : NSObject <ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding>

@property (nonatomic, strong) PasskeyCompletionHandler completionHandler;
Expand All @@ -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"];
Expand Down Expand Up @@ -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]];
Expand All @@ -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];
Expand Down Expand Up @@ -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) {
Expand All @@ -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]
Expand All @@ -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;
Expand All @@ -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 = @{
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
102 changes: 53 additions & 49 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -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<ArrayBuffer> {
return crypto.subtle.digest('SHA-256', buffer);
}

export function mapPublicKey(
Expand All @@ -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 = {
Expand All @@ -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,
},
};
Expand Down