diff --git a/docs/privacy-policy.md b/docs/privacy-policy.md index 0b9a1b2..be04119 100644 --- a/docs/privacy-policy.md +++ b/docs/privacy-policy.md @@ -1,6 +1,6 @@ # PassKey Privacy Policy -**Last updated:** 2026-03-15 +**Last updated:** 2026-05-10 --- @@ -39,6 +39,7 @@ The extension requests only the minimum permissions required: |------------|---------| | `nativeMessaging` | Communicate with PassKey Desktop via Native Messaging | | `activeTab` | Read the current tab's URL to match credentials | +| `tabs` | Inject autofill into the active tab and keep the popup's tab reference current | --- diff --git a/docs/privacy/index.html b/docs/privacy/index.html new file mode 100644 index 0000000..b87d85c --- /dev/null +++ b/docs/privacy/index.html @@ -0,0 +1,343 @@ + + + + + + Privacy Policy — PassKey + + + + +
+ + PassKey +
+ +
+ +
Privacy Policy
+

Your data stays on your device. Always.

+

Last updated: May 10, 2026  ·  Applies to PassKey Desktop and Browser Extension

+ +
+ TL;DR +

PassKey never sends your data anywhere. No cloud, no servers, no analytics, no telemetry. + Everything stays encrypted on your computer.

+
+ + +
+

1. Overview

+

PassKey is a local-first password manager for Windows. The desktop application and its + browser extension store and manage your credentials exclusively on your device. + No account is required to use PassKey. No data is ever transmitted to any remote server + — by design, there is no remote server to transmit data to.

+

This policy describes what information PassKey reads or processes while running on + your computer and how that information is used.

+
+ + +
+

2. Data Storage

+ +
+ + +
+

3. Network Activity

+

PassKey makes zero outbound network connections. The only communication + that occurs is between the browser extension and the PassKey Desktop application on your + own computer, via the browser's Native Messaging API over a local Named Pipe:

+ +

There is no analytics, no telemetry, no crash reporting, no update checking, + and no advertising — not now, not ever.

+
+ + +
+

4. Browser Extension Permissions

+

The PassKey browser extension (available for Chrome, Edge, and Firefox) requests the + minimum permissions necessary to operate. Below is a complete list of what each + permission is used for:

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
PermissionWhy it is neededWhat it accesses
nativeMessagingCommunicate with PassKey Desktop via the browser's Native Messaging APILocal IPC channel to PassKey Desktop — no internet access
activeTabRead the URL of the current tab to find matching credentialsURL only — no page content, no cookies, no form data
tabsInject autofill into the active tab and keep the popup's tab reference currentActive tab ID and URL — no browsing history
+
+

The extension reads the URL of the tab you are currently + viewing solely to identify which saved credentials match the site. This URL is passed + to the local PassKey Desktop app for matching and is never stored by the extension or + sent anywhere else.

+
+ + +
+

5. Data Sharing

+

PassKey does not share any data with third parties. There are no third-party SDKs, + advertising networks, or analytics providers embedded in PassKey. There is no data + to share because no data leaves your device.

+
+ + +
+

6. Backups

+

Encrypted backups (.pkbak files) are stored locally at a location you + choose. Backups are independently encrypted with AES-256-GCM using an Argon2id-derived + key from a password you provide at backup time. PassKey does not offer or access any + cloud backup service.

+
+ + +
+

7. Open Source & Auditability

+

PassKey is open-source software licensed under the + GNU GPL v3. + The complete source code is publicly available. You can audit every line of code that + handles your data at + github.com/pexatar/PassKey.

+
+ + +
+

8. Changes to This Policy

+

If this policy is updated, the new version will be published at this URL with an + updated date at the top. Because PassKey collects no personal data, changes will + typically only reflect new features or clarifications to existing practices.

+
+ + +
+

9. Contact

