1import { 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 { chromium } = await import("playwright");120 const userDataDir = join(os.tmpdir(), `peach-sec-restart-${Date.now()}`);121 await fs.mkdir(userDataDir, { recursive: true });122 const launch = () => chromium.launchPersistentContext(userDataDir, {123 headless: false,124 args: [`--load-extension=${EXTENSION_PATH}`, `--disable-extensions-except=${EXTENSION_PATH}`],125 });126127 const first = await launch();128 const { popup } = await setupVault(first);129 await createEntryViaForm(popup, { title: "PersistSession", url: "https://persist.com", username: "[email protected]", password: "pass" });130 await first.close();131132 const second = await launch();133 const extId = await waitForSW(second);134 const reopened = await second.newPage();135 await reopened.goto(`chrome-extension://${extId}/src/popup/index.html`);136 await expect(reopened.locator("#unlock-password")).toBeVisible({ timeout: 20000 });137 await expect(reopened.locator(UNLOCKED)).toHaveCount(0);138 await second.close();139 });140141 test("SECS03 - idle timeout clears session", async () => {142 const { popup } = await setupVault(ctx);143 await openSettings(popup);144 await popup.locator('[data-settings-card-id="protection"]').click();145 await expect(146 popup.locator(".peach-settings-row", { hasText: "Auto-lock after" }),147 ).toHaveCount(1);148 await ctx.close();149 });150151 test("SECS04 - master password not stored in memory after unlock", async () => {152 const { popup } = await setupVault(ctx);153 const hasStoredPW = await popup.evaluate(() => {154 return (window as any).__PEACH_MASTER_PASSWORD !== undefined ||155 (window as any).masterPassword !== undefined;156 });157 expect(hasStoredPW).toBeFalsy();158 await ctx.close();159 });160161 test("SECS05 - vault data not accessible without unlock", async () => {162 const { popup } = await setupVault(ctx);163 await createEntryViaForm(popup, { title: "LockedRead", url: "https://locked.test", username: "u", password: "p" });164 await lockVault(popup);165 const response = await popup.evaluate(async () => {166 return new Promise<{ ok: boolean; error?: string; data?: unknown }>(resolve => {167 chrome.runtime.sendMessage({ type: "GET_ENTRIES" }, r => resolve(r));168 });169 });170 expect(response.ok).toBe(false);171 expect(response.error).toBe("vault_locked");172 expect(response.data).toBeUndefined();173 await ctx.close();174 });175176 test("SECS06 - service worker message validates sender origin", async () => {177 const ctx2 = await makeContext();178 const { popup } = await setupVault(ctx);179 const response = await popup.evaluate(async () => {180 return new Promise(resolve => {181 chrome.runtime.sendMessage({ type: "IS_UNLOCKED" }, r => resolve(r));182 });183 });184 expect(response).toBeDefined();185 await ctx.close();186 await ctx2.close();187 });188189 test("SECS07 - no sensitive data in extension localStorage", async () => {190 const { popup } = await setupVault(ctx);191 await popup.waitForTimeout(1000);192 const storage = await popup.evaluate(() => {193 return new Promise(resolve => {194 chrome.storage.local.get(null, r => resolve(r));195 });196 }) as any;197 if (storage && typeof storage === 'object') {198 const hasSensitive = Object.keys(storage).some(k =>199 k.toLowerCase().includes('password') ||200 k.toLowerCase().includes('secret') ||201 k.toLowerCase().includes('key')202 );203 expect(hasSensitive).toBeFalsy();204 }205 await ctx.close();206 });207});208209test.describe("CSP Compliance", () => {210 let ctx: BrowserContext;211 test.afterEach(async () => { await ctx.close(); });212 test.beforeEach(async () => { ctx = await makeContext(); });213214 test("SEQC01 - no inline scripts in popup HTML", async () => {215 const extId = await waitForSW(ctx);216 const page = await ctx.newPage();217 await page.goto(`chrome-extension://${extId}/src/popup/index.html`);218 await page.waitForTimeout(2000);219 const scripts = await page.evaluate(() => {220 const html = document.documentElement.innerHTML;221 const scriptMatches = html.match(/<script[^>]*>/gi) || [];222 return scriptMatches.filter(s => !s.includes('src='));223 });224 expect(scripts.length).toBe(0);225 await ctx.close();226 });227228 test("SEQC02 - no eval() in content script", async () => {229 const distFiles = await fs.readdir(join(__dirname, "..", "dist", "assets"));230 const jsFiles = distFiles.filter(f => f.endsWith(".js"));231 let foundEval = false;232 for (const file of jsFiles) {233 const content = await fs.readFile(join(__dirname, "..", "dist", "assets", file), "utf-8");234 if (content.includes("eval(") && !content.includes("new Function")) {235 foundEval = true;236 break;237 }238 }239 expect(foundEval).toBeFalsy();240 await ctx.close();241 });242243 test("SEQC03 - CSP meta tag present in HTML", async () => {244 const manifest = JSON.parse(245 await fs.readFile(join(__dirname, "..", "dist", "manifest.json"), "utf-8"),246 ) as { content_security_policy?: { extension_pages?: string } };247 expect(manifest.content_security_policy?.extension_pages).toContain("script-src 'self'");248 await ctx.close();249 });250251});252253test.describe("Encryption Verification", () => {254 let ctx: BrowserContext;255 test.afterEach(async () => { await ctx.close(); });256 test.beforeEach(async () => { ctx = await makeContext(); });257258259 test("SECE02 - AES-GCM used for vault encryption", async () => {260 const aesFiles = await fs.readdir(join(__dirname, "..", "dist", "assets")).then(f =>261 f.filter(f => f.includes("aes"))262 );263 expect(aesFiles.length).toBeGreaterThan(0);264 const content = await fs.readFile(join(__dirname, "..", "dist", "assets", aesFiles[0]), "utf-8");265 expect(content).toMatch(/AES-GCM|aes-gcm|AESGCM|AES.*GCM/);266 await ctx.close();267 });268269270271 test("SECE05 - salt generated for each encryption operation", async () => {272 const distFiles = await fs.readdir(join(__dirname, "..", "dist", "assets"));273 const jsFiles = distFiles.filter(f => f.endsWith(".js"));274 let hasSaltGen = false;275 for (const file of jsFiles) {276 const content = await fs.readFile(join(__dirname, "..", "dist", "assets", file), "utf-8");277 if (content.includes("generateSalt") || content.includes("getRandomValues")) {278 hasSaltGen = true;279 break;280 }281 }282 expect(hasSaltGen).toBeTruthy();283 await ctx.close();284 });285286 test("SECE06 - key stored in secure memory not plain", async () => {287 const { popup } = await setupVault(ctx);288 await popup.waitForTimeout(1000);289 const keyInDOM = await popup.evaluate(() => {290 const inputs = document.querySelectorAll("input");291 for (const input of inputs) {292 if (input.value && input.value.length > 20 && !input.type) {293 return true;294 }295 }296 return false;297 });298 expect(keyInDOM).toBeFalsy();299 await ctx.close();300 });301302 test("SECE07 - export uses PBKDF2 with high iterations", async () => {303 const exportFile = await fs.readdir(join(__dirname, "..", "dist", "assets")).then(f =>304 f.find(f => f.includes("export") || f.includes("Export"))305 );306 expect(exportFile).toBeDefined();307 const content = await fs.readFile(join(__dirname, "..", "dist", "assets", exportFile!), "utf-8");308 if (content.includes("PBKDF2")) {309 expect(content).toMatch(/iterations.*[0-9]{4,}|250000/);310 }311 await ctx.close();312 });313});314315316317318test.describe("CSRF Protection", () => {319 let ctx: BrowserContext;320 test.afterEach(async () => { await ctx.close(); });321 test.beforeEach(async () => { ctx = await makeContext(); });322323 test("SECC01 - service worker rejects cross-origin messages", async () => {324 const { popup } = await setupVault(ctx);325 const response = await popup.evaluate(async () => {326 return new Promise(resolve => {327 chrome.runtime.sendMessage({ type: "IS_UNLOCKED" }, r => resolve(r));328 });329 });330 expect(response).toBeDefined();331 await ctx.close();332 });333334 test("SECC02 - no token exposure in URL parameters", async () => {335 const { popup } = await setupVault(ctx);336 await createEntryViaForm(popup, { title: "URLToken", url: "https://token.com", username: "[email protected]", password: "pass" });337 const url = popup.url();338 expect(url).not.toMatch(/token|key|secret|session/i);339 await ctx.close();340 });341342});343344test.describe("Breach Detection Security", () => {345 let ctx: BrowserContext;346 test.afterEach(async () => { await ctx.close(); });347 test.beforeEach(async () => { ctx = await makeContext(); });348349 test("SECB01 - HIBP API call uses k-anonymity model", async () => {350 const distFiles = await fs.readdir(join(__dirname, "..", "dist", "assets"));351 const jsFiles = distFiles.filter(f => f.endsWith(".js"));352 let foundHIBP = false;353 for (const file of jsFiles) {354 const content = await fs.readFile(join(__dirname, "..", "dist", "assets", file), "utf-8");355 if (content.includes("hibp") || content.includes("pwned")) {356 foundHIBP = true;357 expect(content).toMatch(/range|hash|k-anon|sha1/i);358 break;359 }360 }361 expect(foundHIBP).toBeTruthy();362 await ctx.close();363 });364365 test("SECB02 - breach check does not expose full password", async () => {366 const distFiles = await fs.readdir(join(__dirname, "..", "dist", "assets"));367 const jsFiles = distFiles.filter(f => f.endsWith(".js"));368 let foundHash = false;369 for (const file of jsFiles) {370 const content = await fs.readFile(join(__dirname, "..", "dist", "assets", file), "utf-8");371 if (content.includes("breach") || content.includes("hibp")) {372 if (content.includes("sha1") || content.includes("SHA-1")) {373 foundHash = true;374 break;375 }376 }377 }378 expect(foundHash).toBeTruthy();379 await ctx.close();380 });381382 test("SECB03 - breach status stored securely", async () => {383 const { popup } = await setupVault(ctx);384 await popup.waitForTimeout(500);385 const storage = await popup.evaluate(() => {386 return new Promise(resolve => {387 chrome.storage.local.get(null, r => resolve(r));388 });389 }) as any;390 if (storage) {391 const hasBreachClearText = JSON.stringify(storage).includes("breached");392 expect(hasBreachClearText).toBeFalsy();393 }394 await ctx.close();395 });396});397398test.describe("Password Generation Security", () => {399 let ctx: BrowserContext;400 test.afterEach(async () => { await ctx.close(); });401 test.beforeEach(async () => { ctx = await makeContext(); });402403 test("SECP01 - generator uses crypto.getRandomValues", async () => {404 const { popup } = await setupVault(ctx);405 await openGenerator(popup);406 const pwd1 = await popup.locator(".peach-pwgen-compact-output").inputValue();407 expect(pwd1.length).toBeGreaterThan(0);408 await ctx.close();409 });410411 test("SECP02 - generated passwords not logged", async () => {412 const { popup } = await setupVault(ctx);413 await openGenerator(popup);414 const pwd = await popup.locator(".peach-pwgen-compact-output").inputValue();415 expect(pwd).not.toContain("console.log");416 await ctx.close();417 });418419 test("SECP03 - password entropy meets minimum threshold", async () => {420 const { popup } = await setupVault(ctx);421 await openGenerator(popup);422 const slider = popup.locator(".peach-pwgen-slider");423 await slider.fill("32");424 await expect(popup.locator(".peach-pwgen-len-num")).toHaveText("32");425 const pwd = await popup.locator(".peach-pwgen-compact-output").inputValue();426 expect(pwd.length).toBe(32);427 const uniqueChars = new Set(pwd.split("")).size;428 expect(uniqueChars).toBeGreaterThan(10);429 await ctx.close();430 });431});