Add GamepadAPI, WebGL readPixels noise, auto-prune, import/export, badge

New fingerprint vectors:
- Gamepad API: getGamepads() returns empty (prevents controller fingerprinting)
- WebGL readPixels: seeded pixel noise on framebuffer reads

New features:
- Auto-prune: configurable automatic removal of inactive containers
- Import/export: backup and restore all settings from options page
- Toolbar badge: shows active container count
- CHANGELOG.md: version history

Bumped to v0.5.0 with 20 spoofed fingerprint vectors.
This commit is contained in:
sal
2026-03-04 21:08:45 -06:00
parent bbe40f87dc
commit d6dabb2646
9 changed files with 352 additions and 4 deletions

View File

@@ -200,6 +200,18 @@
<div class="section-body" id="clienthints-results"></div>
</div>
<!-- Gamepad API -->
<div class="section" id="sec-gamepad">
<div class="section-header"><span class="status-dot pending" id="dot-gamepad"></span><h2>Gamepad API</h2></div>
<div class="section-body" id="gamepad-results"></div>
</div>
<!-- WebGL readPixels -->
<div class="section" id="sec-readpixels">
<div class="section-header"><span class="status-dot pending" id="dot-readpixels"></span><h2>WebGL readPixels</h2></div>
<div class="section-body" id="readpixels-results"></div>
</div>
<button class="copy-btn" onclick="copyReport()">Copy Full Report to Clipboard</button>
<canvas id="test-canvas" width="300" height="60"></canvas>
@@ -981,6 +993,60 @@ async function testClientHints() {
}
}
// ── Gamepad API ──
function testGamepad() {
const el = document.getElementById("gamepad-results");
if (!navigator.getGamepads) {
report.gamepad = { status: "API not available" };
el.innerHTML = row("Status", "navigator.getGamepads not available", "");
sectionStatus.gamepad = "warn";
setDot("dot-gamepad", "warn");
return;
}
const gamepads = navigator.getGamepads();
const count = Array.from(gamepads).filter(g => g !== null).length;
report.gamepad = { totalSlots: gamepads.length, connectedCount: count };
el.innerHTML = row("Gamepad slots", gamepads.length) +
row("Connected gamepads", count, count === 0 ? "spoofed" : "real");
sectionStatus.gamepad = count === 0 ? "pass" : "warn";
setDot("dot-gamepad", sectionStatus.gamepad);
}
// ── WebGL readPixels ──
async function testReadPixels() {
const el = document.getElementById("readpixels-results");
const c = document.createElement("canvas");
c.width = 64;
c.height = 64;
const gl = c.getContext("webgl");
if (!gl) {
report.readpixels = { status: "WebGL not available" };
el.innerHTML = row("Status", "WebGL not available", "");
sectionStatus.readpixels = "warn";
setDot("dot-readpixels", "warn");
return;
}
// Draw something
gl.clearColor(0.5, 0.3, 0.7, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
const pixels = new Uint8Array(64 * 64 * 4);
gl.readPixels(0, 0, 64, 64, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
// Hash the pixel data
let sum = 0;
for (let i = 0; i < pixels.length; i++) sum += pixels[i];
const hash = await sha256Short(sum.toString());
report.readpixels = { pixelSum: sum, hash };
el.innerHTML = row("readPixels hash", hash) +
row("Pixel sum", sum);
sectionStatus.readpixels = "pass";
setDot("dot-readpixels", "pass");
}
// ── Summary ──
function updateSummary() {
let pass = 0, fail = 0, warn = 0;
@@ -1060,6 +1126,7 @@ async function runAll() {
try { testPerf(); } catch(e) { console.error("perf test:", e); }
try { testFontDOM(); } catch(e) { console.error("fontdom test:", e); }
try { testDocFonts(); } catch(e) { console.error("docfonts test:", e); }
try { testGamepad(); } catch(e) { console.error("gamepad test:", e); }
// Async tests — run in parallel with individual error handling
await Promise.allSettled([
@@ -1070,6 +1137,7 @@ async function runAll() {
testHeaders(),
testStorage(),
testClientHints(),
testReadPixels(),
]);
updateSummary();