-
Notifications
You must be signed in to change notification settings - Fork 302
feat(sdk-core): WCN-187 add passkey types, webauthn provider interface, and prf helpers #8610
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
Open
derranW26
wants to merge
1
commit into
master
Choose a base branch
from
WCN-187-passkey-types
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export { WebAuthnOtpDevice, PasskeyAuthResult, PasskeyGetOptions, WebAuthnProvider } from './types'; | ||
| export { buildEvalByCredential, matchDeviceByCredentialId } from './prfHelpers'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import type { KeychainWebauthnDevice } from '../keychain/iKeychains'; | ||
|
|
||
| /** | ||
| * Builds the PRF eval map and credential-to-device lookup from a wallet keychain's webauthn devices. | ||
| * Devices without a prfSalt are skipped. | ||
| */ | ||
| export function buildEvalByCredential(devices: KeychainWebauthnDevice[]): { | ||
| evalByCredential: Record<string, string>; | ||
| credIdToDevice: Map<string, KeychainWebauthnDevice>; | ||
| } { | ||
| const evalByCredential: Record<string, string> = {}; | ||
| const credIdToDevice = new Map<string, KeychainWebauthnDevice>(); | ||
|
|
||
| for (const device of devices) { | ||
| if (!device.prfSalt) continue; | ||
|
|
||
| const { credID } = device.authenticatorInfo; | ||
| evalByCredential[credID] = device.prfSalt; | ||
| credIdToDevice.set(credID, device); | ||
| } | ||
|
|
||
| return { evalByCredential, credIdToDevice }; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the webauthn device matching the given credential ID. | ||
| * @throws if no matching device is found | ||
| */ | ||
| export function matchDeviceByCredentialId( | ||
| devices: KeychainWebauthnDevice[], | ||
| credentialId: string | ||
| ): KeychainWebauthnDevice { | ||
| const { credIdToDevice } = buildEvalByCredential(devices); | ||
| const device = credIdToDevice.get(credentialId); | ||
| if (!device) { | ||
| throw new Error('Could not identify which passkey device was used'); | ||
| } | ||
| return device; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| export type { WebAuthnOtpDevice } from '@bitgo/public-types'; | ||
|
|
||
| /** Result of a WebAuthn assertion with the PRF extension. */ | ||
| export interface PasskeyAuthResult { | ||
| // raw PRF output; undefined if the authenticator does not support PRF | ||
| prfResult: ArrayBuffer | undefined; | ||
| // base64url credential ID identifying which passkey was used | ||
| credentialId: string; | ||
| // OTP code from the assertion | ||
| otpCode: string; | ||
| } | ||
|
|
||
| /** Options for WebAuthnProvider.get(). */ | ||
| export interface PasskeyGetOptions { | ||
| publicKey: PublicKeyCredentialRequestOptions; | ||
| // PRF eval map: { [credentialId]: prfSalt } | ||
| evalByCredential?: Record<string, string>; | ||
| } | ||
|
|
||
| /** Abstraction over the WebAuthn credential API. Inject a mock in tests. */ | ||
| export interface WebAuthnProvider { | ||
| create(options: PublicKeyCredentialCreationOptions): Promise<PublicKeyCredential>; | ||
| get(options: PasskeyGetOptions): Promise<PasskeyAuthResult>; | ||
| } | ||
79 changes: 79 additions & 0 deletions
79
modules/sdk-core/test/unit/bitgo/passkey/prfHelpers.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| import * as assert from 'assert'; | ||
| import { buildEvalByCredential, matchDeviceByCredentialId } from '../../../../src/bitgo/passkey/prfHelpers'; | ||
| import { KeychainWebauthnDevice } from '../../../../src/bitgo/keychain/iKeychains'; | ||
|
|
||
| const device1: KeychainWebauthnDevice = { | ||
| otpDeviceId: 'oid-1', | ||
| authenticatorInfo: { credID: 'cred-aaa', fmt: 'none', publicKey: 'pk-1' }, | ||
| prfSalt: 'salt-aaa', | ||
| encryptedPrv: 'enc-prv-1', | ||
| }; | ||
|
|
||
| const device2: KeychainWebauthnDevice = { | ||
| otpDeviceId: 'oid-2', | ||
| authenticatorInfo: { credID: 'cred-bbb', fmt: 'none', publicKey: 'pk-2' }, | ||
| prfSalt: 'salt-bbb', | ||
| encryptedPrv: 'enc-prv-2', | ||
| }; | ||
|
|
||
| describe('buildEvalByCredential', function () { | ||
| it('maps each device credID to its prfSalt in evalByCredential', function () { | ||
| const { evalByCredential } = buildEvalByCredential([device1, device2]); | ||
| assert.deepStrictEqual(evalByCredential, { | ||
| 'cred-aaa': 'salt-aaa', | ||
| 'cred-bbb': 'salt-bbb', | ||
| }); | ||
| }); | ||
|
|
||
| it('populates credIdToDevice with both devices', function () { | ||
| const { credIdToDevice } = buildEvalByCredential([device1, device2]); | ||
| assert.strictEqual(credIdToDevice.get('cred-aaa'), device1); | ||
| assert.strictEqual(credIdToDevice.get('cred-bbb'), device2); | ||
| }); | ||
|
|
||
| it('returns empty maps for an empty device list', function () { | ||
| const { evalByCredential, credIdToDevice } = buildEvalByCredential([]); | ||
| assert.deepStrictEqual(evalByCredential, {}); | ||
| assert.strictEqual(credIdToDevice.size, 0); | ||
| }); | ||
|
|
||
| it('skips devices with empty prfSalt', function () { | ||
| const deviceNoPrf = { ...device1, prfSalt: '' }; | ||
| const { evalByCredential, credIdToDevice } = buildEvalByCredential([deviceNoPrf, device2]); | ||
| assert.deepStrictEqual(evalByCredential, { 'cred-bbb': 'salt-bbb' }); | ||
| assert.strictEqual(credIdToDevice.has('cred-aaa'), false); | ||
| }); | ||
|
|
||
| it('skips devices with undefined prfSalt', function () { | ||
| const deviceNoPrf = { ...device1, prfSalt: undefined as unknown as string }; | ||
| const { evalByCredential, credIdToDevice } = buildEvalByCredential([deviceNoPrf, device2]); | ||
| assert.deepStrictEqual(evalByCredential, { 'cred-bbb': 'salt-bbb' }); | ||
| assert.strictEqual(credIdToDevice.has('cred-aaa'), false); | ||
| }); | ||
| }); | ||
|
|
||
| describe('matchDeviceByCredentialId', function () { | ||
| it('returns the matching device', function () { | ||
| const result = matchDeviceByCredentialId([device1, device2], 'cred-bbb'); | ||
| assert.strictEqual(result, device2); | ||
| }); | ||
|
|
||
| it('returns the first device when it matches', function () { | ||
| const result = matchDeviceByCredentialId([device1, device2], 'cred-aaa'); | ||
| assert.strictEqual(result, device1); | ||
| }); | ||
|
|
||
| it('throws with the retail error message when no device matches', function () { | ||
| assert.throws( | ||
| () => matchDeviceByCredentialId([device1, device2], 'cred-unknown'), | ||
| (err: Error) => { | ||
| assert.strictEqual(err.message, 'Could not identify which passkey device was used'); | ||
| return true; | ||
| } | ||
| ); | ||
| }); | ||
|
|
||
| it('throws when the device list is empty', function () { | ||
| assert.throws(() => matchDeviceByCredentialId([], 'cred-aaa'), Error); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
detail what these methods do and what they should be used for.