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

Share CredentialData param type of fromCredentials #2742

Merged
merged 2 commits into from
Dec 11, 2024
Merged
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
6 changes: 2 additions & 4 deletions src/frontend/src/flows/recovery/recoverWith/device.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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);
};
2 changes: 1 addition & 1 deletion src/frontend/src/utils/authnMethodData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
20 changes: 20 additions & 0 deletions src/frontend/src/utils/credential-devices.ts
Original file line number Diff line number Diff line change
@@ -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<DeviceData, "alias">
): CredentialData => ({
credentialId: Buffer.from(device.credential_id[0] ?? []),
pubkey: derFromPubkey(device.pubkey),
origin: device.origin[0],
});
75 changes: 36 additions & 39 deletions src/frontend/src/utils/findWebAuthnRpId.test.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,64 @@
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(() => {
vi.spyOn(console, "error").mockImplementation(() => {});
});

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";

expect(findWebAuthnRpId(currentUrl, devices)).toBeUndefined();
});

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";

expect(findWebAuthnRpId(currentUrl, devices)).toBeUndefined();
});

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";

expect(findWebAuthnRpId(currentUrl, devices)).toBeUndefined();
});

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";

expect(findWebAuthnRpId(currentUrl, devices)).toBeUndefined();
});

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";

Expand All @@ -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";

Expand All @@ -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";

Expand All @@ -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";

Expand All @@ -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";

Expand All @@ -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(
Expand All @@ -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(
Expand Down
20 changes: 9 additions & 11 deletions src/frontend/src/utils/findWebAuthnRpId.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DeviceData } from "$generated/internet_identity_types";
import { CredentialData } from "./credential-devices";

const DEFAULT_DOMAIN = "https://identity.ic0.app";

Expand Down Expand Up @@ -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.
Expand All @@ -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);
Expand All @@ -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
Expand Down
17 changes: 4 additions & 13 deletions src/frontend/src/utils/iiConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -361,30 +361,21 @@ 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<LoginSuccess | WebAuthnFailed | AuthFail> => {
/* 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
* that is used in the register flow.
*/
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
Expand Down
14 changes: 2 additions & 12 deletions src/frontend/src/utils/multiWebAuthnIdentity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/src/utils/webAuthn.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
Loading