-
Notifications
You must be signed in to change notification settings - Fork 195
feat(fe): hold a browser key and rotate it on every sign-in #4271
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
Merged
+709
−0
Merged
Changes from 12 commits
Commits
Show all changes
111 commits
Select commit
Hold shift + click to select a range
f7b8adf
feat(fe): hold a browser key and rotate it on every sign-in
sea-snake 9e63a30
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 982a103
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 407c9b9
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 065e36a
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 1822da1
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 5224974
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 0fdc097
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake ff45af8
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake ac4883d
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake d8e484d
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake b68eeba
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 563d5ea
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 69c35d5
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake fdef0fe
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 2e58991
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 730d8f9
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 78afa45
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 074d2d5
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 01df69a
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 476f1b8
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake cc31ddd
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake c2ad3ee
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 9d8353f
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 4541fb1
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake b8cc134
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 4a79d30
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 918a3d7
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 5c222e6
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 579c407
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 661f7e8
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake e15eba1
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 17b53ff
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake ed28779
feat(fe): promote the announced successor when the canister calls a k…
sea-snake 6f843a2
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake f2621b3
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake d447926
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 71dedd1
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake fbd5299
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 44937f0
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake c0b1f4c
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 11350c8
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 24faebd
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake ef0697f
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 53ebdf6
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 57dc297
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 851e361
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake dd496a6
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake a5b7175
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 764b0ab
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 80c49db
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 32cd7c8
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 7be7d7d
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 325526b
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake d3f488e
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 177ba28
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake e510e1f
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 01ed1e3
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 72a15bf
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 8a0bd40
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 8cab998
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake d707470
refactor(fe): the browser key store says browser
sea-snake bc7becf
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake e3d5923
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 80504b5
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake c569a4d
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake c1abf60
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake dc17f14
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 0ab888b
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 2db3e99
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 7688631
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 5b8c473
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 0e86187
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 55ee698
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 41311fc
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 6476f67
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake e6ebdd1
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 0d5420e
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake a649fac
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 6bee296
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 1d23c40
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 741b812
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 591173e
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 0604a3c
chore: merge fe/describe-browser
sea-snake d2a9c35
chore: merge fe/describe-browser
sea-snake 3aecc8b
chore: merge fe/describe-browser
sea-snake ed43310
chore: merge fe/describe-browser
sea-snake f366042
chore: merge fe/describe-browser
sea-snake 2026c27
chore: merge fe/describe-browser
sea-snake d12543e
feat(frontend): a browser that no longer matches signs in as a new one
sea-snake eedeabc
chore: merge fe/describe-browser
sea-snake 9dd96d0
chore: merge fe/describe-browser
sea-snake 5fe7ee2
chore: merge fe/describe-browser
sea-snake 2e79018
test(frontend): fork against a brand that is still named
sea-snake be1ac87
chore: merge fe/describe-browser
sea-snake 6ec18f3
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 76519f3
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake abe58e1
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake edd5e61
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 9953678
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake c157b27
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake c4fe9c3
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 1befa72
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 5a5035f
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake b07e825
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake b9d5f7b
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake ee0a03f
refactor(browser-key): the store rotates, rather than asking to be to…
sea-snake b0e1a39
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 8728364
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 7771951
refactor(browser-key): name the successor the record holds
sea-snake 80c7e09
Merge branch 'fe/describe-browser' into fe/browser-key-store
sea-snake 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
Some comments aren't visible on the classic Files Changed page.
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,225 @@ | ||
| import "fake-indexeddb/auto"; | ||
| import { beforeEach, describe, expect, it } from "vitest"; | ||
| import { clear, createStore } from "idb-keyval"; | ||
| import { currentDeviceId, withBrowserProof } from "./browser-key.store"; | ||
|
|
||
| /// Names the same store the module under test writes to, so a test can wipe it. | ||
| const BROWSER_KEY_STORE = createStore("ii-browser-keys", "keys"); | ||
|
|
||
| const SIGNATURE_DOMAIN = new TextEncoder().encode("ii-session-device-key"); | ||
| const SUCCESSOR_SIGNATURE_DOMAIN = new TextEncoder().encode( | ||
| "ii-session-device-successor", | ||
| ); | ||
|
|
||
| const signedMessage = ( | ||
| domain: Uint8Array, | ||
| sessionKey: Uint8Array, | ||
| otherKey: Uint8Array, | ||
| ): Uint8Array => { | ||
| const message = new Uint8Array( | ||
| domain.length + sessionKey.length + otherKey.length, | ||
| ); | ||
| message.set(domain); | ||
| message.set(sessionKey, domain.length); | ||
| message.set(otherKey, domain.length + sessionKey.length); | ||
| return message; | ||
| }; | ||
|
|
||
| const verify = async ( | ||
| publicKey: Uint8Array, | ||
| signature: Uint8Array, | ||
| message: Uint8Array, | ||
| ): Promise<boolean> => { | ||
| const key = await crypto.subtle.importKey( | ||
| "spki", | ||
| new Uint8Array(publicKey), | ||
| { name: "ECDSA", namedCurve: "P-256" }, | ||
| true, | ||
| ["verify"], | ||
| ); | ||
| return crypto.subtle.verify( | ||
| { name: "ECDSA", hash: "SHA-256" }, | ||
| key, | ||
| new Uint8Array(signature), | ||
| new Uint8Array(message), | ||
| ); | ||
| }; | ||
|
|
||
| const sessionKey = (seed: number) => new Uint8Array(62).fill(seed); | ||
|
|
||
| const IDENTITY = BigInt(10_000); | ||
|
|
||
| /** Signs in and rotates, the way a successful ceremony does. */ | ||
| const signIn = (identityNumber: bigint, seed: number, deviceId = 1) => | ||
| withBrowserProof(identityNumber, sessionKey(seed), async (proof) => { | ||
| await proof.accept(deviceId); | ||
| return proof; | ||
| }); | ||
|
|
||
| /** Signs in without accepting, the way a call that fails or never returns leaves it. */ | ||
| const attempt = (identityNumber: bigint, seed: number) => | ||
| withBrowserProof(identityNumber, sessionKey(seed), (proof) => | ||
| Promise.resolve(proof), | ||
| ); | ||
|
|
||
| /** jsdom has no Web Locks, so this is what serialisation is tested against. */ | ||
| const stubLockApi = (): void => { | ||
| let tail: Promise<unknown> = Promise.resolve(); | ||
| Object.defineProperty(navigator, "locks", { | ||
| configurable: true, | ||
| value: { | ||
| request: (_name: string, run: () => Promise<unknown>) => { | ||
| const next = tail.then(run); | ||
| tail = next.then( | ||
| () => undefined, | ||
| () => undefined, | ||
| ); | ||
| return next; | ||
| }, | ||
| }, | ||
| }); | ||
| }; | ||
|
|
||
| const withoutLockApi = (): void => { | ||
| Object.defineProperty(navigator, "locks", { | ||
| configurable: true, | ||
| value: undefined, | ||
| }); | ||
| }; | ||
|
|
||
| describe("browser key", () => { | ||
| beforeEach(async () => { | ||
| await clear(BROWSER_KEY_STORE); | ||
| withoutLockApi(); | ||
| }); | ||
|
|
||
| it("signs the session key and the successor under the domain the canister verifies", async () => { | ||
| const key = sessionKey(1); | ||
|
|
||
| const proof = await attempt(IDENTITY, 1); | ||
|
|
||
| await expect( | ||
| verify( | ||
| proof.publicKey, | ||
| proof.signature, | ||
| signedMessage(SIGNATURE_DOMAIN, key, proof.nextPublicKey), | ||
| ), | ||
| ).resolves.toBe(true); | ||
| }); | ||
|
|
||
| it("does not sign the session key alone", async () => { | ||
| const proof = await attempt(IDENTITY, 1); | ||
|
|
||
| await expect( | ||
| verify(proof.publicKey, proof.signature, sessionKey(1)), | ||
| ).resolves.toBe(false); | ||
| }); | ||
|
|
||
| it("announces a successor it does not yet use", async () => { | ||
| const proof = await attempt(IDENTITY, 1); | ||
|
|
||
| expect(proof.nextPublicKey).not.toEqual(proof.publicKey); | ||
| }); | ||
|
|
||
| it("rotates to the successor once a sign-in is accepted", async () => { | ||
| const first = await signIn(IDENTITY, 1); | ||
|
|
||
| const second = await attempt(IDENTITY, 2); | ||
|
|
||
| expect(second.publicKey).toEqual(first.nextPublicKey); | ||
| }); | ||
|
|
||
| it("keeps the current key when a sign-in is not accepted", async () => { | ||
| const first = await attempt(IDENTITY, 1); | ||
|
|
||
| const second = await attempt(IDENTITY, 2); | ||
|
|
||
| expect(second.publicKey).toEqual(first.publicKey); | ||
| expect(second.nextPublicKey).not.toEqual(first.nextPublicKey); | ||
| }); | ||
|
|
||
| it("holds a separate key per identity", async () => { | ||
| const first = await attempt(IDENTITY, 1); | ||
|
|
||
| const second = await attempt(BigInt(10_001), 1); | ||
|
|
||
| expect(second.publicKey).not.toEqual(first.publicKey); | ||
| }); | ||
|
|
||
| it("registers a fresh key once storage is cleared", async () => { | ||
| const before = await signIn(IDENTITY, 1); | ||
| await clear(BROWSER_KEY_STORE); | ||
|
|
||
| const after = await attempt(IDENTITY, 1); | ||
|
|
||
| expect(after.publicKey).not.toEqual(before.publicKey); | ||
| expect(after.publicKey).not.toEqual(before.nextPublicKey); | ||
| }); | ||
|
|
||
| it("serialises concurrent sign-ins, so the second builds on the first", async () => { | ||
| stubLockApi(); | ||
|
|
||
| const [first, second] = await Promise.all([ | ||
| signIn(IDENTITY, 1), | ||
| signIn(IDENTITY, 2), | ||
| ]); | ||
|
|
||
| expect(second.publicKey).toEqual(first.nextPublicKey); | ||
| }); | ||
|
|
||
| it("still signs in on a browser without the lock API", async () => { | ||
| const proof = await attempt(IDENTITY, 1); | ||
|
|
||
| expect(proof.publicKey.length).toBe(91); | ||
| }); | ||
|
|
||
| it("exports the keys in the encoding the canister parses", async () => { | ||
| const proof = await attempt(IDENTITY, 1); | ||
|
|
||
| expect(proof.publicKey.length).toBe(91); | ||
| expect(proof.nextPublicKey.length).toBe(91); | ||
| expect(proof.signature.length).toBe(64); | ||
| }); | ||
|
|
||
| it("remembers which browser the canister said this is", async () => { | ||
| await signIn(IDENTITY, 1, 7); | ||
|
|
||
| await expect(currentDeviceId(IDENTITY)).resolves.toBe(7); | ||
| }); | ||
|
|
||
| it("knows of no browser before a sign-in is accepted", async () => { | ||
| await attempt(IDENTITY, 1); | ||
|
|
||
| await expect(currentDeviceId(IDENTITY)).resolves.toBeUndefined(); | ||
| }); | ||
|
|
||
| it("has the successor sign for itself, so an unheld key cannot be announced", async () => { | ||
| const key = sessionKey(1); | ||
|
|
||
| const proof = await attempt(IDENTITY, 1); | ||
|
|
||
| await expect( | ||
| verify( | ||
| proof.nextPublicKey, | ||
| proof.nextSignature, | ||
| signedMessage(SUCCESSOR_SIGNATURE_DOMAIN, key, proof.publicKey), | ||
| ), | ||
| ).resolves.toBe(true); | ||
| }); | ||
|
|
||
| it("keeps the two signatures in their own roles", async () => { | ||
| const key = sessionKey(1); | ||
|
|
||
| const proof = await attempt(IDENTITY, 1); | ||
|
|
||
| // The successor's signature must not verify as the current key's, or one could be | ||
| // replayed as the other. | ||
| await expect( | ||
| verify( | ||
| proof.publicKey, | ||
| proof.nextSignature, | ||
| signedMessage(SIGNATURE_DOMAIN, key, proof.nextPublicKey), | ||
| ), | ||
| ).resolves.toBe(false); | ||
| }); | ||
| }); |
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,160 @@ | ||
| import { createStore, get as idbGet, set as idbSet } from "idb-keyval"; | ||
|
|
||
| /** | ||
| * The key this browser proves itself with when it creates a session, and the id the | ||
| * canister attributed it to. | ||
| * | ||
| * The key never leaves this origin: it appears in no delegation chain and in nothing an app | ||
| * receives, which is what lets it identify the browser without letting two apps recognise | ||
| * it. It is replaced at every sign-in, so a copy of it taken off disk stops working as soon | ||
| * as this browser signs in again. | ||
| */ | ||
| interface BrowserKeyRecord { | ||
| keyPair: CryptoKeyPair; | ||
| /** Absent until a sign-in has told us which browser we are. */ | ||
| deviceId?: number; | ||
| } | ||
|
|
||
| const BROWSER_KEY_STORE = createStore("ii-browser-keys", "keys"); | ||
|
|
||
| /** Must match the domains the canister verifies the two signatures under. */ | ||
| const SIGNATURE_DOMAIN = new TextEncoder().encode("ii-session-device-key"); | ||
| const SUCCESSOR_SIGNATURE_DOMAIN = new TextEncoder().encode( | ||
| "ii-session-device-successor", | ||
| ); | ||
|
|
||
| /** | ||
| * One key per identity, so nothing stored here links two of the user's identities to the | ||
| * same browser. | ||
| */ | ||
| const storageKey = (identityNumber: bigint): string => | ||
| identityNumber.toString(); | ||
|
|
||
| const generate = (): Promise<CryptoKeyPair> => | ||
| crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, false, [ | ||
| "sign", | ||
| "verify", | ||
| ]) as Promise<CryptoKeyPair>; | ||
|
|
||
| const read = async ( | ||
| identityNumber: bigint, | ||
| ): Promise<BrowserKeyRecord | undefined> => { | ||
| try { | ||
| return await idbGet<BrowserKeyRecord>( | ||
| storageKey(identityNumber), | ||
| BROWSER_KEY_STORE, | ||
| ); | ||
| } catch { | ||
| return undefined; | ||
| } | ||
| }; | ||
|
|
||
| const write = async ( | ||
|
sea-snake marked this conversation as resolved.
|
||
| identityNumber: bigint, | ||
| record: BrowserKeyRecord, | ||
| ): Promise<void> => { | ||
| try { | ||
| await idbSet(storageKey(identityNumber), record, BROWSER_KEY_STORE); | ||
| } catch { | ||
| // A browser that cannot keep its key signs in as a new one next time, which the | ||
| // identity sees as a new browser rather than as a failure. | ||
| } | ||
|
sea-snake marked this conversation as resolved.
|
||
| }; | ||
|
|
||
| const exported = (key: CryptoKey): Promise<Uint8Array> => | ||
| crypto.subtle.exportKey("spki", key).then((spki) => new Uint8Array(spki)); | ||
|
|
||
| const signed = async ( | ||
| key: CryptoKey, | ||
| domain: Uint8Array, | ||
| sessionKey: Uint8Array, | ||
| otherKey: Uint8Array, | ||
| ): Promise<Uint8Array> => { | ||
| const message = new Uint8Array( | ||
| domain.length + sessionKey.length + otherKey.length, | ||
| ); | ||
| message.set(domain); | ||
| message.set(sessionKey, domain.length); | ||
| message.set(otherKey, domain.length + sessionKey.length); | ||
| return new Uint8Array( | ||
| await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, key, message), | ||
| ); | ||
| }; | ||
|
|
||
| export interface BrowserProof { | ||
| publicKey: Uint8Array; | ||
| nextPublicKey: Uint8Array; | ||
| signature: Uint8Array; | ||
| /** By the successor itself, so a key the browser does not hold cannot be announced. */ | ||
| nextSignature: Uint8Array; | ||
| /** Rotates to the successor. Called once the canister has accepted the sign-in. */ | ||
| accept: (deviceId: number) => Promise<void>; | ||
|
sea-snake marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| /** Serialises sign-ins for one identity: two at once would leave us holding a key the | ||
| * canister never accepted, which reads as a different browser. */ | ||
| const exclusively = async <T>( | ||
| identityNumber: bigint, | ||
| run: () => Promise<T>, | ||
| ): Promise<T> => { | ||
| const locks = navigator.locks; | ||
| if (locks === undefined) { | ||
| return run(); | ||
| } | ||
| // Awaited, because `request` types its callback's return as the value it resolves to, | ||
| // so the promise `run` returns would otherwise nest. | ||
| return await locks.request(`ii-browser-key:${identityNumber}`, run); | ||
| }; | ||
|
|
||
| /** | ||
| * Proves possession of this browser's key and announces the successor it rotates to. | ||
| * | ||
| * The proof covers the session key, which is fresh for every session, so it is good for | ||
| * exactly one sign-in. `accept` is what advances this browser to the successor, and until | ||
| * it is called the current key stays in place — so a call that never comes back leaves both | ||
| * sides on the key the canister still holds. | ||
| */ | ||
| export const withBrowserProof = <T>( | ||
| identityNumber: bigint, | ||
| sessionKey: Uint8Array, | ||
| signIn: (proof: BrowserProof) => Promise<T>, | ||
| ): Promise<T> => | ||
| exclusively(identityNumber, async () => { | ||
| const stored = await read(identityNumber); | ||
| let keyPair = stored?.keyPair; | ||
| if (keyPair === undefined) { | ||
| // Kept before the call, not after: a first sign-in whose response is lost has still | ||
| // registered this key, and coming back with a different one would enrol us twice. | ||
| keyPair = await generate(); | ||
| await write(identityNumber, { keyPair }); | ||
| } | ||
| const successor = await generate(); | ||
| const [publicKey, nextPublicKey] = await Promise.all([ | ||
| exported(keyPair.publicKey), | ||
| exported(successor.publicKey), | ||
| ]); | ||
|
|
||
| const [signature, nextSignature] = await Promise.all([ | ||
| signed(keyPair.privateKey, SIGNATURE_DOMAIN, sessionKey, nextPublicKey), | ||
| signed( | ||
| successor.privateKey, | ||
| SUCCESSOR_SIGNATURE_DOMAIN, | ||
| sessionKey, | ||
| publicKey, | ||
| ), | ||
| ]); | ||
|
|
||
| return signIn({ | ||
| publicKey, | ||
| nextPublicKey, | ||
| signature, | ||
| nextSignature, | ||
| accept: (deviceId) => | ||
| write(identityNumber, { keyPair: successor, deviceId }), | ||
| }); | ||
| }); | ||
|
|
||
| /** Which browser the canister knows this one as, for the settings list to mark it. */ | ||
| export const currentDeviceId = async ( | ||
| identityNumber: bigint, | ||
| ): Promise<number | undefined> => (await read(identityNumber))?.deviceId; | ||
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.
Uh oh!
There was an error while loading. Please reload this page.