Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
94 changes: 94 additions & 0 deletions browser-extension-host.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/usr/bin/env node
/**
* Soterios Native Messaging Host
* Receives messages from browser extension and forwards to desktop app
*/

const { spawn } = require('child_process');
const fs = require('fs');
const path = require('path');

function readMessage() {
return new Promise((resolve, reject) => {
const lenBuf = Buffer.alloc(4);
let read = 0;
process.stdin.on('readable', () => {
const chunk = process.stdin.read(4 - read);
if (chunk) {
chunk.copy(lenBuf, read);
read += chunk.length;
if (read === 4) {
const len = lenBuf.readUInt32LE(0);
const msgBuf = Buffer.alloc(len);
let msgRead = 0;
process.stdin.on('readable', () => {
const chunk = process.stdin.read(len - msgRead);
if (chunk) {
chunk.copy(msgBuf, msgRead);
msgRead += chunk.length;
if (msgRead === len) {
resolve(JSON.parse(msgBuf.toString('utf8')));
}
}
});
}
}
});
process.stdin.on('error', reject);
});
}

function sendMessage(msg) {
const buf = Buffer.from(JSON.stringify(msg), 'utf8');
const lenBuf = Buffer.alloc(4);
lenBuf.writeUInt32LE(buf.length, 0);
process.stdout.write(lenBuf);
process.stdout.write(buf);
}

async function connectToDesktopApp() {
const pipeName = '\\\\.\\pipe\\soterios-credential-safety';
return new Promise((resolve, reject) => {
const client = require('net').createConnection(pipeName, () => {
resolve(client);
});
client.on('error', reject);
});
}

let desktopClient = null;

async function main() {
console.error('[Soterios Host] Starting...');

try {
desktopClient = await connectToDesktopApp();
console.error('[Soterios Host] Connected to desktop app');
} catch (e) {
console.error('[Soterios Host] Desktop app not running:', e.message);
}

while (true) {
try {
const msg = await readMessage();
console.error('[Soterios Host] Received:', msg.type);

if (msg.type === 'CREDENTIAL_LEAK') {
if (desktopClient) {
desktopClient.write(JSON.stringify({ type: 'CREDENTIAL_LEAK', ...msg.payload }) + '\n');
}
sendMessage({ ok: true });
} else if (msg.type === 'PING') {
sendMessage({ pong: true });
}
} catch (e) {
if (e.message.includes('Unexpected end of JSON')) break;
console.error('[Soterios Host] Error:', e.message);
}
}
}

main().catch(e => {
console.error('[Soterios Host] Fatal:', e);
process.exit(1);
});
9 changes: 9 additions & 0 deletions browser-extension-host.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": "browser-extension-host.exe",
"type": "stdio",
"allowed_origins": [
"chrome-extension://YOUR_EXTENSION_ID_HERE/"
]
}
73 changes: 73 additions & 0 deletions browser-extension/background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
chrome.runtime.onInstalled.addListener(() => {
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
}
});

// 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();
}
});
142 changes: 142 additions & 0 deletions browser-extension/content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* 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;

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;

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);
input.addEventListener('blur', () => setTimeout(() => icon.remove(), 200), { once: true });

passwordFields.set(input, icon);
}

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;
}

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) {
passwordFields.forEach((icon, input) => icon.remove());
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