diff --git a/src/frontend/src/flows/recovery/recoverWith/device.ts b/src/frontend/src/flows/recovery/recoverWith/device.ts index 1a01d3d04f..5cea032d8b 100644 --- a/src/frontend/src/flows/recovery/recoverWith/device.ts +++ b/src/frontend/src/flows/recovery/recoverWith/device.ts @@ -1,6 +1,7 @@ import { CredentialId, DeviceData } from "$generated/internet_identity_types"; import { promptUserNumberTemplate } from "$src/components/promptUserNumber"; import { toast } from "$src/components/toast"; +import { convertToCredentialData } from "$src/utils/credential-devices"; import { AuthFail, Connection, @@ -96,10 +97,7 @@ const attemptRecovery = async ({ const credentialData = recoveryCredentials .filter(hasCredentialId) - .map(({ credential_id, pubkey }) => ({ - pubkey, - credential_id: credential_id[0], - })); + .map(convertToCredentialData); return await connection.fromWebauthnCredentials(userNumber, credentialData); }; diff --git a/src/frontend/src/utils/authnMethodData.ts b/src/frontend/src/utils/authnMethodData.ts index 46e4a833c3..9cccc3897c 100644 --- a/src/frontend/src/utils/authnMethodData.ts +++ b/src/frontend/src/utils/authnMethodData.ts @@ -3,8 +3,8 @@ import { AuthnMethodSecuritySettings, MetadataMapV2, } from "$generated/internet_identity_types"; +import { CredentialId } from "$src/utils/credential-devices"; import { readDeviceOrigin } from "$src/utils/iiConnection"; -import { CredentialId } from "$src/utils/multiWebAuthnIdentity"; import { DerEncodedPublicKey } from "@dfinity/agent"; import { nonNullish } from "@dfinity/utils"; diff --git a/src/frontend/src/utils/credential-devices.ts b/src/frontend/src/utils/credential-devices.ts new file mode 100644 index 0000000000..ee5c5a4f7e --- /dev/null +++ b/src/frontend/src/utils/credential-devices.ts @@ -0,0 +1,20 @@ +import { DeviceData, DeviceKey } from "$generated/internet_identity_types"; +import { DerEncodedPublicKey } from "@dfinity/agent"; + +export type CredentialId = ArrayBuffer; +export type CredentialData = { + pubkey: DerEncodedPublicKey; + credentialId: CredentialId; + origin?: string; +}; + +const derFromPubkey = (pubkey: DeviceKey): DerEncodedPublicKey => + new Uint8Array(pubkey).buffer as DerEncodedPublicKey; + +export const convertToCredentialData = ( + device: Omit +): CredentialData => ({ + credentialId: Buffer.from(device.credential_id[0] ?? []), + pubkey: derFromPubkey(device.pubkey), + origin: device.origin[0], +}); diff --git a/src/frontend/src/utils/findWebAuthnRpId.test.ts b/src/frontend/src/utils/findWebAuthnRpId.test.ts index 2f97962c94..10eff0ceed 100644 --- a/src/frontend/src/utils/findWebAuthnRpId.test.ts +++ b/src/frontend/src/utils/findWebAuthnRpId.test.ts @@ -1,16 +1,11 @@ -import { DeviceData } from "$generated/internet_identity_types"; +import { CredentialData } from "./credential-devices"; import { findWebAuthnRpId } from "./findWebAuthnRpId"; describe("findWebAuthnRpId", () => { - const mockDeviceData = (origin: [] | [string]): DeviceData => ({ + const mockDeviceData = (origin?: string): CredentialData => ({ origin, - alias: "test-device", - metadata: [], - protection: { protected: null }, - pubkey: [], - key_type: { platform: null }, - purpose: { authentication: null }, - credential_id: [], + credentialId: new ArrayBuffer(1), + pubkey: new ArrayBuffer(1), }); beforeEach(() => { @@ -18,10 +13,10 @@ describe("findWebAuthnRpId", () => { }); test("returns undefined if a device is registered for the current domain", () => { - const devices: DeviceData[] = [ - mockDeviceData(["https://identity.ic0.app"]), - mockDeviceData(["https://identity.internetcomputer.org"]), - mockDeviceData(["https://identity.icp0.io"]), + const devices: CredentialData[] = [ + mockDeviceData("https://identity.ic0.app"), + mockDeviceData("https://identity.internetcomputer.org"), + mockDeviceData("https://identity.icp0.io"), ]; const currentUrl = "https://identity.ic0.app"; @@ -29,10 +24,10 @@ describe("findWebAuthnRpId", () => { }); test("returns undefined for devices with default domain when the current domain matches", () => { - const devices: DeviceData[] = [ - mockDeviceData([]), // Empty origin defaults to defaultDomain `https://identity.ic0.app` - mockDeviceData(["https://identity.internetcomputer.org"]), - mockDeviceData(["https://identity.icp0.io"]), + const devices: CredentialData[] = [ + mockDeviceData(), // Empty origin defaults to defaultDomain `https://identity.ic0.ap` + mockDeviceData("https://identity.internetcomputer.org"), + mockDeviceData("https://identity.icp0.io"), ]; const currentUrl = "https://identity.ic0.app"; @@ -40,9 +35,9 @@ describe("findWebAuthnRpId", () => { }); test("returns undefined if a device is registered for the current domain", () => { - const devices: DeviceData[] = [ - mockDeviceData(["https://beta.identity.ic0.app"]), - mockDeviceData(["https://beta.identity.internetcomputer.org"]), + const devices: CredentialData[] = [ + mockDeviceData("https://beta.identity.ic0.app"), + mockDeviceData("https://beta.identity.internetcomputer.org"), ]; const currentUrl = "https://beta.identity.ic0.app"; @@ -50,10 +45,10 @@ describe("findWebAuthnRpId", () => { }); test("returns undefined if a device is registered for the current domain", () => { - const devices: DeviceData[] = [ - mockDeviceData(["https://identity.ic0.app"]), - mockDeviceData(["https://identity.internetcomputer.org"]), - mockDeviceData(["https://identity.icp0.io"]), + const devices: CredentialData[] = [ + mockDeviceData("https://identity.ic0.app"), + mockDeviceData("https://identity.internetcomputer.org"), + mockDeviceData("https://identity.icp0.io"), ]; const currentUrl = "https://identity.internetcomputer.org"; @@ -61,9 +56,9 @@ describe("findWebAuthnRpId", () => { }); test("returns the second default preferred domain if no device is registered for the current domain", () => { - const devices: DeviceData[] = [ - mockDeviceData(["https://identity.internetcomputer.org"]), - mockDeviceData(["https://identity.icp0.io"]), + const devices: CredentialData[] = [ + mockDeviceData("https://identity.internetcomputer.org"), + mockDeviceData("https://identity.icp0.io"), ]; const currentUrl = "https://identity.ic0.app"; @@ -73,9 +68,9 @@ describe("findWebAuthnRpId", () => { }); test("returns the first default preferred domain if no device is registered for the current domain", () => { - const devices: DeviceData[] = [ - mockDeviceData(["https://identity.ic0.app"]), - mockDeviceData(["https://identity.icp0.io"]), + const devices: CredentialData[] = [ + mockDeviceData("https://identity.ic0.app"), + mockDeviceData("https://identity.icp0.io"), ]; const currentUrl = "https://identity.internetcomputer.org"; @@ -85,8 +80,8 @@ describe("findWebAuthnRpId", () => { }); test("returns the least preferred domain if devices are only on that domain", () => { - const devices: DeviceData[] = [ - mockDeviceData(["https://identity.icp0.io"]), + const devices: CredentialData[] = [ + mockDeviceData("https://identity.icp0.io"), ]; const currentUrl = "https://identity.ic0.app"; @@ -98,9 +93,9 @@ describe("findWebAuthnRpId", () => { test("uses preferred domains when provided", () => { const preferredDomains = ["ic0.app", "icp0.io", "internetcomputer.org"]; - const devices: DeviceData[] = [ - mockDeviceData(["https://identity.internetcomputer.org"]), - mockDeviceData(["https://identity.icp0.io"]), + const devices: CredentialData[] = [ + mockDeviceData("https://identity.internetcomputer.org"), + mockDeviceData("https://identity.icp0.io"), ]; const currentUrl = "https://identity.ic0.app"; @@ -110,8 +105,8 @@ describe("findWebAuthnRpId", () => { }); test("throws an error if the current domain is invalid", () => { - const devices: DeviceData[] = [ - mockDeviceData(["https://identity.ic0.app"]), + const devices: CredentialData[] = [ + mockDeviceData("https://identity.ic0.app"), ]; const currentUrl = "not-a-valid-url"; @@ -121,7 +116,9 @@ describe("findWebAuthnRpId", () => { }); test("throws an error if no devices are registered for the current or preferred domains", () => { - const devices: DeviceData[] = [mockDeviceData(["https://otherdomain.com"])]; + const devices: CredentialData[] = [ + mockDeviceData("https://otherdomain.com"), + ]; const currentUrl = "https://identity.ic0.app"; expect(() => findWebAuthnRpId(currentUrl, devices)).toThrowError( @@ -130,7 +127,7 @@ describe("findWebAuthnRpId", () => { }); test("throws an error if there are no registered devices", () => { - const devices: DeviceData[] = []; + const devices: CredentialData[] = []; const currentUrl = "https://identity.ic0.app"; expect(() => findWebAuthnRpId(currentUrl, devices)).toThrowError( diff --git a/src/frontend/src/utils/findWebAuthnRpId.ts b/src/frontend/src/utils/findWebAuthnRpId.ts index 62e3daf52d..25ab660af8 100644 --- a/src/frontend/src/utils/findWebAuthnRpId.ts +++ b/src/frontend/src/utils/findWebAuthnRpId.ts @@ -1,4 +1,4 @@ -import { DeviceData } from "$generated/internet_identity_types"; +import { CredentialData } from "./credential-devices"; const DEFAULT_DOMAIN = "https://identity.ic0.app"; @@ -32,14 +32,12 @@ const getTopAndSecondaryLevelDomain = (url: string): string => { * @returns {DeviceData[]} The list of devices registered for the domain. */ const getDevicesForDomain = ( - devices: DeviceData[], + devices: CredentialData[], domain: string -): DeviceData[] => - devices.filter((d) => { - if (d.origin.length === 0) - return domain === getTopAndSecondaryLevelDomain(DEFAULT_DOMAIN); - return d.origin.some((o) => getTopAndSecondaryLevelDomain(o) === domain); - }); +): CredentialData[] => + devices.filter( + (d) => getTopAndSecondaryLevelDomain(d.origin ?? DEFAULT_DOMAIN) === domain + ); /** * Returns the domain to use as the RP ID for WebAuthn registration. @@ -63,7 +61,7 @@ const getDevicesForDomain = ( */ export const findWebAuthnRpId = ( currentUrl: string, - devices: DeviceData[], + devices: CredentialData[], preferredDomains: string[] = ["ic0.app", "internetcomputer.org", "icp0.io"] ): string | undefined => { const currentDomain = getTopAndSecondaryLevelDomain(currentUrl); @@ -74,13 +72,13 @@ export const findWebAuthnRpId = ( ); } - const getFirstDomain = (devices: DeviceData[]): string => { + const getFirstDomain = (devices: CredentialData[]): string => { if (devices[0] === undefined) { throw new Error( "Not possible. Call this function only if devices exist." ); } - return devices[0].origin[0] ?? DEFAULT_DOMAIN; + return devices[0].origin ?? DEFAULT_DOMAIN; }; // Try current domain first if devices exist diff --git a/src/frontend/src/utils/iiConnection.ts b/src/frontend/src/utils/iiConnection.ts index 10de71a608..28ddfc7731 100644 --- a/src/frontend/src/utils/iiConnection.ts +++ b/src/frontend/src/utils/iiConnection.ts @@ -26,7 +26,6 @@ import { Timestamp, UserNumber, VerifyTentativeDeviceResponse, - WebAuthnCredential, } from "$generated/internet_identity_types"; import { fromMnemonicWithoutValidation } from "$src/crypto/ed25519"; import { features } from "$src/features"; @@ -50,6 +49,7 @@ import { } from "@dfinity/identity"; import { Principal } from "@dfinity/principal"; import { isNullish, nonNullish } from "@dfinity/utils"; +import { convertToCredentialData, CredentialData } from "./credential-devices"; import { MultiWebAuthnIdentity } from "./multiWebAuthnIdentity"; import { isRecoveryDevice, RecoveryDevice } from "./recoveryDevice"; import { isWebAuthnCancel } from "./webAuthnErrorUtils"; @@ -361,17 +361,13 @@ export class Connection { return this.fromWebauthnCredentials( userNumber, - devices.flatMap(({ credential_id, pubkey }) => { - return credential_id.length === 0 - ? [] - : [{ credential_id: credential_id[0], pubkey }]; - }) + devices.map(convertToCredentialData) ); }; fromWebauthnCredentials = async ( userNumber: bigint, - credentials: WebAuthnCredential[] + credentials: CredentialData[] ): Promise => { /* Recover the Identity (i.e. key pair) used when creating the anchor. * If the "DUMMY_AUTH" feature is set, we use a dummy identity, the same identity @@ -379,12 +375,7 @@ export class Connection { */ const identity = features.DUMMY_AUTH ? new DummyIdentity() - : MultiWebAuthnIdentity.fromCredentials( - credentials.map(({ credential_id, pubkey }) => ({ - pubkey: derFromPubkey(pubkey), - credentialId: Buffer.from(credential_id), - })) - ); + : MultiWebAuthnIdentity.fromCredentials(credentials); let delegationIdentity: DelegationIdentity; // Here we expect a webauth exception if the user canceled the webauthn prompt (triggered by diff --git a/src/frontend/src/utils/multiWebAuthnIdentity.ts b/src/frontend/src/utils/multiWebAuthnIdentity.ts index 0ba8b0688f..64253ef596 100644 --- a/src/frontend/src/utils/multiWebAuthnIdentity.ts +++ b/src/frontend/src/utils/multiWebAuthnIdentity.ts @@ -7,23 +7,13 @@ * then we know which one the user is actually using * - It doesn't support creating credentials; use `WebAuthnIdentity` for that */ -import { - DerEncodedPublicKey, - PublicKey, - Signature, - SignIdentity, -} from "@dfinity/agent"; +import { PublicKey, Signature, SignIdentity } from "@dfinity/agent"; import { DER_COSE_OID, unwrapDER, WebAuthnIdentity } from "@dfinity/identity"; import { isNullish } from "@dfinity/utils"; import borc from "borc"; +import { CredentialData } from "./credential-devices"; import { bufferEqual } from "./iiConnection"; -export type CredentialId = ArrayBuffer; -export type CredentialData = { - pubkey: DerEncodedPublicKey; - credentialId: CredentialId; -}; - /** * A SignIdentity that uses `navigator.credentials`. See https://webauthn.guide/ for * more information about WebAuthentication. diff --git a/src/frontend/src/utils/webAuthn.ts b/src/frontend/src/utils/webAuthn.ts index de5e01fb5c..5cd8b87129 100644 --- a/src/frontend/src/utils/webAuthn.ts +++ b/src/frontend/src/utils/webAuthn.ts @@ -1,9 +1,9 @@ import { DeviceData } from "$generated/internet_identity_types"; import { features } from "$src/features"; import { - creationOptions, DummyIdentity, IIWebAuthnIdentity, + creationOptions, } from "$src/utils/iiConnection"; import { diagnosticInfo, unknownToString } from "$src/utils/utils"; import { WebAuthnIdentity } from "@dfinity/identity";