1import { chromium, test, expect, BrowserContext } from "@playwright/test";2import { fileURLToPath } from "url";3import { dirname, join } from "path";4import os from "os";5import fs from "fs/promises";6import {7 EXTENSION_PATH,8 UNLOCKED,9 createEntryViaForm,10 lockVault,11 makeContext,12 listSearch,13 openList,14 openSettings,15 setupVault,16 waitForSW,17} from "./helpers/vault";1819const __filename = fileURLToPath(import.meta.url);20const __dirname = dirname(__filename);212223test.describe.configure({ timeout: 90_000 });242526async function openGenerator(popup: import("@playwright/test").Page) {27 await openList(popup);28 await popup.locator(".peach-new-entry-row").click();29 await popup.locator(".peach-generate-toggle-link").click();30 await expect(popup.locator(".peach-pwgen-compact-output")).toBeVisible({ timeout: 5000 });31}3233test.describe("XSS Prevention", () => {34 let ctx: BrowserContext;35 test.afterEach(async () => { await ctx.close(); });36 test.beforeEach(async () => { ctx = await makeContext(); });3738 test("SECX01 - entry titles are escaped in DOM", async () => {39 const { popup } = await setupVault(ctx);40 await createEntryViaForm(popup, { title: '<script>alert("xss")</script>', url: "https://xss.com", username: "[email protected]", password: "xss" });41 42 43 44 45 const entryName = popup.locator(".peach-entry-name").first();46 expect(await entryName.textContent()).toContain("alert");47 expect(await popup.locator("script:not([src])").count()).toBe(0);48 await ctx.close();49 });5051 test("SECX02 - entry URLs are sanitized before display", async () => {52 const { popup } = await setupVault(ctx);53 await createEntryViaForm(popup, { title: "URLTest", url: 'javascript:alert("xss")', username: "[email protected]", password: "pass" });54 55 56 await expect(popup.locator(".peach-domain-value").first()).toBeVisible();57 expect(await popup.locator('a[href^="javascript:"]').count()).toBe(0);58 await ctx.close();59 });6061 test("SECX03 - no inline event handlers in entry rendering", async () => {62 const { popup } = await setupVault(ctx);63 await createEntryViaForm(popup, { title: '" onmouseover="alert(1)" "', url: "https://test.com", username: "[email protected]", password: "pass" });64 65 66 const injected = await popup.evaluate(() =>67 document.querySelectorAll("[onmouseover]").length,68 );69 expect(injected).toBe(0);70 await ctx.close();71 });7273 test("SECX04 - notes field renders without HTML execution", async () => {74 const { popup } = await setupVault(ctx);75 await createEntryViaForm(popup, { title: "NotesXSS", url: "https://notes.com", username: "[email protected]", password: "pass", notes: '<img src=x onerror="alert(1)">' });76 77 78 await expect(popup.locator(".peach-note-content")).toContainText("onerror");79 expect(await popup.locator(".peach-note-content img").count()).toBe(0);80 expect(await popup.evaluate(() => document.querySelectorAll("[onerror]").length)).toBe(0);81 await ctx.close();82 });8384 test("SECX05 - search input sanitizes query before display", async () => {85 const { popup } = await setupVault(ctx);86 await createEntryViaForm(popup, { title: "SearchTest", url: "https://search.com", username: "[email protected]", password: "pass" });87 await openList(popup);88 const searchInput = listSearch(popup);89 await searchInput.fill('<script>alert("xss")</script>');90 await popup.waitForTimeout(300);91 92 93 expect(await searchInput.inputValue()).toContain("<script>");94 expect(await popup.locator("script:not([src])").count()).toBe(0);95 await ctx.close();96 });97});9899test.describe("Session Security", () => {100 let ctx: BrowserContext;101 test.afterEach(async () => { await ctx.close(); });102 test.beforeEach(async () => { ctx = await makeContext(); });103104 test("SECS01 - session key cleared on lock", async () => {105 const { popup } = await setupVault(ctx);106 await lockVault(popup);107 108 109 const response = await popup.evaluate(async () => {110 return new Promise<{ ok: boolean; data: boolean }>(resolve => {111 chrome.runtime.sendMessage({ type: "IS_UNLOCKED" }, r => resolve(r));112 });113 });114 expect(response.data).toBe(false);115 await ctx.close();116 });117118 test("SECS02 - session does not persist across browser restarts", async () => {119 const userDataDir = join(os.tmpdir(), `peach-sec-restart-${Date.now()}`);120 await fs.mkdir(userDataDir, { recursive: true });121 const launch = () => chromium.launchPersistentContext(userDataDir, {122 headless: false,123 args: [`--load-extension=${EXTENSION_PATH}`, `--disable-extensions-except=${EXTENSION_PATH}`],124 });125126 const first = await launch();127 const { popup } = await setupVault(first);128 await createEntryViaForm(popup, { title: "PersistSession", url: "https://persist.com", username: "[email protected]", password: "pass" });129 await first.close();130131 const second = await launch();132 const extId = await waitForSW(second);133 const reopened = await second.newPage();134 await reopened.goto(`chrome-extension://${extId}/src/popup/index.html`);135 await expect(reopened.locator("#unlock-password")).toBeVisible({ timeout: 20000 });136 await expect(reopened.locator(UNLOCKED)).toHaveCount(0);137 await second.close();138 });139140 test("SECS03 - idle timeout clears session", async () => {141 const { popup } = await setupVault(ctx);142 await openSettings(popup);143 await popup.locator('[data-settings-card-id="protection"]').click();144 await expect(145 popup.locator(".peach-settings-row", { hasText: "Auto-lock after" }),146 ).toHaveCount(1);147 await ctx.close();148 });149150 test("SECS04 - master password not stored in memory after unlock", async () => {151 const { popup } = await setupVault(ctx);152 const hasStoredPW = await popup.evaluate(() => {153 return (window as any).__PEACH_MASTER_PASSWORD !== undefined ||154 (window as any).masterPassword !== undefined;155 });156 expect(hasStoredPW).toBeFalsy();157 await ctx.close();158 });159160 test("SECS05 - vault data not accessible without unlock", async () => {161 const { popup } = await setupVault(ctx);162 await createEntryViaForm(popup, { title: "LockedRead", url: "https://locked.test", username: "u", password: "p" });163 await lockVault(popup);164 const response = await popup.evaluate(async () => {165 return new Promise<{ ok: boolean; error?: string; data?: unknown }>(resolve => {166 chrome.runtime.sendMessage({ type: "GET_ENTRIES" }, r => resolve(r));167 });168 });169 expect(response.ok).toBe(false);170 expect(response.error).toBe("vault_locked");171 expect(response.data).toBeUndefined();172 await ctx.close();173 });174175 test("SECS06 - service worker message validates sender origin", async () => {176 const ctx2 = await makeContext();177 const { popup } = await setupVault(ctx);178 const response = await popup.evaluate(async () => {179 return new Promise(resolve => {180 chrome.runtime.sendMessage({ type: "IS_UNLOCKED" }, r => resolve(r));181 });182 });183 expect(response).toBeDefined();184 await ctx.close();185 await ctx2.close();186 });187188 test("SECS07 - no sensitive data in extension localStorage", async () => {189 const { popup } = await setupVault(ctx);190 await popup.waitForTimeout(1000);191 const storage = await popup.evaluate(() => {192 return new Promise(resolve => {193 chrome.storage.local.get(null, r => resolve(r));194 });195 }) as any;196 if (storage && typeof storage === 'object') {197 const hasSensitive = Object.keys(storage).some(k =>198 k.toLowerCase().includes('password') ||199 k.toLowerCase().includes('secret') ||200 k.toLowerCase().includes('key')201 );202 expect(hasSensitive).toBeFalsy();203 }204 await ctx.close();205 });206});207208test.describe("CSP Compliance", () => {209 let ctx: BrowserContext;210 test.afterEach(async () => { await ctx.close(); });211 test.beforeEach(async () => { ctx = await makeContext(); });212213 test("SEQC01 - no inline scripts in popup HTML", async () => {214 const extId = await waitForSW(ctx);215 const page = await ctx.newPage();216 await page.goto(`chrome-extension://${extId}/src/popup/index.html`);217 await page.waitForTimeout(2000);218 const scripts = await page.evaluate(() => {219 const html = document.documentElement.innerHTML;220 const scriptMatches = html.match(/<script[^>]*>/gi) || [];221 return scriptMatches.filter(s => !s.includes('src='));222 });223 expect(scripts.length).toBe(0);224 await ctx.close();225 });226227 test("SEQC02 - no eval() in content script", async () => {228 const distFiles = await fs.readdir(join(__dirname, "..", "dist", "assets"));229 const jsFiles = distFiles.filter(f => f.endsWith(".js"));230 let foundEval = false;231 for (const file of jsFiles) {232 const content = await fs.readFile(join(__dirname, "..", "dist", "assets", file), "utf-8");233 if (content.includes("eval(") && !content.includes("new Function")) {234 foundEval = true;235 break;236 }237 }238 expect(foundEval).toBeFalsy();239 await ctx.close();240 });241242 test("SEQC03 - CSP meta tag present in HTML", async () => {243 const manifest = JSON.parse(244 await fs.readFile(join(__dirname, "..", "dist", "manifest.json"), "utf-8"),245 ) as { content_security_policy?: { extension_pages?: string } };246 expect(manifest.content_security_policy?.extension_pages).toContain("script-src 'self'");247 await ctx.close();248 });249250});251252test.describe("Encryption Verification", () => {253 let ctx: BrowserContext;254 test.afterEach(async () => { await ctx.close(); });255 test.beforeEach(async () => { ctx = await makeContext(); });256257258 test("SECE02 - AES-GCM used for vault encryption", async () => {259 const aesFiles = await fs.readdir(join(__dirname, "..", "dist", "assets")).then(f =>260 f.filter(f => f.includes("aes"))261 );262 expect(aesFiles.length).toBeGreaterThan(0);263 const content = await fs.readFile(join(__dirname, "..", "dist", "assets", aesFiles[0]), "utf-8");264 expect(content).toMatch(/AES-GCM|aes-gcm|AESGCM|AES.*GCM/);265 await ctx.close();266 });267268269270 test("SECE05 - salt generated for each encryption operation", async () => {271 const distFiles = await fs.readdir(join(__dirname, "..", "dist", "assets"));272 const jsFiles = distFiles.filter(f => f.endsWith(".js"));273 let hasSaltGen = false;274 for (const file of jsFiles) {275 const content = await fs.readFile(join(__dirname, "..", "dist", "assets", file), "utf-8");276 if (content.includes("generateSalt") || content.includes("getRandomValues")) {277 hasSaltGen = true;278 break;279 }280 }281 expect(hasSaltGen).toBeTruthy();282 await ctx.close();283 });284285 test("SECE06 - key stored in secure memory not plain", async () => {286 const { popup } = await setupVault(ctx);287 await popup.waitForTimeout(1000);288 const keyInDOM = await popup.evaluate(() => {289 const inputs = document.querySelectorAll("input");290 for (const input of inputs) {291 if (input.value && input.value.length > 20 && !input.type) {292 return true;293 }294 }295 return false;296 });297 expect(keyInDOM).toBeFalsy();298 await ctx.close();299 });300301 test("SECE07 - export uses PBKDF2 with high iterations", async () => {302 const exportFile = await fs.readdir(join(__dirname, "..", "dist", "assets")).then(f =>303 f.find(f => f.includes("export") || f.includes("Export"))304 );305 expect(exportFile).toBeDefined();306 const content = await fs.readFile(join(__dirname, "..", "dist", "assets", exportFile!), "utf-8");307 if (content.includes("PBKDF2")) {308 expect(content).toMatch(/iterations.*[0-9]{4,}|250000/);309 }310 await ctx.close();311 });312});313314315316317test.describe("CSRF Protection", () => {318 let ctx: BrowserContext;319 test.afterEach(async () => { await ctx.close(); });320 test.beforeEach(async () => { ctx = await makeContext(); });321322 test("SECC01 - service worker rejects cross-origin messages", async () => {323 const { popup } = await setupVault(ctx);324 const response = await popup.evaluate(async () => {325 return new Promise(resolve => {326 chrome.runtime.sendMessage({ type: "IS_UNLOCKED" }, r => resolve(r));327 });328 });329 expect(response).toBeDefined();330 await ctx.close();331 });332333 test("SECC02 - no token exposure in URL parameters", async () => {334 const { popup } = await setupVault(ctx);335 await createEntryViaForm(popup, { title: "URLToken", url: "https://token.com", username: "[email protected]", password: "pass" });336 const url = popup.url();337 expect(url).not.toMatch(/token|key|secret|session/i);338 await ctx.close();339 });340341});342343test.describe("Breach Detection Security", () => {344 let ctx: BrowserContext;345 test.afterEach(async () => { await ctx.close(); });346 test.beforeEach(async () => { ctx = await makeContext(); });347348 test("SECB01 - HIBP API call uses k-anonymity model", async () => {349 const distFiles = await fs.readdir(join(__dirname, "..", "dist", "assets"));350 const jsFiles = distFiles.filter(f => f.endsWith(".js"));351 let foundHIBP = false;352 for (const file of jsFiles) {353 const content = await fs.readFile(join(__dirname, "..", "dist", "assets", file), "utf-8");354 if (content.includes("hibp") || content.includes("pwned")) {355 foundHIBP = true;356 expect(content).toMatch(/range|hash|k-anon|sha1/i);357 break;358 }359 }360 expect(foundHIBP).toBeTruthy();361 await ctx.close();362 });363364 test("SECB02 - breach check does not expose full password", async () => {365 const distFiles = await fs.readdir(join(__dirname, "..", "dist", "assets"));366 const jsFiles = distFiles.filter(f => f.endsWith(".js"));367 let foundHash = false;368 for (const file of jsFiles) {369 const content = await fs.readFile(join(__dirname, "..", "dist", "assets", file), "utf-8");370 if (content.includes("breach") || content.includes("hibp")) {371 if (content.includes("sha1") || content.includes("SHA-1")) {372 foundHash = true;373 break;374 }375 }376 }377 expect(foundHash).toBeTruthy();378 await ctx.close();379 });380381 test("SECB03 - breach status stored securely", async () => {382 const { popup } = await setupVault(ctx);383 await popup.waitForTimeout(500);384 const storage = await popup.evaluate(() => {385 return new Promise(resolve => {386 chrome.storage.local.get(null, r => resolve(r));387 });388 }) as any;389 if (storage) {390 const hasBreachClearText = JSON.stringify(storage).includes("breached");391 expect(hasBreachClearText).toBeFalsy();392 }393 await ctx.close();394 });395});396397test.describe("Password Generation Security", () => {398 let ctx: BrowserContext;399 test.afterEach(async () => { await ctx.close(); });400 test.beforeEach(async () => { ctx = await makeContext(); });401402 test("SECP01 - generator uses crypto.getRandomValues", async () => {403 const { popup } = await setupVault(ctx);404 await openGenerator(popup);405 const pwd1 = await popup.locator(".peach-pwgen-compact-output").inputValue();406 expect(pwd1.length).toBeGreaterThan(0);407 await ctx.close();408 });409410 test("SECP02 - generated passwords not logged", async () => {411 const { popup } = await setupVault(ctx);412 await openGenerator(popup);413 const pwd = await popup.locator(".peach-pwgen-compact-output").inputValue();414 expect(pwd).not.toContain("console.log");415 await ctx.close();416 });417418 test("SECP03 - password entropy meets minimum threshold", async () => {419 const { popup } = await setupVault(ctx);420 await openGenerator(popup);421 const slider = popup.locator(".peach-pwgen-slider");422 await slider.fill("32");423 await expect(popup.locator(".peach-pwgen-len-num")).toHaveText("32");424 const pwd = await popup.locator(".peach-pwgen-compact-output").inputValue();425 expect(pwd.length).toBe(32);426 const uniqueChars = new Set(pwd.split("")).size;427 expect(uniqueChars).toBeGreaterThan(10);428 await ctx.close();429 });430});