-
Notifications
You must be signed in to change notification settings - Fork 10
Feature/emergency lockdown #92
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ce0cb83
75173dc
4fe4ec2
9ef97ea
a189d87
673f2c1
8a139be
dfa0339
13d2a43
3219547
d84046a
3602e0b
5e21afb
79ebac8
827ccfe
24052fe
90a00a8
900e96b
cd69cd5
c70124d
83c9ceb
d29150b
c924c20
1f606ea
4bb7e89
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| chrome.runtime.onInstalled.addListener((details) => { | ||
| if (details.reason === 'install') { | ||
| chrome.storage.sync.set({ externalLookupsEnabled: true }); | ||
| } | ||
| }); | ||
|
|
||
| // Handle CHECK_PASSWORD from content script | ||
| chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { | ||
| if (msg.type === 'CHECK_PASSWORD' && msg.password) { | ||
| checkPassword(msg.password).then(sendResponse); | ||
| return true; // async response | ||
| } | ||
| if (msg.type === 'CHECK_NATIVE_HOST') { | ||
| // Check if native host is connected | ||
| const connected = nativePort !== null; | ||
| sendResponse({ | ||
| connected, | ||
| error: connected ? null : 'Native host not installed or desktop app not running' | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| // Native messaging port for desktop app communication | ||
| let nativePort = null; | ||
|
|
||
| function connectNative() { | ||
| try { | ||
| nativePort = chrome.runtime.connectNative('com.soterios.credential_safety'); | ||
| nativePort.onDisconnect.addListener(() => { | ||
| console.log('[Soterios] Native host disconnected'); | ||
| nativePort = null; | ||
| }); | ||
| nativePort.onMessage.addListener(handleNativeMessage); | ||
| } catch (e) { | ||
| console.log('[Soterios] Native host connection failed:', e.message); | ||
| } | ||
| } | ||
|
|
||
| function handleNativeMessage(msg) { | ||
| console.log('[Soterios] Native message:', msg); | ||
| // Handle responses from desktop app if needed | ||
| } | ||
|
|
||
| async function checkPassword(password) { | ||
| const HIBP_API = 'https://api.pwnedpasswords.com/range/'; | ||
| const encoder = new TextEncoder(); | ||
| const data = encoder.encode(password); | ||
| const hashBuffer = await crypto.subtle.digest('SHA-1', data); | ||
| const hash = Array.from(new Uint8Array(hashBuffer)) | ||
| .map(b => b.toString(16).padStart(2, '0')) | ||
| .join('') | ||
| .toUpperCase(); | ||
|
|
||
| const prefix = hash.slice(0, 5); | ||
| const suffix = hash.slice(5); | ||
|
|
||
| try { | ||
| const resp = await fetch(`${HIBP_API}${prefix}`); | ||
| const text = await resp.text(); | ||
| const lines = text.trim().split('\n'); | ||
|
|
||
| for (const line of lines) { | ||
| const [suf, count] = line.split(':'); | ||
| if (suf === suffix) { | ||
| return { pwned: true, count: parseInt(count, 10) }; | ||
| } | ||
| } | ||
| return { pwned: false, count: 0 }; | ||
| } catch (e) { | ||
| console.error('[Soterios] HIBP check failed:', e); | ||
| return { error: e.message }; | ||
| } | ||
| } | ||
|
|
||
| // Connect to native host on startup | ||
| connectNative(); | ||
|
|
||
| // Reconnect if native host disconnects | ||
| chrome.runtime.onConnect.addListener(port => { | ||
| if (port.name === 'native-reconnect') { | ||
| connectNative(); | ||
| } | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,220 @@ | ||
| /** | ||
| * Soterios Browser Extension - Content Script | ||
| * Detects password fields, monitors for credential entry, and shows breach indicators | ||
| */ | ||
|
|
||
| let soteriosIcon = null; | ||
| let passwordFields = new Map(); | ||
| let observer = null; | ||
| let currentSettings = { showIcon: true, autoCheck: false }; | ||
|
|
||
| // Load settings from storage | ||
| function loadSettings() { | ||
| chrome.storage.sync.get(['showIcon', 'autoCheck'], (result) => { | ||
| currentSettings.showIcon = result.showIcon !== false; | ||
| currentSettings.autoCheck = result.autoCheck === true; | ||
| }); | ||
| } | ||
|
|
||
| // Listen for settings updates | ||
| chrome.storage.onChanged.addListener((changes, namespace) => { | ||
| if (namespace === 'sync') { | ||
| if (changes.showIcon !== undefined) { | ||
| currentSettings.showIcon = changes.showIcon.newValue !== false; | ||
| } | ||
| if (changes.autoCheck !== undefined) { | ||
| currentSettings.autoCheck = changes.autoCheck.newValue === true; | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| function createIcon() { | ||
| const icon = document.createElement('img'); | ||
| icon.src = chrome.runtime.getURL('icons/icon16.png'); | ||
| icon.style.cssText = ` | ||
| position: absolute; | ||
| width: 16px; height: 16px; | ||
| cursor: pointer; | ||
| opacity: 0.7; | ||
| transition: opacity 0.2s; | ||
| z-index: 2147483647; | ||
| pointer-events: auto; | ||
| `; | ||
| icon.title = 'Check password with Soterios'; | ||
| icon.addEventListener('mouseenter', () => icon.style.opacity = '1'); | ||
| icon.addEventListener('mouseleave', () => icon.style.opacity = '0.7'); | ||
| icon.addEventListener('click', onIconClick); | ||
| return icon; | ||
| } | ||
|
|
||
| function positionIcon(icon, input) { | ||
| const rect = input.getBoundingClientRect(); | ||
| icon.style.top = `${rect.top + window.scrollY + (rect.height - 16) / 2}px`; | ||
| icon.style.left = `${rect.right + window.scrollX - 20}px`; | ||
| } | ||
|
|
||
| async function onIconClick(e) { | ||
| const input = e.target.dataset.forInput; | ||
| const el = document.querySelector(`[data-soterios-id="${input}"]`); | ||
| if (!el) return; | ||
|
|
||
| const password = el.value; | ||
| if (!password) return; | ||
|
|
||
| try { | ||
| const result = await chrome.runtime.sendMessage({ type: 'CHECK_PASSWORD', password }); | ||
| showResult(el, result); | ||
| } catch (err) { | ||
| console.error('[Soterios] Check failed:', err); | ||
| } | ||
| } | ||
|
|
||
| function showResult(input, result) { | ||
| removeResult(input); | ||
|
|
||
| const badge = document.createElement('span'); | ||
| badge.dataset.soteriosBadge = input.dataset.soteriosId; | ||
| badge.style.cssText = ` | ||
| position: absolute; | ||
| top: -20px; right: -20px; | ||
| padding: 2px 6px; | ||
| border-radius: 3px; | ||
| font-size: 11px; | ||
| font-weight: 600; | ||
| color: white; | ||
| z-index: 2147483647; | ||
| background: ${result.pwned ? '#dc3545' : '#28a745'}; | ||
| box-shadow: 0 1px 3px rgba(0,0,0,0.3); | ||
| `; | ||
| badge.textContent = result.pwned ? `Pwned ${result.count}x` : 'Safe'; | ||
| badge.title = result.pwned | ||
| ? `Found in ${result.count} breach${result.count !== 1 ? 'es' : ''}. Change immediately.` | ||
| : 'Not found in known breaches (HIBP)'; | ||
| input.parentElement.style.position = 'relative'; | ||
| input.parentElement.appendChild(badge); | ||
|
|
||
| setTimeout(() => removeResult(input), 5000); | ||
| } | ||
|
|
||
| function removeResult(input) { | ||
| const badge = document.querySelector(`[data-soterios-badge="${input.dataset.soteriosId}"]`); | ||
| if (badge) badge.remove(); | ||
| } | ||
|
|
||
| function addIconToField(input) { | ||
| if (input.dataset.soteriosId) return; | ||
|
|
||
| // Check showIcon setting before adding icon | ||
| if (!currentSettings.showIcon) return; | ||
|
|
||
| const id = `soterios-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; | ||
| input.dataset.soteriosId = id; | ||
|
|
||
| const icon = createIcon(); | ||
| icon.dataset.forInput = id; | ||
| document.body.appendChild(icon); | ||
| positionIcon(icon, input); | ||
|
|
||
| const updatePos = () => positionIcon(icon, input); | ||
| window.addEventListener('scroll', updatePos, true); | ||
| window.addEventListener('resize', updatePos); | ||
|
|
||
| // Store handler references for cleanup | ||
| icon._soteriosHandlers = { updatePos, scroll: true, resize: true }; | ||
|
|
||
| const cleanup = () => { | ||
| if (icon._soteriosHandlers) { | ||
| if (icon._soteriosHandlers.scroll) { | ||
| window.removeEventListener('scroll', icon._soteriosHandlers.updatePos, true); | ||
| } | ||
| if (icon._soteriosHandlers.resize) { | ||
| window.removeEventListener('resize', icon._soteriosHandlers.updatePos); | ||
| } | ||
| if (icon._soteriosHandlers.autoCheckHandler) { | ||
| input.removeEventListener('input', icon._soteriosHandlers.autoCheckHandler); | ||
| } | ||
| } | ||
| icon.remove(); | ||
| passwordFields.delete(input); | ||
| delete input.dataset.soteriosId; | ||
| }; | ||
|
|
||
| input.addEventListener('blur', () => setTimeout(cleanup, 200), { once: true }); | ||
|
|
||
| // Add autoCheck listener if enabled | ||
| if (currentSettings.autoCheck) { | ||
| const autoCheckHandler = async () => { | ||
| const password = input.value; | ||
| if (password && password.length >= 8) { | ||
| try { | ||
| const result = await chrome.runtime.sendMessage({ type: 'CHECK_PASSWORD', password }); | ||
| showResult(input, result); | ||
|
Comment on lines
+150
to
+151
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Do not render HIBP failures as a safe password.
🤖 Prompt for AI Agents |
||
| } catch (err) { | ||
| console.error('[Soterios] Auto-check failed:', err); | ||
| } | ||
| } | ||
| }; | ||
| input.addEventListener('input', autoCheckHandler); | ||
| icon._soteriosHandlers.autoCheckHandler = autoCheckHandler; | ||
| } | ||
|
|
||
| passwordFields.set(input, icon); | ||
| } | ||
|
qodo-code-review[bot] marked this conversation as resolved.
|
||
|
|
||
| function scanForPasswordFields() { | ||
| const inputs = document.querySelectorAll('input[type="password"]:not([data-soterios-id])'); | ||
| inputs.forEach(addIconToField); | ||
| } | ||
|
|
||
| function init() { | ||
| if (document.readyState === 'loading') { | ||
| document.addEventListener('DOMContentLoaded', init, { once: true }); | ||
| return; | ||
| } | ||
|
|
||
| loadSettings(); | ||
| scanForPasswordFields(); | ||
|
|
||
| observer = new MutationObserver(mutations => { | ||
| for (const m of mutations) { | ||
| m.addedNodes.forEach(node => { | ||
| if (node.nodeType === 1) { | ||
| if (node.matches('input[type="password"]')) addIconToField(node); | ||
| node.querySelectorAll('input[type="password"]').forEach(addIconToField); | ||
| } | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| observer.observe(document.body, { childList: true, subtree: true }); | ||
| } | ||
|
|
||
| if (typeof window !== 'undefined') { | ||
| init(); | ||
| } | ||
|
|
||
| chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { | ||
| if (msg.type === 'SETTINGS_UPDATED') { | ||
| if (!msg.settings.showIcon) { | ||
| // Properly clean up all icons and their listeners | ||
| passwordFields.forEach((icon, input) => { | ||
| if (icon._soteriosHandlers) { | ||
| if (icon._soteriosHandlers.scroll) { | ||
| window.removeEventListener('scroll', icon._soteriosHandlers.updatePos, true); | ||
| } | ||
| if (icon._soteriosHandlers.resize) { | ||
| window.removeEventListener('resize', icon._soteriosHandlers.updatePos); | ||
| } | ||
| if (icon._soteriosHandlers.autoCheckHandler) { | ||
| input.removeEventListener('input', icon._soteriosHandlers.autoCheckHandler); | ||
| } | ||
| } | ||
| icon.remove(); | ||
| delete input.dataset.soteriosId; | ||
| }); | ||
| passwordFields.clear(); | ||
| } else { | ||
| scanForPasswordFields(); | ||
| } | ||
| } | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| { | ||
| "manifest_version": 3, | ||
| "name": "Soterios Credential Safety", | ||
| "version": "1.2.1", | ||
| "description": "Password breach checker and credential safety companion for Soterios", | ||
| "icons": { | ||
| "16": "icons/icon16.png", | ||
| "32": "icons/icon32.png", | ||
| "48": "icons/icon48.png", | ||
| "128": "icons/icon128.png" | ||
| }, | ||
| "action": { | ||
| "default_popup": "popup.html", | ||
| "default_title": "Soterios Credential Safety" | ||
| }, | ||
| "options_page": "options.html", | ||
| "permissions": ["storage", "nativeMessaging"], | ||
| "host_permissions": ["https://api.pwnedpasswords.com/*"], | ||
| "background": { | ||
| "service_worker": "background.js" | ||
| }, | ||
| "content_scripts": [ | ||
| { | ||
| "matches": ["<all_urls>"], | ||
| "js": ["content.js"], | ||
| "run_at": "document_idle", | ||
| "all_frames": true | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| { | ||
| "name": "com.soterios.credential_safety", | ||
| "description": "Soterios Credential Safety Native Messaging Host", | ||
| "path": "native-host.bat", | ||
| "type": "stdio", | ||
| "allowed_origins": [ | ||
| "chrome-extension://<EXTENSION_ID>/" | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| @echo off | ||
| REM Soterios Native Messaging Host | ||
| REM This batch file launches the Node.js native host that communicates with the desktop app | ||
|
|
||
| set NODE_PATH=%~dp0..\..\node_modules | ||
| node "%~dp0native-host.js" %* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: chrisriv10/Soterios
Length of output: 7603
🏁 Script executed:
Repository: chrisriv10/Soterios
Length of output: 197
Apply settings before the first scan.
init()callsscanForPasswordFields()beforechrome.storage.sync.get()finishes, so a savedshowIcon: falsecan still add icons on first load. The storage-change listener only updatescurrentSettings; it never removes or adds existing icons, so the DOM can stay out of sync until a laterSETTINGS_UPDATEDmessage. Make the initial settings read part of the startup flow, and reconcile already-added fields when settings change.🤖 Prompt for AI Agents