feat: in-page detection with overlay UI and 5-char signatures

This commit is contained in:
2026-09-12 17:46:28 -07:00
parent 94252f010b
commit 652d55cdd7
14 changed files with 445 additions and 61 deletions

215
content.js Normal file
View File

@@ -0,0 +1,215 @@
import zwus from 'zwus';
import * as speck48_96ctr from './speck48_96ctr.js';
import * as speck32_64ecb from './speck32_64ecb.js';
import { SIG_PREFIX, parseSig, getPayloadEnd } from './sig.js';
let host, shadow;
const active = new Set();
function initShadow() {
if (host) return;
host = document.createElement('div');
host.id = 'in0-host';
shadow = host.attachShadow({ mode: 'open' });
const s = document.createElement('style');
s.textContent = `
.in0-wrap {
position: absolute;
pointer-events: auto;
z-index: 2147483647;
display: flex;
flex-direction: column;
align-items: center;
font-family: system-ui, -apple-system, sans-serif;
}
.in0-btn {
background: #18191c;
color: #fff;
border: 1px solid #3a3b40;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
padding: 4px 10px;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
position: relative;
white-space: nowrap;
}
.in0-btn:hover {
border-color: #00b4d8;
box-shadow: 0 0 8px rgba(0,180,216,0.4);
}
.in0-badge {
position: absolute;
top: -6px;
right: -6px;
width: 15px;
height: 15px;
border-radius: 50%;
background: #00b4d8;
color: #fff;
font-size: 10px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.in0-arrow {
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 6px solid #18191c;
margin-top: -1px;
}
`;
shadow.appendChild(s);
(document.body || document.documentElement).appendChild(host);
}
function createOverlay(node, parsed, start, end) {
initShadow();
const wrap = document.createElement('div');
wrap.className = 'in0-wrap';
const btn = document.createElement('button');
btn.className = 'in0-btn';
btn.textContent = parsed.cipher === 'PLAIN' ? 'Decode' : 'Decrypt';
const badge = document.createElement('span');
badge.className = 'in0-badge';
badge.textContent = '\u00D8';
btn.appendChild(badge);
const arrow = document.createElement('div');
arrow.className = 'in0-arrow';
wrap.appendChild(btn);
wrap.appendChild(arrow);
shadow.appendChild(wrap);
const entry = { wrap, node, start, end };
active.add(entry);
btn.onclick = () => onAction(entry, parsed);
updatePos(entry);
}
function updatePos(entry) {
const { wrap, node, start, end } = entry;
if (!node.isConnected) {
wrap.remove();
active.delete(entry);
return;
}
const r = document.createRange();
try {
r.setStart(node, start);
r.setEnd(node, Math.min(end, node.nodeValue.length));
} catch {
wrap.remove();
active.delete(entry);
return;
}
let rect = r.getBoundingClientRect();
if (!rect.width && !rect.height && node.parentElement) {
rect = node.parentElement.getBoundingClientRect();
}
const x = rect.left + window.scrollX + (rect.width || 0) / 2;
const y = rect.top + window.scrollY;
wrap.style.left = `${x - wrap.offsetWidth / 2}px`;
wrap.style.top = `${y - wrap.offsetHeight - 2}px`;
}
function onAction(entry, parsed) {
const { wrap, node, start, end } = entry;
const rawPayload = node.nodeValue.slice(start + parsed.sigLen, end);
let decoded = '';
if (parsed.cipher === 'PLAIN') {
try {
decoded = zwus.decodeToString(rawPayload, parsed.base);
} catch (e) {
console.error(e);
}
} else {
const pass = prompt(`in\u00D8sight: enter password (${parsed.cipher}):`);
if (!pass) return;
try {
const arr = zwus.decodeToNumberArray(rawPayload, parsed.base);
if (parsed.cipher === 'SPECK48_96CTR')
decoded = speck48_96ctr.decrypt(arr, speck48_96ctr.getKey(pass));
else if (parsed.cipher === 'SPECK32_64ECB (insecure)')
decoded = speck32_64ecb.decrypt(arr, speck32_64ecb.getKey(pass));
} catch (e) {
console.error(e);
}
if (!decoded) {
alert('Decryption failed.');
return;
}
}
if (decoded) {
const r = document.createRange();
r.setStart(node, start);
r.setEnd(node, end);
r.deleteContents();
const span = document.createElement('span');
span.className = 'inzerosight-decoded';
span.style.color = '#00b4d8';
span.style.fontWeight = '600';
span.textContent = ` ${decoded} `;
r.insertNode(span);
wrap.remove();
active.delete(entry);
}
}
const scanned = new WeakSet();
function scanNode(node) {
if (!node || node.nodeType !== Node.TEXT_NODE || scanned.has(node)) return;
const val = node.nodeValue;
if (!val || !val.includes(SIG_PREFIX)) return;
scanned.add(node);
let idx = 0;
while (idx < val.length) {
const sub = val.slice(idx);
const p = parseSig(sub);
if (!p) break;
const start = idx + p.sigIdx;
const end = getPayloadEnd(val, p.base, start + p.sigLen);
createOverlay(node, p, start, end);
idx = end + 1;
}
}
function scanTree(root) {
if (!root) return;
const ign = { SCRIPT: 1, STYLE: 1, TEXTAREA: 1, INPUT: 1, NOSCRIPT: 1, 'IN0-HOST': 1 };
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
acceptNode: n => (ign[n.parentElement?.tagName] ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT)
});
let n;
while ((n = walker.nextNode())) scanNode(n);
}
scanTree(document.body);
const obs = new MutationObserver(muts => {
for (const m of muts) {
if (m.type === 'characterData') scanNode(m.target);
else for (const an of m.addedNodes) {
if (an.nodeType === Node.TEXT_NODE) scanNode(an);
else if (an.nodeType === Node.ELEMENT_NODE && an.id !== 'in0-host') scanTree(an);
}
}
});
obs.observe(document.documentElement, { childList: true, subtree: true, characterData: true });
window.addEventListener('scroll', () => active.forEach(updatePos), { passive: true });
window.addEventListener('resize', () => active.forEach(updatePos), { passive: true });