diff --git a/Readme.md b/Readme.md index 30977ae..3075a09 100644 --- a/Readme.md +++ b/Readme.md @@ -903,11 +903,14 @@ The PIN file controls access to hidden sites (duration = -1): - **Default:** `1234` - **Configure via:** Main Menu → Core Settings → Sites → Configure Hidden Sites PIN - **Disable PIN:** Set content to `NOPIN` to allow any entry -- **Custom PIN:** 4-8 digits +- **Custom PIN:** 4-8 digits, independent of the lock screen's password/PIN (same or different, as you like) +- **Stored hashed:** the file holds `sha256:`, never the digits. A file with plain digits (set by hand, or from before this) still works and is converted to the hashed form when the kiosk app starts. +- **Wrong-PIN slowdown:** after 5 wrong PINs in a row, PIN entry is refused for 30 seconds, doubling up to 5 minutes. The hidden-sites PIN box and the WiFi screen share this count. +- Also guards the WiFi screen (Shift+W+S+F+H). ```bash -# Set custom PIN -echo "5678" | sudo -u kiosk tee /home/kiosk/kiosk-app/.jitsi-pin +# Set custom PIN (the menu does this for you) +echo "sha256:$(echo -n 5678 | sha256sum | cut -d' ' -f1)" | sudo -u kiosk tee /home/kiosk/kiosk-app/.jitsi-pin # Disable PIN protection echo "NOPIN" | sudo -u kiosk tee /home/kiosk/kiosk-app/.jitsi-pin @@ -1528,6 +1531,7 @@ full migration pass. - **Local files as sites:** an absolute path (`/home/kiosk/docs/menu.pdf`) shows a local page, PDF or image; a folder (`/home/kiosk/photos/`) plays as a full-screen **image slideshow** that picks up added/removed images within a minute. Works in both the Sites menu and the Web UI. - **Fix: the first-boot kiosk setup never started on 26.04.** Its service was ordered after cloud-init's final stage, which itself runs after `multi-user.target` - an ordering cycle that systemd broke by dropping the kiosk setup. It also "conflicted" with the tty1 login prompt, which systemd can resolve the same way. Both removed; checked with `systemd-analyze verify` against 26.04's own units. Rebuild the ISO to pick this up. - **WiFi tools in the base install:** `wpasupplicant` and `iw` are now installed (and bundled offline). A default Ubuntu Server install only has them if WiFi was set up in the installer; without them, Core Settings → WiFi couldn't connect an offline kiosk. The WiFi menu also installs them itself if missing. +- **Hidden-sites PIN hashed, with the same slowdown:** the F10 PIN is now stored as a SHA-256 hash (existing PIN files convert automatically) and repeated wrong PINs pause entry (30s, doubling to 5 min), shared with the WiFi screen. The PIN pad no longer shows "Default PIN: 1234" on screen. - **Lock screen PINs and a separate boot code:** the unlock code and the boot code can each be a password or a 4-8 digit PIN (on-screen number pad, works on touch screens), and the boot lock can have its own code. Repeated wrong codes now pause the lock screen (30s, doubling to 5 min). Set in Core Settings → Password Protection & Lockout or the Web UI's Lockout page. Existing settings keep working unchanged (password, same code at boot). - **New: WiFi from the kiosk screen** - hold Shift+W+S+F+H (or Ctrl+Alt+Shift+Super+W) to scan for and join a WiFi network right on the kiosk, PIN-protected, for kiosks with the consoles turned off. Reverts to the previous WiFi if the new one doesn't connect. See "WiFi from the kiosk screen". - **New: `iso/e2b-contig.sh`** - makes a file on an Easy2Boot (or any NTFS/FAT32/exFAT) USB drive contiguous from Linux, so ISOs that need to be in one piece boot. diff --git a/kiosk-app/main.js b/kiosk-app/main.js index 11cb9c3..b82f89c 100644 --- a/kiosk-app/main.js +++ b/kiosk-app/main.js @@ -73,8 +73,7 @@ let bootCodeType='password'; // Password lockout state let isLockedOut=false; let lockoutMode='unlock'; // 'boot' while the boot lock screen is up -let lockoutFailures=0; -let lockoutBlockedUntil=0; +const lockoutThrottle=createCodeThrottle(); let lockoutWindow=null; let lockoutTimer=null; let lockoutActivityTime=Date.now(); @@ -163,6 +162,75 @@ function markKeyboardActivity(){ keyboardClosePending=false; } +// Wrong-code slowdown, shared by the lock screen and the hidden-sites PIN +// (one throttle per code): after every 5 wrong codes in a row, input is +// refused for 30s, doubling up to 5 minutes - so a 4-digit PIN can't +// simply be tried through. A right code resets it. +function createCodeThrottle(){ + return{failures:0,blockedUntil:0}; +} +function throttleWaitSeconds(t){ + return Math.max(0,Math.ceil((t.blockedUntil-Date.now())/1000)); +} +// Counts a wrong code; returns the seconds input is now refused for (0 = none). +function throttleFail(t){ + t.failures++; + if(t.failures%5!==0)return 0; + const wait=Math.min(300,30*Math.pow(2,t.failures/5-1)); + t.blockedUntil=Date.now()+wait*1000; + return wait; +} + +function sha256Hex(v){ + return crypto.createHash('sha256').update(String(v==null?'':v)).digest('hex'); +} + +// Hidden-sites PIN (.jitsi-pin, set in Core Settings -> Hidden Sites PIN): +// "NOPIN" (no PIN), "sha256:", or - written before the PIN was +// hashed - the plain digits, which migrateHiddenPin() rewrites hashed at +// startup. No file = the default PIN 1234. Read on every check, so a +// change from the menu applies without a restart. Guards the hidden +// sites (F10) and the WiFi screen, with one shared slowdown. +const HIDDEN_PIN_FILE=path.join(__dirname,'.jitsi-pin'); +const hiddenPinThrottle=createCodeThrottle(); + +function readHiddenPinEntry(){ + try{ + const stored=fs.readFileSync(HIDDEN_PIN_FILE,'utf8').trim(); + if(stored==='NOPIN')return{none:true}; + if(stored.startsWith('sha256:'))return{hash:stored.slice(7)}; + if(stored)return{hash:sha256Hex(stored)}; + }catch(e){} + return{hash:sha256Hex('1234')}; +} + +function migrateHiddenPin(){ + try{ + const stored=fs.readFileSync(HIDDEN_PIN_FILE,'utf8').trim(); + if(/^[0-9]{4,8}$/.test(stored)){ + fs.writeFileSync(HIDDEN_PIN_FILE,'sha256:'+sha256Hex(stored)+'\n',{mode:0o600}); + console.log('[PIN] Hidden-sites PIN converted to hashed form'); + } + }catch(e){} +} + +function hiddenPinRequired(){ + return!readHiddenPinEntry().none; +} + +function checkHiddenPin(pin){ + const blocked=throttleWaitSeconds(hiddenPinThrottle); + if(blocked)return{ok:false,waitSeconds:blocked,error:'Too many wrong tries - wait '+blocked+'s'}; + const entry=readHiddenPinEntry(); + if(entry.none||sha256Hex(pin)===entry.hash){ + hiddenPinThrottle.failures=0; + return{ok:true}; + } + const wait=throttleFail(hiddenPinThrottle); + console.log('[PIN] Wrong hidden-sites PIN, attempt',hiddenPinThrottle.failures); + return wait?{ok:false,waitSeconds:wait,error:'Too many wrong tries - wait '+wait+'s'}:{ok:false,error:'Incorrect PIN'}; +} + // Password lockout functions function showLockoutScreen(mode){ if(isLockedOut||!enablePasswordProtection||!lockoutPassword)return; @@ -1131,23 +1199,13 @@ function showHiddenTab(index){ // WiFi screen (hold Shift+W+S+F+H, or Ctrl+Alt+Shift+Super+W): join a WiFi network from the kiosk // itself, for when the console (Ctrl+Alt+F1..F6) is switched off and the -// admin menu can't be reached. Gated by the hidden-tab PIN (.jitsi-pin, -// same rules as pin-entry.html: no file = 1234, NOPIN = none). The root +// admin menu can't be reached. Gated by the hidden-sites PIN +// (checkHiddenPin - same PIN and slowdown as F10). The root // work is done by /usr/local/bin/kiosk-wifi-helper, which the kiosk user // may run through sudo and nothing else (/etc/sudoers.d/kiosk-wifi). const WIFI_HELPER='/usr/local/bin/kiosk-wifi-helper'; const WIFI_IDLE_TIMEOUT=5*60000; // closes after 5 minutes untouched -const WIFI_MAX_PIN_TRIES=5; -let wifiWindow=null,wifiUnlocked=false,wifiPinTries=0,wifiIdleTimer=null; - -function readHiddenPin(){ - try{ - const stored=fs.readFileSync(path.join(__dirname,'.jitsi-pin'),'utf8').trim(); - if(stored==='NOPIN')return null; - if(stored)return stored; - }catch(e){} - return '1234'; -} +let wifiWindow=null,wifiUnlocked=false,wifiIdleTimer=null; function runWifiHelper(action,input){ return new Promise(resolve=>{ @@ -1199,8 +1257,7 @@ function showWifiWindow(){ wifiWindow.focus(); return; } - wifiUnlocked=(readHiddenPin()===null); - wifiPinTries=0; + wifiUnlocked=!hiddenPinRequired(); let width=720,height=820; if(mainWindow&&!mainWindow.isDestroyed()){ const[w,h]=mainWindow.getContentSize(); @@ -1268,17 +1325,9 @@ ipcMain.handle('wifi-request',async(event,req)=>{ if(action==='needs-pin')return{ok:true,needsPin:!wifiUnlocked}; if(action==='unlock'){ if(wifiUnlocked)return{ok:true}; - if(String(req.pin||'')===readHiddenPin()){ - wifiUnlocked=true; - return{ok:true}; - } - wifiPinTries++; - console.log('[WIFI] Wrong PIN, attempt',wifiPinTries); - if(wifiPinTries>=WIFI_MAX_PIN_TRIES){ - setTimeout(closeWifiWindow,1500); - return{ok:false,error:'Too many wrong PINs'}; - } - return{ok:false,error:'Incorrect PIN'}; + const result=checkHiddenPin(req.pin); + if(result.ok)wifiUnlocked=true; + return result; } if(!wifiUnlocked)return{ok:false,error:'Enter the PIN first'}; if(action==='status'||action==='scan')return runWifiHelper(action); @@ -1394,6 +1443,7 @@ async function autheliaAuthenticate(){ async function createWindow(){ tabs=loadConfig(); + migrateHiddenPin(); await autheliaAuthenticate(); mainWindow=new BrowserWindow({ @@ -1552,29 +1602,32 @@ async function createWindow(){ // hash (how the menu and Web UI store it). After every 5 wrong codes in // a row the screen refuses input for a while - 30s, doubling up to 5 // minutes - so a 4-digit PIN can't simply be tried through. + // The lock screen sends the code as typed; compared here as a SHA-256 + // hash (how the menu and Web UI store it), with the wrong-code slowdown + // (see createCodeThrottle). ipcMain.on('check-lockout-password',(event,code)=>{ if(!lockoutWindow||lockoutWindow.isDestroyed()||event.sender!==lockoutWindow.webContents)return; - if(Date.now(){ + if(!pinWindow||pinWindow.isDestroyed()||event.sender!==pinWindow.webContents)return{ok:false,error:'Not allowed'}; + return checkHiddenPin(pin); + }); + ipcMain.on('get-config',(event)=>{ try{ if(fs.existsSync(CONFIG_FILE)){ diff --git a/kiosk-app/pin-entry.html b/kiosk-app/pin-entry.html index f638219..120565a 100644 --- a/kiosk-app/pin-entry.html +++ b/kiosk-app/pin-entry.html @@ -78,20 +78,13 @@ -
Default PIN: 1234 (4-8 digits)
+
4-8 digits