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 @@