Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ce0cb83
Add browser extension native messaging: credential leak notifications
chrisriv10 Jul 21, 2026
75173dc
Add browser extension integration toggle to Settings
chrisriv10 Jul 21, 2026
4fe4ec2
Enhance tray dashboard: health score, RTP status, quick scan, network…
chrisriv10 Jul 22, 2026
9ef97ea
Add tray dashboard: health score, RTP, quick scan, network sparkline
chrisriv10 Jul 22, 2026
a189d87
Fix health score translations for all locales
chrisriv10 Jul 22, 2026
673f2c1
Add health.reason.* translations for all locales
chrisriv10 Jul 22, 2026
8a139be
Complete health score translations for all locales
chrisriv10 Jul 22, 2026
dfa0339
Fix browser extension issues from Qodo review
chrisriv10 Jul 22, 2026
13d2a43
Add custom NSIS installer with Soterios branding
chrisriv10 Jul 22, 2026
3219547
Fix NSIS installer: remove duplicate MUI_ICON definitions that confli…
chrisriv10 Jul 22, 2026
d84046a
Fix NSIS installer: remove conflicting MUI definitions
chrisriv10 Jul 22, 2026
3602e0b
Fix NSIS installer: fix UninstPage types and add custom uninstaller p…
chrisriv10 Jul 22, 2026
5e21afb
Fix NSIS installer: remove MUI_SETFONT macro (not available), use Set…
chrisriv10 Jul 22, 2026
79ebac8
Fix NSIS installer: remove SetFont from functions (not valid in NSIS …
chrisriv10 Jul 22, 2026
827ccfe
Add click alert popup on lockdown page
chrisriv10 Jul 23, 2026
24052fe
Add click alert for emergency lockdown sidebar nav item
chrisriv10 Jul 23, 2026
90a00a8
Remove page click alert, keep only sidebar nav click alert for lockdown
chrisriv10 Jul 23, 2026
900e96b
Remove sidebar click alert, add emergency lockdown feature toggle in …
chrisriv10 Jul 23, 2026
cd69cd5
fix: update extension build paths and remove duplicate locale entries
chrisriv10 Jul 23, 2026
c70124d
Replace Chris Rivera with Christopher Rivera across codebase
chrisriv10 Jul 23, 2026
83c9ceb
fix(extension): resolve listener leaks, add host check, fix icon cleanup
chrisriv10 Jul 23, 2026
d29150b
feat(browser-extension): add icon/auto-check settings, fix install/bu…
chrisriv10 Jul 23, 2026
c924c20
feat(i18n): Add Arabic and German translations for Emergency Lockdown…
chrisriv10 Jul 25, 2026
1f606ea
feat(native-host): add named pipe fallback, fix native message parsing
chrisriv10 Jul 30, 2026
4bb7e89
Merge main into feature/emergency-lockdown: resolve IPC refactoring c…
chrisriv10 Jul 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,4 +224,4 @@ Even small improvements, bug reports, or suggestions are appreciated.

Soterios is released under the [MIT License](build/LICENSE.txt).

**Copyright © 2026 Chris Rivera**
**Copyright © 2026 Christopher Rivera**
83 changes: 83 additions & 0 deletions browser-extension/background.js
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();
}
});
220 changes: 220 additions & 0 deletions browser-extension/content.js
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;
}
}
});
Comment on lines +11 to +29

Copy link
Copy Markdown

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:

#!/bin/bash
set -euo pipefail

# Map the relevant file and nearby symbols first.
git ls-files browser-extension/content.js
printf '\n--- outline ---\n'
ast-grep outline browser-extension/content.js --view expanded || true
printf '\n--- relevant lines ---\n'
wc -l browser-extension/content.js
sed -n '1,260p' browser-extension/content.js

Repository: chrisriv10/Soterios

Length of output: 7603


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the file around init/loadSettings and the settings change handling.
nl -ba browser-extension/content.js | sed -n '1,260p'

Repository: chrisriv10/Soterios

Length of output: 197


Apply settings before the first scan.

init() calls scanForPasswordFields() before chrome.storage.sync.get() finishes, so a saved showIcon: false can still add icons on first load. The storage-change listener only updates currentSettings; it never removes or adds existing icons, so the DOM can stay out of sync until a later SETTINGS_UPDATED message. Make the initial settings read part of the startup flow, and reconcile already-added fields when settings change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@browser-extension/content.js` around lines 11 - 29, Update init() so it waits
for loadSettings() to complete before calling scanForPasswordFields(), ensuring
persisted settings apply to the first scan. Extend the chrome.storage.onChanged
listener to reconcile existing password fields after updating currentSettings,
adding or removing icons as needed to match the new showIcon setting.


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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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.

background.js returns { error } on fetch failures, but this passes that object to showResult(), where a missing pwned value is treated as false. Render an unavailable/unknown state when result.error is present instead of a clean result.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@browser-extension/content.js` around lines 150 - 151, Update the
CHECK_PASSWORD message handling around showResult so responses containing
result.error render an unavailable or unknown state instead of being treated as
safe; preserve the existing clean-result behavior for successful responses with
a valid pwned value.

} catch (err) {
console.error('[Soterios] Auto-check failed:', err);
}
}
};
input.addEventListener('input', autoCheckHandler);
icon._soteriosHandlers.autoCheckHandler = autoCheckHandler;
}

passwordFields.set(input, icon);
}
Comment thread
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();
}
}
});
11 changes: 11 additions & 0 deletions browser-extension/icons/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added browser-extension/icons/icon128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added browser-extension/icons/icon16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added browser-extension/icons/icon32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added browser-extension/icons/icon48.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions browser-extension/manifest.json
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
}
]
}
9 changes: 9 additions & 0 deletions browser-extension/native-host-manifest.json
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>/"
]
}
6 changes: 6 additions & 0 deletions browser-extension/native-host.bat
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" %*
Loading
Loading