+ +
+ +
+ + + + + diff --git a/extensions/chrome/background.js b/extensions/chrome/background.js index 8d6f28c..070985d 100644 --- a/extensions/chrome/background.js +++ b/extensions/chrome/background.js @@ -221,7 +221,7 @@ async function handleMessage(msg, sender) { async function handleGetStatus() { try { const req = buildRequest('get-status'); - const resp = await sendNativeMessage(req); + const resp = parseResponse(await sendNativeMessage(req)); return resp; } catch (err) { return { success: false, error: err.message || 'desktop-not-running' }; @@ -238,7 +238,7 @@ async function handleGetStatus() { async function handleGetCredentials(url) { try { const req = buildRequest('get-credentials', { url }); - const resp = await sendNativeMessage(req); + const resp = parseResponse(await sendNativeMessage(req)); return resp; } catch (err) { return { success: false, error: err.message || 'desktop-not-running' }; @@ -253,7 +253,7 @@ async function handleGetCredentials(url) { async function handleGetAllCredentials() { try { const req = buildRequest('get-all-credentials'); - const resp = await sendNativeMessage(req); + const resp = parseResponse(await sendNativeMessage(req)); return resp; } catch (err) { return { success: false, error: err.message || 'desktop-not-running' }; @@ -272,7 +272,7 @@ async function handleCopyCredential(credentialId) { try { await ensureSession(); const req = buildRequest('get-credential-password', { id: credentialId }); - const resp = await sendNativeMessage(req); + const resp = parseResponse(await sendNativeMessage(req)); if (!resp.success) return resp; let password; @@ -297,7 +297,7 @@ async function handleCopyCredential(credentialId) { async function handleUnlockVault(masterPassword) { try { const req = buildRequest('unlock-vault', { masterPassword }); - const resp = await sendNativeMessage(req); + const resp = parseResponse(await sendNativeMessage(req)); return resp; } catch (err) { return { success: false, error: err.message || 'unlock-failed' }; @@ -312,7 +312,7 @@ async function handleUnlockVault(masterPassword) { async function handleShowWindow() { try { const req = buildRequest('show-window'); - const resp = await sendNativeMessage(req); + const resp = parseResponse(await sendNativeMessage(req)); return resp; } catch (err) { return { success: false, error: err.message || 'show-window-failed' }; @@ -371,7 +371,7 @@ async function handleFillCredential(credentialId, username, tabId) { // Request encrypted password const req = buildRequest('get-credential-password', { id: credentialId }); - const resp = await sendNativeMessage(req); + const resp = parseResponse(await sendNativeMessage(req)); if (!resp.success) { return resp; diff --git a/extensions/chrome/lib/i18n.js b/extensions/chrome/lib/i18n.js index c62d2e9..cf492b6 100644 --- a/extensions/chrome/lib/i18n.js +++ b/extensions/chrome/lib/i18n.js @@ -103,7 +103,7 @@ const STRINGS = { unlockBtn: 'Déverrouiller', wrongPassword: 'Mot de passe incorrect. Réessayez.', unlocking: 'Déverrouillage...', - emptyTitle: 'Aucune identifiant', + emptyTitle: 'Aucun identifiant', emptySub: 'Aucun identifiant enregistré.', loadingText: 'Connexion...', tabThisSite: 'Ce site', @@ -215,6 +215,6 @@ const STRINGS = { }; // Resolve the two-character primary language subtag from the browser locale. -// Falls back to Italian ('it') for unsupported locales. -const lang = (navigator.language || 'it').slice(0, 2).toLowerCase(); -window.t = STRINGS[lang] || STRINGS['it']; +// Falls back to English ('en') for unsupported locales. +const lang = (navigator.language || 'en').slice(0, 2).toLowerCase(); +window.t = STRINGS[lang] || STRINGS['en']; diff --git a/extensions/chrome/manifest.json b/extensions/chrome/manifest.json index d9357b2..5a524d3 100644 --- a/extensions/chrome/manifest.json +++ b/extensions/chrome/manifest.json @@ -2,18 +2,24 @@ "manifest_version": 3, "name": "PassKey", "version": "1.0.0", + "author": "Giuseppe Imperato", + "homepage_url": "https://github.com/pexatar/PassKey", "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4pcZ6gFcMwryE8/OgBRW0lFn7a8kad6/J84GostBKfqf1at5RVaGW31arX54L+usiJ4EUizkwaTvUKKtoWaxyUYJ3nOxPNfrRysoJQwjP50IBQdOnR7VXqcKzO+L8xvfcdsK4CHsyTX7bU1Q/xoeawR9Or5yVKSPhNy86A63Qa2z2Y2QIyCB713jrElycb2sOsK/szEkIpG5teJCg/NA1nZ1VQXMu8j+kROaKNL0atpZAUoef2EGLU6uZK46es+HyvGXDZk6ZiQoey3FPhz5iMAd2ATS7Ml+9+TuTSE+tvtmF2swWI8SvWBshxa5a7y/UGE2Qqnr/XWY1eNLhP4MuQIDAQAB", - "description": "PassKey Password Manager - Autofill Extension", + "description": "Local password manager integration for PassKey desktop app. Autofill credentials, credit cards and identities stored securely on your PC — no cloud, no subscription.", "permissions": [ "nativeMessaging", - "activeTab" + "activeTab", + "tabs" ], + "content_security_policy": { + "extension_pages": "script-src 'self'; object-src 'none';" + }, "background": { "service_worker": "background.js" }, "content_scripts": [ { - "matches": [""], + "matches": ["https://*/*", "http://*/*"], "js": ["lib/url-utils.js", "lib/messages.js", "content.js"], "run_at": "document_idle", "all_frames": false diff --git a/extensions/chrome/popup/popup.html b/extensions/chrome/popup/popup.html index e11c509..b85d287 100644 --- a/extensions/chrome/popup/popup.html +++ b/extensions/chrome/popup/popup.html @@ -1,5 +1,5 @@ - + diff --git a/extensions/chrome/popup/popup.js b/extensions/chrome/popup/popup.js index 1006f68..136e756 100644 --- a/extensions/chrome/popup/popup.js +++ b/extensions/chrome/popup/popup.js @@ -39,6 +39,21 @@ const SPIN_SVG = ` `; +// ─── SVG helper (CSP-safe: avoids .innerHTML) ──────────────────────────────── + +/** + * Parses an SVG string and appends the resulting SVG element as the sole child + * of the given element. Uses DOMParser instead of .innerHTML to comply with + * the extension's Content Security Policy (script-src 'self'). + * + * @param {Element} element - The container element to update. + * @param {string} svgString - Raw SVG markup string. + */ +function setSvgIcon(element, svgString) { + const doc = new DOMParser().parseFromString(svgString, 'image/svg+xml'); + element.replaceChildren(doc.documentElement); +} + // ─── State ──────────────────────────────────────────────────────────────────── const STATES = ['loading', 'disconnected', 'unlock', 'empty', 'list']; @@ -85,6 +100,9 @@ const footerVersion = $('footer-version'); */ function initStrings() { const t = window.t; + // Set lang attribute dynamically so screen readers and CSS :lang() rules + // reflect the actual locale (popup.html defaults to 'en' as static fallback). + document.documentElement.lang = (navigator.language || 'en').slice(0, 2).toLowerCase(); loadingText.textContent = t.loadingText; disconnectedTitle.textContent = t.disconnectedTitle; disconnectedSub.textContent = t.disconnectedSub; @@ -283,7 +301,7 @@ tabAll.addEventListener('click', () => switchView('all')); * @param {Array<{id: string, title: string, username: string, hasPassword: boolean}>} creds */ function renderList(creds) { - credList.innerHTML = ''; + credList.replaceChildren(); for (const cred of creds) { credList.appendChild(buildItem(cred)); } @@ -345,7 +363,7 @@ function buildItem(cred) { const pwIndicator = document.createElement('span'); pwIndicator.className = 'pk-pw-indicator'; if (window.t.hasPassword) pwIndicator.setAttribute('aria-label', window.t.hasPassword); - pwIndicator.innerHTML = LOCK_SM_SVG; + setSvgIcon(pwIndicator, LOCK_SM_SVG); li.appendChild(avatar); li.appendChild(body); @@ -397,7 +415,7 @@ function makeIconBtn(svgHtml, label, action) { btn.setAttribute('data-action', action); btn.setAttribute('tabindex', '-1'); btn.type = 'button'; - btn.innerHTML = svgHtml; + setSvgIcon(btn, svgHtml); return btn; } @@ -422,7 +440,7 @@ function makeActionBtn(svgHtml, label, action) { const icon = document.createElement('span'); icon.className = 'pk-action-icon'; icon.setAttribute('aria-hidden', 'true'); - icon.innerHTML = svgHtml; + setSvgIcon(icon, svgHtml); const text = document.createElement('span'); text.className = 'pk-action-label'; @@ -459,7 +477,7 @@ async function onCopyUsername(cred, btn) { */ async function onCopyPassword(cred, btn) { const iconTarget = btn.querySelector('.pk-action-icon') ?? btn; - iconTarget.innerHTML = SPIN_SVG; + setSvgIcon(iconTarget, SPIN_SVG); btn.disabled = true; try { @@ -485,15 +503,19 @@ async function onCopyPassword(cred, btn) { async function onFill(cred, btn) { if (btn) { const iconTarget = btn.querySelector('.pk-action-icon') ?? btn; - iconTarget.innerHTML = SPIN_SVG; + setSvgIcon(iconTarget, SPIN_SVG); btn.disabled = true; } try { + // Re-query active tab at fill time to avoid stale tab ID (user may have + // switched tabs without closing the popup). + const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true }); + if (!activeTab?.id) { window.close(); return; } await chrome.runtime.sendMessage({ type: 'fill-credential', id: cred.id, username: cred.username, - tabId: activeTabId + tabId: activeTab.id }); } catch { /* ignore */ } window.close(); @@ -512,7 +534,7 @@ async function onFill(cred, btn) { function showBtnFeedback(btn, iconSvg, ok, isError = false, restoreIcon = null) { // Support both icon-only (.pk-icon-btn) and labeled (.pk-action-btn) buttons const iconTarget = btn.querySelector('.pk-action-icon') ?? btn; - iconTarget.innerHTML = iconSvg; + setSvgIcon(iconTarget, iconSvg); btn.classList.toggle('success', ok && !isError); btn.classList.toggle('error', isError); btn.disabled = false; @@ -521,7 +543,7 @@ function showBtnFeedback(btn, iconSvg, ok, isError = false, restoreIcon = null) copyFeedback.textContent = window.t[msgKey]; setTimeout(() => { - iconTarget.innerHTML = restoreIcon ?? (ok ? CHECK_SVG : iconSvg); + setSvgIcon(iconTarget, restoreIcon ?? (ok ? CHECK_SVG : iconSvg)); btn.classList.remove('success', 'error'); copyFeedback.textContent = ''; }, 1500); @@ -543,7 +565,7 @@ async function doUnlock() { if (!pw) return; unlockError.hidden = true; - btnUnlock.innerHTML = SPIN_SVG; + setSvgIcon(btnUnlock, SPIN_SVG); btnUnlock.disabled = true; let result; @@ -562,15 +584,18 @@ async function doUnlock() { } else { unlockError.textContent = window.t.wrongPassword; unlockError.hidden = false; - btnUnlock.innerHTML = window.t.unlockBtn; - btnUnlock.disabled = false; + btnUnlock.textContent = window.t.unlockBtn; + btnUnlock.disabled = false; pwInput.focus(); } } // ─── Retry / reconnect ──────────────────────────────────────────────────────── -btnRetry.addEventListener('click', init); +btnRetry.addEventListener('click', () => { + btnRetry.disabled = true; + init().finally(() => { btnRetry.disabled = false; }); +}); // ─── Search / filter ────────────────────────────────────────────────────────── diff --git a/extensions/firefox/background.js b/extensions/firefox/background.js index 0ce33ea..b5f1b5a 100644 --- a/extensions/firefox/background.js +++ b/extensions/firefox/background.js @@ -224,7 +224,7 @@ async function handleMessage(msg, sender) { async function handleGetStatus() { try { const req = buildRequest('get-status'); - const resp = await sendNativeMessage(req); + const resp = parseResponse(await sendNativeMessage(req)); return resp; } catch (err) { return { success: false, error: err.message || 'desktop-not-running' }; @@ -241,7 +241,7 @@ async function handleGetStatus() { async function handleGetCredentials(url) { try { const req = buildRequest('get-credentials', { url }); - const resp = await sendNativeMessage(req); + const resp = parseResponse(await sendNativeMessage(req)); return resp; } catch (err) { return { success: false, error: err.message || 'desktop-not-running' }; @@ -256,7 +256,7 @@ async function handleGetCredentials(url) { async function handleGetAllCredentials() { try { const req = buildRequest('get-all-credentials'); - const resp = await sendNativeMessage(req); + const resp = parseResponse(await sendNativeMessage(req)); return resp; } catch (err) { return { success: false, error: err.message || 'desktop-not-running' }; @@ -275,7 +275,7 @@ async function handleCopyCredential(credentialId) { try { await ensureSession(); const req = buildRequest('get-credential-password', { id: credentialId }); - const resp = await sendNativeMessage(req); + const resp = parseResponse(await sendNativeMessage(req)); if (!resp.success) return resp; let password; @@ -300,7 +300,7 @@ async function handleCopyCredential(credentialId) { async function handleUnlockVault(masterPassword) { try { const req = buildRequest('unlock-vault', { masterPassword }); - const resp = await sendNativeMessage(req); + const resp = parseResponse(await sendNativeMessage(req)); return resp; } catch (err) { return { success: false, error: err.message || 'unlock-failed' }; @@ -315,7 +315,7 @@ async function handleUnlockVault(masterPassword) { async function handleShowWindow() { try { const req = buildRequest('show-window'); - const resp = await sendNativeMessage(req); + const resp = parseResponse(await sendNativeMessage(req)); return resp; } catch (err) { return { success: false, error: err.message || 'show-window-failed' }; @@ -374,7 +374,7 @@ async function handleFillCredential(credentialId, username, tabId) { // Request encrypted password const req = buildRequest('get-credential-password', { id: credentialId }); - const resp = await sendNativeMessage(req); + const resp = parseResponse(await sendNativeMessage(req)); if (!resp.success) { return resp; diff --git a/extensions/firefox/lib/i18n.js b/extensions/firefox/lib/i18n.js index c62d2e9..cf492b6 100644 --- a/extensions/firefox/lib/i18n.js +++ b/extensions/firefox/lib/i18n.js @@ -103,7 +103,7 @@ const STRINGS = { unlockBtn: 'Déverrouiller', wrongPassword: 'Mot de passe incorrect. Réessayez.', unlocking: 'Déverrouillage...', - emptyTitle: 'Aucune identifiant', + emptyTitle: 'Aucun identifiant', emptySub: 'Aucun identifiant enregistré.', loadingText: 'Connexion...', tabThisSite: 'Ce site', @@ -215,6 +215,6 @@ const STRINGS = { }; // Resolve the two-character primary language subtag from the browser locale. -// Falls back to Italian ('it') for unsupported locales. -const lang = (navigator.language || 'it').slice(0, 2).toLowerCase(); -window.t = STRINGS[lang] || STRINGS['it']; +// Falls back to English ('en') for unsupported locales. +const lang = (navigator.language || 'en').slice(0, 2).toLowerCase(); +window.t = STRINGS[lang] || STRINGS['en']; diff --git a/extensions/firefox/manifest.json b/extensions/firefox/manifest.json index e84c7cc..bc77934 100644 --- a/extensions/firefox/manifest.json +++ b/extensions/firefox/manifest.json @@ -2,14 +2,18 @@ "manifest_version": 3, "name": "PassKey", "version": "1.0.0", - "description": "PassKey Password Manager - Autofill Extension", + "author": "Giuseppe Imperato", + "homepage_url": "https://github.com/pexatar/PassKey", + "description": "Local password manager integration for PassKey desktop app. Autofill credentials, credit cards and identities stored securely on your PC — no cloud, no subscription.", "permissions": [ "nativeMessaging", - "activeTab" + "activeTab", + "tabs" ], + "host_permissions": ["https://*/*", "http://*/*"], "browser_specific_settings": { "gecko": { - "id": "passkey@passkey.local", + "id": "{3E08FACC-D43B-4B20-89E7-7888F6082E9D}", "strict_min_version": "128.0" } }, @@ -18,7 +22,7 @@ }, "content_scripts": [ { - "matches": [""], + "matches": ["https://*/*", "http://*/*"], "js": ["lib/url-utils.js", "lib/messages.js", "content.js"], "run_at": "document_idle", "all_frames": false diff --git a/extensions/firefox/popup/popup.html b/extensions/firefox/popup/popup.html index e11c509..b85d287 100644 --- a/extensions/firefox/popup/popup.html +++ b/extensions/firefox/popup/popup.html @@ -1,5 +1,5 @@ - + diff --git a/extensions/firefox/popup/popup.js b/extensions/firefox/popup/popup.js index 121bb7e..0fead01 100644 --- a/extensions/firefox/popup/popup.js +++ b/extensions/firefox/popup/popup.js @@ -41,6 +41,20 @@ const SPIN_SVG = ` `; +// ─── CSP-safe SVG injection ─────────────────────────────────────────────────── + +/** + * Sets the SVG content of an element in a CSP-compliant way (no innerHTML). + * Parses the SVG string via DOMParser and replaces the element's children. + * + * @param {Element} element - Target element to receive the SVG. + * @param {string} svgString - SVG markup string. + */ +function setSvgIcon(element, svgString) { + const doc = new DOMParser().parseFromString(svgString, 'image/svg+xml'); + element.replaceChildren(doc.documentElement); +} + // ─── State ──────────────────────────────────────────────────────────────────── const STATES = ['loading', 'disconnected', 'unlock', 'empty', 'list']; @@ -103,6 +117,7 @@ function initStrings() { btnClear.setAttribute('aria-label', t.clearSearch); credList.setAttribute('aria-label', t.tabAll); btnOpenApp.textContent = t.openApp; + document.documentElement.lang = (navigator.language || 'en').slice(0, 2).toLowerCase(); } // ─── State machine ──────────────────────────────────────────────────────────── @@ -285,7 +300,7 @@ tabAll.addEventListener('click', () => switchView('all')); * @param {Array<{id: string, title: string, username: string, hasPassword: boolean}>} creds */ function renderList(creds) { - credList.innerHTML = ''; + credList.replaceChildren(); for (const cred of creds) { credList.appendChild(buildItem(cred)); } @@ -347,7 +362,7 @@ function buildItem(cred) { const pwIndicator = document.createElement('span'); pwIndicator.className = 'pk-pw-indicator'; if (window.t.hasPassword) pwIndicator.setAttribute('aria-label', window.t.hasPassword); - pwIndicator.innerHTML = LOCK_SM_SVG; + setSvgIcon(pwIndicator, LOCK_SM_SVG); li.appendChild(avatar); li.appendChild(body); @@ -399,7 +414,7 @@ function makeIconBtn(svgHtml, label, action) { btn.setAttribute('data-action', action); btn.setAttribute('tabindex', '-1'); btn.type = 'button'; - btn.innerHTML = svgHtml; + setSvgIcon(btn, svgHtml); return btn; } @@ -424,7 +439,7 @@ function makeActionBtn(svgHtml, label, action) { const icon = document.createElement('span'); icon.className = 'pk-action-icon'; icon.setAttribute('aria-hidden', 'true'); - icon.innerHTML = svgHtml; + setSvgIcon(icon, svgHtml); const text = document.createElement('span'); text.className = 'pk-action-label'; @@ -461,7 +476,7 @@ async function onCopyUsername(cred, btn) { */ async function onCopyPassword(cred, btn) { const iconTarget = btn.querySelector('.pk-action-icon') ?? btn; - iconTarget.innerHTML = SPIN_SVG; + setSvgIcon(iconTarget, SPIN_SVG); btn.disabled = true; try { @@ -487,15 +502,17 @@ async function onCopyPassword(cred, btn) { async function onFill(cred, btn) { if (btn) { const iconTarget = btn.querySelector('.pk-action-icon') ?? btn; - iconTarget.innerHTML = SPIN_SVG; + setSvgIcon(iconTarget, SPIN_SVG); btn.disabled = true; } try { + const [activeTab] = await browser.tabs.query({ active: true, currentWindow: true }); + if (!activeTab?.id) { window.close(); return; } await browser.runtime.sendMessage({ type: 'fill-credential', id: cred.id, username: cred.username, - tabId: activeTabId + tabId: activeTab.id }); } catch { /* ignore */ } window.close(); @@ -514,7 +531,7 @@ async function onFill(cred, btn) { function showBtnFeedback(btn, iconSvg, ok, isError = false, restoreIcon = null) { // Support both icon-only (.pk-icon-btn) and labeled (.pk-action-btn) buttons const iconTarget = btn.querySelector('.pk-action-icon') ?? btn; - iconTarget.innerHTML = iconSvg; + setSvgIcon(iconTarget, iconSvg); btn.classList.toggle('success', ok && !isError); btn.classList.toggle('error', isError); btn.disabled = false; @@ -523,7 +540,7 @@ function showBtnFeedback(btn, iconSvg, ok, isError = false, restoreIcon = null) copyFeedback.textContent = window.t[msgKey]; setTimeout(() => { - iconTarget.innerHTML = restoreIcon ?? (ok ? CHECK_SVG : iconSvg); + setSvgIcon(iconTarget, restoreIcon ?? (ok ? CHECK_SVG : iconSvg)); btn.classList.remove('success', 'error'); copyFeedback.textContent = ''; }, 1500); @@ -545,7 +562,7 @@ async function doUnlock() { if (!pw) return; unlockError.hidden = true; - btnUnlock.innerHTML = SPIN_SVG; + setSvgIcon(btnUnlock, SPIN_SVG); btnUnlock.disabled = true; let result; @@ -564,7 +581,7 @@ async function doUnlock() { } else { unlockError.textContent = window.t.wrongPassword; unlockError.hidden = false; - btnUnlock.innerHTML = window.t.unlockBtn; + btnUnlock.textContent = window.t.unlockBtn; btnUnlock.disabled = false; pwInput.focus(); } @@ -572,7 +589,10 @@ async function doUnlock() { // ─── Retry / reconnect ──────────────────────────────────────────────────────── -btnRetry.addEventListener('click', init); +btnRetry.addEventListener('click', () => { + btnRetry.disabled = true; + init().finally(() => { btnRetry.disabled = false; }); +}); // ─── Search / filter ────────────────────────────────────────────────────────── diff --git a/scripts/build-installer.ps1 b/scripts/build-installer.ps1 index 32881b5..80381ba 100644 --- a/scripts/build-installer.ps1 +++ b/scripts/build-installer.ps1 @@ -52,22 +52,34 @@ dotnet publish "$RepoRoot\src\PassKey.BrowserHost\PassKey.BrowserHost.csproj" ` -o $publishPath if ($LASTEXITCODE -ne 0) { throw "BrowserHost publish failed" } -# 3. Generate Native Messaging Host manifest -Write-Host "Generating NMH manifest..." -ForegroundColor Green -$nmhManifest = @{ +# 3. Generate Native Messaging Host manifest (reference copy bundled with installer) +# NOTE: The authoritative manifests are generated at runtime by NativeMessagingRegistrationService +# into %LOCALAPPDATA%\PassKey\native-messaging\ with correct absolute paths. +# Chrome and Firefox need separate manifests (different field names), so two files are created. +Write-Host "Generating NMH manifests..." -ForegroundColor Green + +$chromeManifest = @{ name = "com.passkey.host" description = "PassKey Native Messaging Host" path = "PassKey.BrowserHost.exe" type = "stdio" allowed_origins = @( - "chrome-extension://passkey-extension-id/" + "chrome-extension://jmddfinmjgpgmfkiblhnjccagheadpop/" ) +} | ConvertTo-Json -Depth 3 + +$firefoxManifest = @{ + name = "com.passkey.host" + description = "PassKey Native Messaging Host" + path = "PassKey.BrowserHost.exe" + type = "stdio" allowed_extensions = @( - "passkey@passkey.local" + "{3E08FACC-D43B-4B20-89E7-7888F6082E9D}" ) } | ConvertTo-Json -Depth 3 -$nmhManifest | Out-File -Encoding utf8 -FilePath (Join-Path $publishPath "com.passkey.host.json") +$chromeManifest | Out-File -Encoding utf8 -FilePath (Join-Path $publishPath "com.passkey.host.json") +$firefoxManifest | Out-File -Encoding utf8 -FilePath (Join-Path $publishPath "com.passkey.host.firefox.json") # 4. Compile Inno Setup installer if (Test-Path $InnoSetupPath) { diff --git a/src/PassKey.BrowserHost/Manifests/com.passkey.host.firefox.json b/src/PassKey.BrowserHost/Manifests/com.passkey.host.firefox.json index 232c686..250d763 100644 --- a/src/PassKey.BrowserHost/Manifests/com.passkey.host.firefox.json +++ b/src/PassKey.BrowserHost/Manifests/com.passkey.host.firefox.json @@ -4,6 +4,6 @@ "path": "A:\\Progetti\\00 PassKey\\PK4\\src\\PassKey.BrowserHost\\bin\\Debug\\net10.0\\win-x64\\PassKey.BrowserHost.exe", "type": "stdio", "allowed_extensions": [ - "passkey@passkey.local" + "{3E08FACC-D43B-4B20-89E7-7888F6082E9D}" ] } diff --git a/src/PassKey.Desktop/Services/NativeMessagingRegistrationService.cs b/src/PassKey.Desktop/Services/NativeMessagingRegistrationService.cs index c02f16a..5f3eb1c 100644 --- a/src/PassKey.Desktop/Services/NativeMessagingRegistrationService.cs +++ b/src/PassKey.Desktop/Services/NativeMessagingRegistrationService.cs @@ -23,7 +23,8 @@ public static class NativeMessagingRegistrationService internal const string ChromeExtensionId = "jmddfinmjgpgmfkiblhnjccagheadpop"; // Firefox extension ID — defined in browser_specific_settings.gecko.id in Firefox manifest. - private const string FirefoxExtensionId = "passkey@passkey.local"; + // Must match extensions/firefox/manifest.json → browser_specific_settings.gecko.id. + private const string FirefoxExtensionId = "{3E08FACC-D43B-4B20-89E7-7888F6082E9D}"; /// /// Ensures that Chrome, Edge, and Firefox Native Messaging Host manifests are written to disk