Skip to content

Commit

Permalink
Pre-fill anchors in VC flow (#2109)
Browse files Browse the repository at this point in the history
* Pre-fill anchors in VC flow

This updates the storage to store (a digest of) principals of visited
dapps. This allows the VC flow to know (in the general, best case
scenario) which anchor (identity number) the user is intending to use.

The storage migration includes three things:

* Anchors are not written directly as `{ 10000:  <data> }` anymore but
  are nested as `{ anchors: { 10000: <data> } }` to allow for a new
  top-level field `hasher` which is global to all anchors.
* Said new field `hasher` is introduced
* Anchors themselves now have a new field, `knownPrincipalDigests`.

See storage module documentation for more information on the digests.

* Improve principal storage & test

* Prune old principals
  • Loading branch information
nmattia authored Dec 7, 2023
1 parent 999a073 commit e80c5b6
Show file tree
Hide file tree
Showing 6 changed files with 494 additions and 126 deletions.
10 changes: 10 additions & 0 deletions src/frontend/src/flows/authorize/postMessageInterface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Types and functions related to the window post message interface used by
// applications that want to authenticate the user using Internet Identity
import { setKnownPrincipal } from "$src/storage";
import { LoginData } from "$src/utils/flowResult";
import { unknownToRecord } from "$src/utils/utils";
import { Signature } from "@dfinity/agent";
Expand Down Expand Up @@ -179,6 +180,15 @@ export async function authenticationProtocol({

const [userKey, parsed_signed_delegation] = result;

const userPublicKey = Uint8Array.from(userKey);
const principal = Principal.selfAuthenticating(userPublicKey);

await setKnownPrincipal({
userNumber: authSuccess.userNumber,
origin: authContext.requestOrigin,
principal,
});

window.opener.postMessage(
{
kind: "authorize-client-success",
Expand Down
8 changes: 7 additions & 1 deletion src/frontend/src/flows/verifiableCredentials/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { withLoader } from "$src/components/loader";
import { showMessage } from "$src/components/message";
import { showSpinner } from "$src/components/spinner";
import { fetchDelegation } from "$src/flows/authorize/fetchDelegation";
import { getAnchorByPrincipal } from "$src/storage";
import { AuthenticatedConnection, Connection } from "$src/utils/iiConnection";
import {
Delegation,
Expand Down Expand Up @@ -98,12 +99,17 @@ const verifyCredentials = async ({
return abortedCredentials({ reason: "auth_failed_issuer" });
}

const userNumber_ = await getAnchorByPrincipal({
origin: rpOrigin,
principal: givenP_RP,
});

// Ask user to confirm the verification of credentials
const allowed = await allowCredentials({
relyingOrigin: rpOrigin,
providerOrigin: issuerOrigin,
consentMessage: consentInfo.consent_message,
userNumber: undefined,
userNumber: userNumber_,
});
if (allowed.tag === "canceled") {
return "aborted";
Expand Down
150 changes: 107 additions & 43 deletions src/frontend/src/storage/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Principal } from "@dfinity/principal";
import { nonNullish } from "@dfinity/utils";
import { IDBFactory } from "fake-indexeddb";
import {
Expand All @@ -6,7 +7,14 @@ import {
keys as idbKeys,
set as idbSet,
} from "idb-keyval";
import { MAX_SAVED_ANCHORS, getAnchors, setAnchorUsed } from ".";
import {
MAX_SAVED_ANCHORS,
MAX_SAVED_PRINCIPALS,
getAnchorByPrincipal,
getAnchors,
setAnchorUsed,
setKnownPrincipal,
} from ".";

beforeAll(() => {
// Initialize the IndexedDB global
Expand All @@ -18,7 +26,7 @@ test("anchors default to nothing", async () => {
});

test(
"old userNumber is recovered",
"old userNumber V0 is recovered",
withStorage(
async () => {
expect(await getAnchors()).toStrictEqual([BigInt(123456)]);
Expand Down Expand Up @@ -76,20 +84,18 @@ test(
{
localStorage: {
before: { userNumber: "123456" },
after: (storage) => {
const value = storage["anchors"];
expect(value).toBeDefined();
const anchors = JSON.parse(value);
expect(anchors).toBeTypeOf("object");
expect(anchors["123456"]).toBeDefined();
},
},
indexeddb: {
after: (storage) => {
// Written to V3
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const anchors: any = storage["anchors"];
expect(anchors).toBeTypeOf("object");
expect(anchors["123456"]).toBeDefined();
const storageV3: any = storage["ii-storage-v3"];
expect(storageV3).toBeTypeOf("object");

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const anchorsV3: any = storageV3["anchors"];
expect(anchorsV3).toBeTypeOf("object");
expect(anchorsV3["123456"]).toBeDefined();
},
},
}
Expand Down Expand Up @@ -117,23 +123,46 @@ test(
);

test(
"anchors are also written to localstorage",
"V2 anchors are migrated",
withStorage(
async () => {
expect(await getAnchors()).toContain(BigInt(10000));
expect(await getAnchors()).toContain(BigInt(10001));
expect(await getAnchors()).toContain(BigInt(10003));
},
{
indexeddb: {
before: {
/* V2 layout */
anchors: {
"10000": { lastUsedTimestamp: 0 },
"10001": { lastUsedTimestamp: 0 },
"10003": { lastUsedTimestamp: 0 },
},
},
},
}
)
);

test(
"anchors are also written to V2",
withStorage(
async () => {
await setAnchorUsed(BigInt(10000));
await setAnchorUsed(BigInt(10001));
await setAnchorUsed(BigInt(10003));
},
{
localStorage: {
indexeddb: {
after: (storage) => {
const value = storage["anchors"];
expect(value).toBeDefined();
const anchors = JSON.parse(value);
expect(anchors).toBeTypeOf("object");
expect(anchors["10000"]).toBeDefined();
expect(anchors["10001"]).toBeDefined();
expect(anchors["10003"]).toBeDefined();
// Written to V2
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const anchorsV2: any = storage["anchors"];
expect(anchorsV2).toBeTypeOf("object");
expect(anchorsV2["10000"]).toBeDefined();
expect(anchorsV2["10001"]).toBeDefined();
expect(anchorsV2["10003"]).toBeDefined();
},
},
}
Expand Down Expand Up @@ -162,6 +191,28 @@ test(
})
);

test(
"principal digests are stored",
withStorage(async () => {
const origin = "https://example.com";
const principal = Principal.fromText("2vxsx-fae");
await setKnownPrincipal({
userNumber: BigInt(10000),
origin,
principal,
});

const otherOrigin = "https://other.com";
expect(
await getAnchorByPrincipal({ origin: otherOrigin, principal })
).not.toBeDefined();

expect(await getAnchorByPrincipal({ origin, principal })).toBe(
BigInt(10000)
);
})
);

test(
"old anchors are dropped",
withStorage(async () => {
Expand All @@ -180,30 +231,43 @@ test(
);

test(
"unknown fields are not dropped",
withStorage(
async () => {
vi.useFakeTimers().setSystemTime(new Date(20));
await setAnchorUsed(BigInt(10000));
vi.useRealTimers();
},
{
indexeddb: {
before: {
anchors: {
"10000": { lastUsedTimestamp: 10, hello: "world" },
},
},
after: {
anchors: {
"10000": { lastUsedTimestamp: 20, hello: "world" },
},
},
},
"old principals are dropped",
withStorage(async () => {
const userNumber = BigInt(10000);
const principal = Principal.fromText("2vxsx-fae");
const oldOrigin = "https://old.com";
const veryOldOrigin = "https://very.old.com";
vi.useFakeTimers().setSystemTime(new Date(0));
await setKnownPrincipal({ userNumber, principal, origin: veryOldOrigin });
vi.useFakeTimers().setSystemTime(new Date(1));
await setKnownPrincipal({ userNumber, principal, origin: oldOrigin });
let date = 2;
vi.useFakeTimers().setSystemTime(new Date(date));
for (let i = 0; i < MAX_SAVED_PRINCIPALS; i++) {
date++;
vi.useFakeTimers().setSystemTime(new Date(date));
await setKnownPrincipal({
userNumber,
principal,
origin: `https://new${i}.com`,
});
}
)
date++;
vi.useFakeTimers().setSystemTime(new Date(date));
const newOrigin = "https://new.com";
await setKnownPrincipal({ userNumber, principal, origin: newOrigin });
expect(
await getAnchorByPrincipal({ principal, origin: veryOldOrigin })
).not.toBeDefined();
expect(
await getAnchorByPrincipal({ principal, origin: oldOrigin })
).not.toBeDefined();
expect(
await getAnchorByPrincipal({ principal, origin: newOrigin })
).toBeDefined();
vi.useRealTimers();
})
);

/** Test storage usage. Storage is cleared after the callback has returned.
* If `before` is specified, storage is populated with its content before the test is run.
* If `after` is specified, the content of storage are checked against `after` after the
Expand Down
Loading

0 comments on commit e80c5b6

Please sign in to comment.