Skip to content
Draft
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
2 changes: 2 additions & 0 deletions build/entitlements.mac.plist
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,7 @@
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.developer.web-browser.public-key-credential</key>
<true/>
</dict>
</plist>
13 changes: 13 additions & 0 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"": {
"name": "flow-browser",
"dependencies": {
"@electron-webauthn/native": "^0.0.5",
"@ghostery/adblocker-electron": "^2.5.2",
"@phosphor-icons/core": "^2.1.1",
"better-sqlite3": "^11.10.0",
Expand Down Expand Up @@ -149,6 +150,18 @@

"@electron-toolkit/tsconfig": ["@electron-toolkit/tsconfig@1.0.1", "", { "peerDependencies": { "@types/node": "*" } }, "sha512-M0Mol3odspvtCuheyujLNAW7bXq7KFNYVMRtpjFa4ZfES4MuklXBC7Nli/omvc+PRKlrklgAGx3l4VakjNo8jg=="],

"@electron-webauthn/native": ["@electron-webauthn/native@0.0.5", "", { "optionalDependencies": { "@electron-webauthn/native-darwin-arm64": "0.0.5", "@electron-webauthn/native-darwin-x64": "0.0.5", "@electron-webauthn/native-linux-x64-gnu": "0.0.5", "@electron-webauthn/native-win32-arm64-msvc": "0.0.5", "@electron-webauthn/native-win32-x64-msvc": "0.0.5" } }, "sha512-Cd5PuHGfY/o1Sf7RD6DuJ+276CjSyGs++u7wb4NwZihXioDs8Jo7ikFnTeVVMtfWP+ddrV7Wk4KUmKOnnubclw=="],

"@electron-webauthn/native-darwin-arm64": ["@electron-webauthn/native-darwin-arm64@0.0.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B6PKEKUpU2YJHfr9lj0N01w69vmOwvlKDumAKnicuzcZEA1Hto6DXnDlehNNu/FGH1L8nihLCTalFSEaAM2yIg=="],

"@electron-webauthn/native-darwin-x64": ["@electron-webauthn/native-darwin-x64@0.0.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-Mfa5z7pSfad1JNegA9fpMEr5WucLpB3uyMsR4qY7F7+qVT5bRBWNZDnZ3vFzLDTGkt7m8M+LlbOD0yjAMGJrOw=="],

"@electron-webauthn/native-linux-x64-gnu": ["@electron-webauthn/native-linux-x64-gnu@0.0.5", "", { "os": "linux", "cpu": "x64" }, "sha512-+0FGl9E7Q2tC3lzNlgFj21sICw2mne99rHp+L224u/JVuUxenNjsFDOx/W/a6khJj7rhmH+0xgFILHL9H8zPXw=="],

"@electron-webauthn/native-win32-arm64-msvc": ["@electron-webauthn/native-win32-arm64-msvc@0.0.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WJmMtNroY6tgb7kM7CZPd8cROsAuGejsSFEgN5PsJTCXu6wmg9bmoT7FdWLQ8PrDrdqAZOMu2Y7V0UENM4f6nw=="],

"@electron-webauthn/native-win32-x64-msvc": ["@electron-webauthn/native-win32-x64-msvc@0.0.5", "", { "os": "win32", "cpu": "x64" }, "sha512-ma5bPByzIXPIPRtg/le447nSwEZoySnVpz8zo6H6IJIYh/pwLZgoQAFDn41gzU6BSs3mMnKuzbe6mW3JrusqsQ=="],

"@electron/asar": ["@electron/asar@3.2.18", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-2XyvMe3N3Nrs8cV39IKELRHTYUWFKrmqqSY1U+GMlc0jvqjIVnoxhNd2H4JolWQncbJi1DCvb5TNxZuI2fEjWg=="],

"@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="],
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"script:upgrade-electron-to-next": "bun run scripts/electron-upgrader/next.ts"
},
"dependencies": {
"@electron-webauthn/native": "^0.0.5",
"@ghostery/adblocker-electron": "^2.5.2",
"@phosphor-icons/core": "^2.1.1",
"better-sqlite3": "^11.10.0",
Expand Down
1 change: 1 addition & 0 deletions src/main/ipc/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ import "@/ipc/app/onboarding";

// Special
import "@/ipc/listeners-manager";
import "@/ipc/webauthn";
93 changes: 93 additions & 0 deletions src/main/ipc/webauthn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import * as webauthn from "@electron-webauthn/native";
import { ipcMain } from "electron";

// Helper function to convert BufferSource to Buffer
function toBuffer(data: BufferSource | undefined): Buffer {
if (!data) return Buffer.alloc(0);
if (Buffer.isBuffer(data)) return data;
if (data instanceof ArrayBuffer) return Buffer.from(data);
if (ArrayBuffer.isView(data)) return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
return Buffer.alloc(0);
}

// Helper function to convert Buffer to ArrayBuffer
function toArrayBuffer(buffer: Buffer): ArrayBuffer {
const arrayBuffer = new ArrayBuffer(buffer.length);
const view = new Uint8Array(arrayBuffer);
for (let i = 0; i < buffer.length; ++i) {
view[i] = buffer[i];
}
return arrayBuffer;
}

ipcMain.handle(
"webauthn:create",
async (_event, options: CredentialCreationOptions | undefined): Promise<PublicKeyCredential | null> => {
// TODO: Implement create
console.log("create", options);

return null;
}
);

ipcMain.handle(
"webauthn:get",
async (_event, options: CredentialRequestOptions | undefined): Promise<PublicKeyCredential | null> => {
if (!options) {
return null;
}

// Conditional mediation is not supported yet
if (options.mediation === "conditional") {
return null;
}

console.log("get", options);

const publicKeyOptions = options.publicKey;
if (!publicKeyOptions) {
return null;
}

const allowCredentials: webauthn.PublicKeyCredentialDescriptor[] =
publicKeyOptions.allowCredentials?.map((cred) => ({
type: cred.type,
id: toBuffer(cred.id),
transports: cred.transports
})) ?? [];

const credential = await webauthn.get({
challenge: toBuffer(publicKeyOptions.challenge),
rpId: publicKeyOptions.rpId,
timeout: publicKeyOptions.timeout,
userVerification: publicKeyOptions.userVerification,
allowCredentials: allowCredentials
});

const publicKeyCredential: PublicKeyCredential = {
authenticatorAttachment: credential.authenticatorAttachment ?? null,
getClientExtensionResults: () => ({}),
id: credential.id ?? null,
rawId: toArrayBuffer(credential.rawId),
type: credential.type ?? null,
response: {
clientDataJSON: toArrayBuffer(credential.response)
},
toJSON() {
const cloned: Partial<PublicKeyCredential> = {
...publicKeyCredential
};
delete cloned.toJSON;
delete cloned.getClientExtensionResults;
return JSON.stringify(cloned);
}
};
return publicKeyCredential;
}
);

ipcMain.handle("webauthn:is-available", async (): Promise<boolean> => {
const isSupported = await webauthn.isSupported();
console.log("webauthn:is-available", isSupported);
return isSupported;
});
81 changes: 81 additions & 0 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,87 @@ if (hasPermission("browser")) {
injectBrowserAction();
}

// PASSKEYS PATCH //
const SHOULD_PATCH_PASSKEYS = "navigator" in globalThis && "credentials" in globalThis.navigator;
if (SHOULD_PATCH_PASSKEYS) {
type PatchedCredentialsContainer = Pick<CredentialsContainer, "create" | "get"> & {
isAvailable: () => Promise<boolean>;
isConditionalMediationAvailable: () => Promise<boolean>;
};

const patchedCredentialsContainer: PatchedCredentialsContainer = {
create: async (options) => {
return ipcRenderer.invoke("webauthn:create", options);
},
get: async (options) => {
return ipcRenderer.invoke("webauthn:get", options);
},
isAvailable: async () => {
return ipcRenderer.invoke("webauthn:is-available");
},
isConditionalMediationAvailable: async () => {
return false;
}
};
contextBridge.exposeInMainWorld("electronCredentials", patchedCredentialsContainer);

const tinyPasskeysScript = () => {
if ("electronCredentials" in globalThis) {
const patchedCredentials: typeof patchedCredentialsContainer = globalThis.electronCredentials;

if ("navigator" in globalThis && "credentials" in globalThis.navigator) {
const credentials = globalThis.navigator.credentials;
const oldCredentialsCreate = credentials.create.bind(credentials);
const oldCredentialsGet = credentials.get.bind(credentials);

// navigator.credentials.create()
credentials.create = async (options) => {
if (options) {
if (options.publicKey) {
return await patchedCredentials.create(options);
}
}

return await oldCredentialsCreate(options);
};

// navigator.credentials.get()
credentials.get = async (options) => {
if (options) {
// Conditional mediation is not supported yet
if (options.mediation === "conditional") {
return null;
}

if (options.publicKey) {
return await patchedCredentials.get(options);
}
}

return await oldCredentialsGet(options);
};
}

if (
"PublicKeyCredential" in globalThis &&
"isUserVerifyingPlatformAuthenticatorAvailable" in globalThis.PublicKeyCredential
) {
// PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
globalThis.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable = patchedCredentials.isAvailable;

// PublicKeyCredential.isConditionalMediationAvailable()
globalThis.PublicKeyCredential.isConditionalMediationAvailable =
patchedCredentials.isConditionalMediationAvailable;
}

delete globalThis.electronCredentials;
}
};
contextBridge.executeInMainWorld({
func: tinyPasskeysScript
});
}

// INTERNAL FUNCTIONS //
function getOSFromPlatform(platform: NodeJS.Platform) {
switch (platform) {
Expand Down
Loading