diff options
Diffstat (limited to 'webext/firefox')
| -rw-r--r-- | webext/firefox/background.js | 69 | ||||
| -rw-r--r-- | webext/firefox/content.js | 57 | ||||
| -rw-r--r-- | webext/firefox/manifest.json | 35 | ||||
| -rw-r--r-- | webext/firefox/popup.html | 65 |
4 files changed, 226 insertions, 0 deletions
diff --git a/webext/firefox/background.js b/webext/firefox/background.js new file mode 100644 index 0000000..d05cd37 --- /dev/null +++ b/webext/firefox/background.js @@ -0,0 +1,69 @@ +// Basic background script for Cerberus Firefox extension + +browser.runtime.onInstalled.addListener(() => { + console.log("Cerberus extension installed"); +}); + +function connectNative() { + try { + return browser.runtime.connectNative('com.cerberus.pm'); + } catch (e) { + console.error('Native host connection failed', e); + return null; + } +} + +function nativeRequest(payload) { + return new Promise((resolve, reject) => { + const port = connectNative(); + if (!port) { + reject(new Error('no_native_host')); + return; + } + const onMessage = (resp) => { + port.onMessage.removeListener(onMessage); + port.disconnect(); + resolve(resp); + }; + const onDisconnect = () => { + port.onMessage.removeListener(onMessage); + reject(new Error('disconnected')); + }; + port.onMessage.addListener(onMessage); + port.onDisconnect.addListener(onDisconnect); + port.postMessage(payload); + }); +} + +async function getOriginForTab(tabId) { + const tab = await browser.tabs.get(tabId); + try { + const url = new URL(tab.url); + return url.origin; + } catch (e) { + return tab.url; + } +} + +// Message router between popup/content and native host +browser.runtime.onMessage.addListener(async (message, sender) => { + if (!message || !message.type) return; + if (message.type === 'GET_PAGE_FORMS') { + return browser.tabs.sendMessage(sender.tab.id, { type: 'SCAN_FORMS' }); + } + if (message.type === 'FILL_CREDENTIALS') { + return browser.tabs.sendMessage(sender.tab.id, { + type: 'FILL_CREDENTIALS', + payload: message.payload, + }); + } + if (message.type === 'GET_CREDENTIALS_FOR_TAB') { + const origin = await getOriginForTab(sender.tab.id); + const resp = await nativeRequest({ type: 'get_for_origin', origin, include_password: true }).catch(err => ({ ok: false, error: String(err) })); + return resp; + } + if (message.type === 'PING_NATIVE') { + const resp = await nativeRequest({ type: 'ping' }).catch(err => ({ ok: false, error: String(err) })); + return resp; + } +}); diff --git a/webext/firefox/content.js b/webext/firefox/content.js new file mode 100644 index 0000000..9db4005 --- /dev/null +++ b/webext/firefox/content.js @@ -0,0 +1,57 @@ +// Content script: detects login/change-password forms and can fill them + +function findLoginForms() { + const forms = Array.from(document.querySelectorAll('form')); + const results = []; + for (const f of forms) { + const inputs = Array.from(f.querySelectorAll('input')); + const hasPassword = inputs.some(i => (i.type || '').toLowerCase() === 'password'); + const username = inputs.find(i => ['text','email','tel','username'].includes((i.type || '').toLowerCase()) || /user|email|login/i.test(i.name || i.id || '')); + const password = inputs.find(i => (i.type || '').toLowerCase() === 'password'); + if (hasPassword && (username || password)) { + results.push({ + action: f.getAttribute('action') || location.href, + usernameName: username && (username.name || username.id) || null, + passwordName: password && (password.name || password.id) || null, + }); + } + } + return results; +} + +function fillCredentials(payload) { + const { username, password } = payload || {}; + if (!username && !password) return false; + // Try to fill the first reasonable form + const forms = Array.from(document.querySelectorAll('form')); + for (const f of forms) { + const inputs = Array.from(f.querySelectorAll('input')); + const u = inputs.find(i => ['text','email','tel','username'].includes((i.type || '').toLowerCase()) || /user|email|login/i.test(i.name || i.id || '')); + const p = inputs.find(i => (i.type || '').toLowerCase() === 'password'); + if (u || p) { + if (u && username) { + u.focus(); + u.value = username; + u.dispatchEvent(new Event('input', { bubbles: true })); + } + if (p && password) { + p.focus(); + p.value = password; + p.dispatchEvent(new Event('input', { bubbles: true })); + } + return true; + } + } + return false; +} + +browser.runtime.onMessage.addListener((message) => { + if (!message || !message.type) return; + if (message.type === 'SCAN_FORMS') { + return Promise.resolve({ forms: findLoginForms() }); + } + if (message.type === 'FILL_CREDENTIALS') { + const ok = fillCredentials(message.payload || {}); + return Promise.resolve({ ok }); + } +}); diff --git a/webext/firefox/manifest.json b/webext/firefox/manifest.json new file mode 100644 index 0000000..12b2e7a --- /dev/null +++ b/webext/firefox/manifest.json @@ -0,0 +1,35 @@ +{ + "manifest_version": 2, + "name": "Cerberus Password Manager", + "version": "0.1.0", + "description": "Auto-fill and manage passwords with Cerberus.", + "permissions": [ + "activeTab", + "storage", + "contextMenus", + "tabs", + "nativeMessaging", + "<all_urls>" + ], + "background": { + "scripts": ["background.js"], + "persistent": false + }, + "content_scripts": [ + { + "matches": ["<all_urls>"], + "js": ["content.js"], + "run_at": "document_end" + } + ], + "browser_action": { + "default_title": "Cerberus", + "default_popup": "popup.html" + } + , + "browser_specific_settings": { + "gecko": { + "id": "cerberus@example.com" + } + } +} diff --git a/webext/firefox/popup.html b/webext/firefox/popup.html new file mode 100644 index 0000000..0c51b06 --- /dev/null +++ b/webext/firefox/popup.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8" /> + <title>Cerberus</title> + <style> + body { font-family: sans-serif; width: 280px; margin: 10px; } + button { width: 100%; padding: 8px; margin: 6px 0; } + input { width: 100%; padding: 6px; margin: 6px 0; } + </style> +</head> +<body> + <h3>Cerberus</h3> + <input id="username" placeholder="Username" /> + <input id="password" placeholder="Password" type="password" /> + <button id="fetch">Fetch from Vault</button> + <button id="fill">Fill on Page</button> + <div id="status" style="font-size: 12px; color: #666; margin-top: 6px;"></div> + <script> + async function getActiveTab() { + const tabs = await browser.tabs.query({ active: true, currentWindow: true }); + return tabs[0]; + } + + async function fill(username, password) { + const tab = await getActiveTab(); + await browser.tabs.sendMessage(tab.id, { type: 'FILL_CREDENTIALS', payload: { username, password } }); + } + + document.getElementById('fill').addEventListener('click', async () => { + const username = document.getElementById('username').value; + const password = document.getElementById('password').value; + try { + await fill(username, password); + window.close(); + } catch (e) { console.error(e); } + }); + + document.getElementById('fetch').addEventListener('click', async () => { + const status = document.getElementById('status'); + status.textContent = 'Fetching credentials from vault...'; + try { + const resp = await browser.runtime.sendMessage({ type: 'GET_CREDENTIALS_FOR_TAB' }); + if (!resp || !resp.ok) { + status.textContent = 'Failed to fetch (is native host installed and unlocked?)'; + return; + } + const results = resp.result || []; + if (results.length === 0) { + status.textContent = 'No entries matched for this origin'; + return; + } + // Choose the first match (later: add a dropdown) + const { username, password } = results[0]; + if (username) document.getElementById('username').value = username; + if (password) document.getElementById('password').value = password; + status.textContent = 'Fetched from vault'; + } catch (e) { + console.error(e); + status.textContent = 'Error fetching credentials'; + } + }); + </script> +</body> +</html> |
