import { expect, test } from "@playwright/test"; import jsQR from "jsqr"; import { bytesToBase32 } from "../../src/crypto/encoding"; import { parseOtpAuth } from "../../src/otp/profile"; test("displays scannable imported TOTP and HOTP tokens on a dark mobile screen", async ({ page, }) => { await page.setViewportSize({ width: 390, height: 844 }); await page.emulateMedia({ colorScheme: "dark" }); const requests: string[] = []; page.on("request", (request) => requests.push(request.url())); await page.goto("/deep/nested/auth/"); await page.getByRole("button", { name: "Import & migration" }).click(); await page .getByPlaceholder("Paste migration data. It never leaves this tab.") .fill( "otpauth://totp/Ex%C3%A4mple:alice%40example.test?secret=JBSWY3DPEHPK3PXP&issuer=Ex%C3%A4mple&algorithm=SHA256&digits=8&period=45\n" + "otpauth://hotp/Other:bob?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&issuer=Other&counter=18446744073709551615", ); await page.getByRole("button", { name: "Inspect import" }).click(); for (const account of ["Exämple: alice@example.test", "Other: bob"]) { const trigger = page.getByRole("button", { name: `Show QR code for ${account}`, exact: true, }); await trigger.click(); const dialog = page.getByRole("dialog", { name: "OTP token QR code" }); await expect(dialog).toBeVisible(); const svg = dialog.getByRole("img", { name: "OTP provisioning QR code" }); await expect(svg).toBeVisible(); const bounds = await svg.boundingBox(); expect(bounds!.width).toBeGreaterThan(250); expect(bounds!.height).toBeCloseTo(bounds!.width, 0); expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(390); const pixels = await svg.evaluate(async (element) => { const source = new XMLSerializer().serializeToString(element); const url = URL.createObjectURL( new Blob([source], { type: "image/svg+xml" }), ); try { const image = new Image(); image.src = url; await image.decode(); const canvas = document.createElement("canvas"); canvas.width = canvas.height = 640; const context = canvas.getContext("2d")!; context.drawImage(image, 0, 0, 640, 640); return Array.from(context.getImageData(0, 0, 640, 640).data); } finally { URL.revokeObjectURL(url); } }); expect(pixels.slice(0, 4)).toEqual([255, 255, 255, 255]); const decoded = jsQR(new Uint8ClampedArray(pixels), 640, 640); expect(decoded).not.toBeNull(); const { profile } = parseOtpAuth(decoded!.data); if (account.startsWith("Exämple")) { expect(profile).toMatchObject({ kind: "totp", issuer: "Exämple", account: "alice@example.test", algorithm: "SHA-256", digits: 8, period: 45, }); expect(bytesToBase32(profile.secret)).toBe("JBSWY3DPEHPK3PXP"); } else { expect(profile).toMatchObject({ kind: "hotp", issuer: "Other", account: "bob", counter: 18446744073709551615n, }); expect(bytesToBase32(profile.secret)).toBe( "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", ); } await page.keyboard.press("Escape"); await expect(dialog).toHaveCount(0); await expect(trigger).toBeFocused(); } expect( requests.every((url) => new URL(url).origin === "http://127.0.0.1:4173"), ).toBe(true); }); test("runs from a nested path and keeps authentication material local", async ({ page, }) => { const requests: string[] = []; page.on("request", (request) => requests.push(request.url())); await page.goto("/deep/nested/auth/"); await expect(page.getByText("Sensitive session · memory only")).toBeVisible(); await expect( page.getByRole("heading", { name: "Credential parameters" }), ).toBeVisible(); await page.getByLabel("Issuer").fill("Example"); await page.getByLabel("Account").fill("alice@example.test"); await expect(page.getByText("TOTP code")).toBeVisible(); await page.getByRole("button", { name: "Show QR code", exact: true }).click(); await expect(page.locator(".qr svg")).toBeVisible(); await page.keyboard.press("Escape"); await expect(page.getByRole("dialog")).toHaveCount(0); expect( requests.every((url) => new URL(url).origin === "http://127.0.0.1:4173"), ).toBe(true); }); test("computes an RFC OCRA vector and decodes client data", async ({ page, }) => { await page.goto("/deep/nested/auth/"); await page.getByRole("button", { name: "OCRA challenge" }).click(); await page .getByLabel("Base32 shared secret") .fill("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"); await page.getByRole("button", { name: "Compute OCRA response" }).click(); await expect(page.getByText("237653")).toBeVisible(); await page.getByRole("button", { name: /WebAuthn \/ Passkeys/ }).click(); const client = btoa( JSON.stringify({ type: "webauthn.get", challenge: "YQ", origin: "https://example.test", }), ) .replaceAll("+", "-") .replaceAll("/", "_") .replace(/=+$/u, ""); await page.getByLabel("Encoded input").fill(client); await page.getByRole("button", { name: "Decode locally" }).click(); await expect(page.locator(".diagnostic-output")).toContainText( "webauthn.get", ); }); test("localhost development enables live credential creation", async ({ page, }) => { await page.goto("/deep/nested/auth/"); await page.getByRole("button", { name: /WebAuthn \/ Passkeys/ }).click(); await page.getByRole("button", { name: "Live ceremony" }).click(); await expect(page.getByText("Live enabled")).toBeVisible(); await expect( page.getByRole("button", { name: "Create test credential" }), ).toBeEnabled(); }); test("exposes the advanced OTP and WebAuthn laboratories", async ({ page }) => { await page.goto("/deep/nested/auth/"); await page.getByRole("button", { name: "Resync & rotation" }).click(); await expect( page.getByRole("heading", { name: "HOTP resynchronization" }), ).toBeVisible(); await expect( page.getByRole("heading", { name: "Collection comparison" }), ).toBeVisible(); await page.getByRole("button", { name: "Import & migration" }).click(); await expect( page.getByRole("button", { name: "Scan with camera" }), ).toBeVisible(); await expect( page.getByRole("button", { name: "Decrypt PSKC" }), ).toBeDisabled(); await page.getByRole("button", { name: /WebAuthn \/ Passkeys/ }).click(); await page.getByRole("button", { name: "Extensions & traces" }).click(); await expect( page.getByRole("heading", { name: "Extension configuration" }), ).toBeVisible(); await expect( page.getByRole("heading", { name: "Ceremony traces" }), ).toBeVisible(); await page.getByRole("button", { name: "Attestation verifier" }).click(); await expect( page.getByRole("heading", { name: "Trust root and historical policy" }), ).toBeVisible(); });