Hash the hidden-sites PIN and slow down repeated wrong PINs

.jitsi-pin now holds "sha256:<hex>" (or NOPIN) instead of the digits.
The menu writes the hashed form. A plain-digits file still works and
is converted by the kiosk app at startup.

The PIN is now checked in the main process instead of the PIN pad's
renderer, using the lock screen's slowdown, now factored out
(createCodeThrottle): 5 wrong in a row refuse entry for 30s, doubling
up to 5 minutes. The F10 PIN pad and the WiFi screen share one count.
The WiFi screen's close-after-5-tries is replaced by this. The PIN pad
no longer shows the default PIN on screen, and the menu no longer
echoes the PIN as it's typed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm
This commit is contained in:
Claude
2026-10-10 15:33:05 +00:00
parent f215c64085
commit d6769d5207
4 changed files with 152 additions and 66 deletions
+33 -19
View File
@@ -78,20 +78,13 @@
<button class="btn" id="submit" onclick="submitPin()">✓ Submit</button>
<button class="btn" id="cancel" onclick="cancel()">✗ Cancel</button>
</div>
<div class="info">Default PIN: 1234 (4-8 digits)</div>
<div class="info">4-8 digits</div>
</div>
<script>
const {ipcRenderer} = require('electron');
const fs = require('fs');
const path = require('path');
const pinFile = path.join(__dirname, '.jitsi-pin');
let correctPin = '1234';
let enteredPin = '';
try {
const stored = fs.readFileSync(pinFile, 'utf8').trim();
if (stored !== 'NOPIN') correctPin = stored;
else correctPin = null;
} catch(e) {}
let blockedUntil = 0;
let blockTimer = null;
function updateDisplay() {
const display = document.getElementById('pin-display');
if (enteredPin.length === 0) {
@@ -103,6 +96,7 @@
}
}
function addDigit(digit) {
if (Date.now() < blockedUntil) return;
if (enteredPin.length < 8) {
enteredPin += digit;
updateDisplay();
@@ -111,20 +105,40 @@
}
function backspace() { enteredPin = enteredPin.slice(0, -1); updateDisplay(); }
function clearPin() { enteredPin = ''; updateDisplay(); document.getElementById('error').style.display = 'none'; }
function submitPin() {
function showError(text) {
document.getElementById('error').textContent = '❌ ' + text;
document.getElementById('error').style.display = 'block';
}
// Checked in the main process (the stored PIN is hashed), which also
// refuses input for a while after repeated wrong PINs.
async function submitPin() {
if (Date.now() < blockedUntil) return;
if (enteredPin.length < 4) {
document.getElementById('error').textContent = '❌ PIN must be 4-8 digits';
document.getElementById('error').style.display = 'block';
showError('PIN must be 4-8 digits');
return;
}
if (correctPin === null || enteredPin === correctPin) {
const r = await ipcRenderer.invoke('check-hidden-pin', enteredPin)
.catch(() => ({ok: false, error: 'PIN check failed'}));
enteredPin = '';
updateDisplay();
if (r.ok) {
ipcRenderer.send('pin-correct');
} else {
document.getElementById('error').textContent = '❌ Incorrect PIN';
document.getElementById('error').style.display = 'block';
enteredPin = '';
updateDisplay();
return;
}
if (!r.waitSeconds) { showError(r.error || 'Incorrect PIN'); return; }
blockedUntil = Date.now() + r.waitSeconds * 1000;
clearInterval(blockTimer);
const tick = () => {
const left = Math.ceil((blockedUntil - Date.now()) / 1000);
if (left <= 0) {
clearInterval(blockTimer);
document.getElementById('error').style.display = 'none';
return;
}
showError('Too many wrong tries - wait ' + left + 's');
};
tick();
blockTimer = setInterval(tick, 500);
}
function cancel() { ipcRenderer.send('pin-cancelled'); }
document.addEventListener('keydown', (e) => {