Skip to content
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
21 changes: 15 additions & 6 deletions modules/passkey-crypto/src/derivePasskeyPrfKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { buildEvalByCredential, matchDeviceByCredentialId } from './prfHelpers';
import { derivePassword } from './derivePassword';
import type { WebAuthnProvider } from './webAuthnTypes';

interface AssertionChallengeResponse {
interface AuthChallengeResponse {
challenge: string;
allowCredentials?: Array<{ id: string; type: string; transports?: string[] }>;
origin?: string;
}

/**
Expand All @@ -23,7 +25,7 @@ export async function derivePasskeyPrfKey(params: {

// Fetch the wallet's user keychain to get webauthnDevices
const keychain = await wallet.getEncryptedUserKeychain();
const devices = keychain.webauthnDevices;
const devices = (keychain as any).webauthnDevices ?? (keychain as any).webAuthnDevices;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please avoid any casts


if (!devices || devices.length === 0) {
throw new Error('No passkey devices available');
Expand All @@ -36,15 +38,22 @@ export async function derivePasskeyPrfKey(params: {
throw new Error('No passkey devices available with a valid PRF salt');
}

// Fetch a server-issued assertion challenge
const { challenge } = (await bitgo
.get(bitgo.url('/user/otp/webauthn/assertion', 2))
.result()) as AssertionChallengeResponse;
// Fetch a server-issued assertion challenge via the auth endpoint
const { challenge } = (await bitgo.get(bitgo.url('/user/otp/webauthn/auth', 2)).result()) as AuthChallengeResponse;

// Build allowCredentials so the browser knows which credentials to use.
// Pass the Buffer (Uint8Array) directly — not .buffer — so the provider
// layer can correctly slice it via ArrayBuffer.isView.
const allowCredentials = Object.keys(evalByCredential).map((credId) => ({
type: 'public-key' as const,
id: Buffer.from(credId.replace(/-/g, '+').replace(/_/g, '/'), 'base64') as unknown as ArrayBuffer,
}));

// Trigger WebAuthn assertion with PRF evaluation via the provider (navigator layer)
const result = await provider.get({
publicKey: {
challenge: Buffer.from(challenge, 'base64'),
allowCredentials,
} as PublicKeyCredentialRequestOptions,
evalByCredential,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ describe('derivePasskeyPrfKey', function () {
assert.strictEqual(getCallArgs.evalByCredential['cred-bbb'], 'salt-bbb');
// Verify bitgo was used to fetch the assertion challenge
assert.ok(mockBitGo.get.calledOnce);
assert.ok(mockBitGo.url.calledWith('/user/otp/webauthn/assertion', 2));
assert.ok(mockBitGo.url.calledWith('/user/otp/webauthn/auth', 2));
});

it("should throw 'No passkey devices available' when no devices", async function () {
Expand Down
1 change: 1 addition & 0 deletions modules/web-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"@bitgo/sdk-coin-xtz": "^2.10.7",
"@bitgo/sdk-coin-zec": "^2.8.7",
"@bitgo/sdk-core": "^36.44.0",
"@bitgo/passkey-crypto": "*",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this be versioned?

"@bitgo/sdk-hmac": "^1.9.0",
"@bitgo/sdk-lib-mpc": "^10.12.0",
"@bitgo/sdk-opensslbytes": "^2.1.0",
Expand Down
2 changes: 2 additions & 0 deletions modules/web-demo/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const EcdsaChallengeComponent = lazy(
() => import('@components/EcdsaChallenge'),
);
const WebCryptoAuthComponent = lazy(() => import('@components/WebCryptoAuth'));
const PasskeyDemo = lazy(() => import('@components/PasskeyDemo'));

const Loading = () => <div>Loading route...</div>;

Expand All @@ -40,6 +41,7 @@ const App = () => {
path="/webcrypto-auth"
element={<WebCryptoAuthComponent />}
/>
<Route path="/passkey-demo" element={<PasskeyDemo />} />
</Routes>
</Suspense>
</Layout>
Expand Down
6 changes: 6 additions & 0 deletions modules/web-demo/src/components/Navbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ const Navbar = () => {
>
WebCrypto Auth
</NavItem>
<NavItem
activeRoute={pathname === '/passkey-demo'}
onClick={() => navigate('/passkey-demo')}
>
Passkey Demo
</NavItem>
</NavbarContainer>
);
};
Expand Down
Loading
Loading