diff --git a/CHANGELOG.md b/CHANGELOG.md index 516fc58..666a029 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Security +- **Browser IPC hardening (SEC-01/02/03)** on the Named Pipe channel used by the browser + extension: + - **SEC-01** — removed the plaintext password fallback: an ECDH session is now mandatory and + the password is always AES-GCM encrypted; requests without a valid session are rejected with + `ecdh-session-required`. + - **SEC-02** — sessions are bound to the exact extension `clientId` that performed the ECDH + handshake, so a session cannot be reused by a different client even if its id leaks. + - **SEC-03** — sensitive password retrieval requires explicit user consent via a native dialog + ("Remember for this session", until the vault is locked). Consent and all ECDH sessions are + cleared on vault lock. + - Hardening: `unlock-vault` attempts over IPC are rate-limited (lockout after repeated + failures) and the number of concurrent ECDH sessions is capped. + +### Added +- **Browser extension**: autofill success toast ("Credentials filled on {site}") and a visible + connection-status indicator ("Connected"/"Locked") in the popup header (6 languages). + +### Fixed +- **Browser extension**: the popup no longer prompts for consent when it opens (consent applies + only to actual password retrieval); per-action request timeouts prevent spurious "PassKey is + not running" during slow operations such as consent or unlock. + ## [2.0.0] - 2026-06-04 ### Added diff --git a/extensions/chrome/background.js b/extensions/chrome/background.js index bb01206..facde3b 100644 --- a/extensions/chrome/background.js +++ b/extensions/chrome/background.js @@ -16,6 +16,16 @@ importScripts('lib/messages.js', 'lib/crypto.js'); const NATIVE_HOST = 'com.passkey.host'; const REQUEST_TIMEOUT_MS = 5000; +// Some actions wait on a human (SEC-03 consent dialog) or a slow KDF (unlock) on the Desktop +// side, so they need a longer timeout than the default request. +const TIMEOUT_OVERRIDES = { + 'get-credential-password': 120000, // may wait for the user's consent dialog + 'unlock-vault': 30000, // Argon2id KDF on the Desktop +}; +function timeoutForAction(action) { + return TIMEOUT_OVERRIDES[action] ?? REQUEST_TIMEOUT_MS; +} + // ─── State (lost on service worker restart — re-established automatically) ──── let port = null; @@ -90,7 +100,7 @@ function sendNativeMessage(message) { const timeoutId = setTimeout(() => { pendingRequests.delete(message.requestId); reject(new Error('timeout')); - }, REQUEST_TIMEOUT_MS); + }, timeoutForAction(message.action)); pendingRequests.set(message.requestId, { resolve, reject, timeoutId }); @@ -278,7 +288,7 @@ async function handleCopyCredential(credentialId) { // Desktop always returns an AES-GCM encrypted password tied to the ECDH session. // A missing nonce means no valid session — never accept a plaintext fallback. if (!resp.payload?.nonce || resp.payload.nonce.length === 0) { - return { success: false, error: 'no-session' }; + return { success: false, error: 'ecdh-session-required' }; } const password = await decryptPassword(sessionKey, resp.payload.nonce, resp.payload.encryptedPassword); return { success: true, password }; @@ -380,7 +390,7 @@ async function handleFillCredential(credentialId, username, tabId) { // Desktop always returns an AES-GCM encrypted password tied to the ECDH session. // A missing nonce means no valid session — never accept a plaintext fallback. if (!resp.payload?.nonce || resp.payload.nonce.length === 0) { - return { success: false, error: 'no-session' }; + return { success: false, error: 'ecdh-session-required' }; } const password = await decryptPassword( sessionKey, diff --git a/extensions/chrome/lib/i18n.js b/extensions/chrome/lib/i18n.js index cf492b6..74c62e1 100644 --- a/extensions/chrome/lib/i18n.js +++ b/extensions/chrome/lib/i18n.js @@ -54,6 +54,7 @@ const STRINGS = { // Feedback copied: 'Copiato!', copyError: 'Errore', + filledIn: (site) => `Credenziali inserite in ${site}`, // Footer openApp: 'Apri PassKey', }, @@ -86,6 +87,7 @@ const STRINGS = { hasPassword: 'Password saved', copied: 'Copied!', copyError: 'Error', + filledIn: (site) => `Credentials filled on ${site}`, openApp: 'Open PassKey', }, fr: { @@ -117,6 +119,7 @@ const STRINGS = { hasPassword: 'Mot de passe enregistré', copied: 'Copié !', copyError: 'Erreur', + filledIn: (site) => `Identifiants saisis sur ${site}`, openApp: 'Ouvrir PassKey', }, de: { @@ -148,6 +151,7 @@ const STRINGS = { hasPassword: 'Passwort gespeichert', copied: 'Kopiert!', copyError: 'Fehler', + filledIn: (site) => `Anmeldedaten in ${site} eingetragen`, openApp: 'PassKey öffnen', }, es: { @@ -179,6 +183,7 @@ const STRINGS = { hasPassword: 'Contraseña guardada', copied: '¡Copiado!', copyError: 'Error', + filledIn: (site) => `Credenciales rellenadas en ${site}`, openApp: 'Abrir PassKey', }, pt: { @@ -210,6 +215,7 @@ const STRINGS = { hasPassword: 'Senha guardada', copied: 'Copiado!', copyError: 'Erro', + filledIn: (site) => `Credenciais preenchidas em ${site}`, openApp: 'Abrir PassKey', }, }; diff --git a/extensions/chrome/popup/popup.css b/extensions/chrome/popup/popup.css index a74b94c..bc3c3c0 100644 --- a/extensions/chrome/popup/popup.css +++ b/extensions/chrome/popup/popup.css @@ -110,6 +110,13 @@ body { .pk-status.locked { background: var(--pk-warning); } .pk-status.disconnected { background: var(--pk-error); } +/* Status text (T6.3) — visible connection status next to the dot */ +.pk-status-text { + font-size: 11px; + color: var(--pk-text-2); + white-space: nowrap; +} + /* Domain badge */ .pk-domain { display: flex; @@ -460,3 +467,21 @@ body { to { transform: rotate(360deg); } } .pk-spin { animation: pk-spin 0.7s linear infinite; display: block; } + +/* ── Autofill success toast (T6.2) ──────────────────────────────────────── */ +.pk-toast { + position: fixed; + left: 50%; + bottom: 52px; + transform: translateX(-50%); + max-width: 320px; + padding: 8px 14px; + border-radius: 6px; + background: var(--pk-success); + color: #fff; + font-size: 12px; + font-weight: 500; + text-align: center; + box-shadow: 0 2px 8px rgba(0,0,0,0.25); + z-index: 10; +} diff --git a/extensions/chrome/popup/popup.html b/extensions/chrome/popup/popup.html index b85d287..47d1490 100644 --- a/extensions/chrome/popup/popup.html +++ b/extensions/chrome/popup/popup.html @@ -12,7 +12,8 @@
PassKey -
+ +
@@ -124,6 +125,9 @@

+ + +