WiFi screen: Ctrl+Alt+Super+W, gated by the unlock PIN/password
The WiFi screen now opens with Ctrl+Alt+Super+W, which is easier to press than the five-key chord. The Shift+W+S+F+H chord and Ctrl+Alt+Shift+Super+W are removed. Fn isn't part of the combo: most keyboards never send it to the computer. With password protection on, the screen asks for the unlock code: the PIN pad for a PIN, a password field for a password. It shares the lock screen's wrong-code slowdown. With protection off, it falls back to the hidden-sites PIN as before. The lock screen's check now goes through the same checkCode() helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm
This commit is contained in:
@@ -495,17 +495,22 @@ Both can be used at the same time — they serve different purposes:
|
||||
- **Multi-method WiFi scan** - nmcli, iw, wpa_cli fallbacks
|
||||
- **Watchdog support** - Auto-revert bad WiFi configs
|
||||
- **Emergency hotspot** - Fallback if no internet
|
||||
- **WiFi from the kiosk screen** - hold **Shift+W+S+F+H** together (or press Ctrl+Alt+Shift+Super+W) to join a WiFi network without the admin console - see below
|
||||
- **WiFi from the kiosk screen** - press **Ctrl+Alt+Super+W** (Super = the Windows key) to join a WiFi network without the admin console, guarded by the kiosk's PIN/password - see below
|
||||
|
||||
### WiFi from the kiosk screen (consoles turned off)
|
||||
|
||||
With virtual consoles switched off, there's no way to reach the admin menu at the kiosk itself. To join a WiFi network anyway, plug in a keyboard and **hold Shift, W, S, F and H all at once** (or press Ctrl+Alt+Shift+Super+W). A WiFi screen opens:
|
||||
With virtual consoles switched off, there's no way to reach the admin menu at the kiosk itself. To join a WiFi network anyway, plug in a keyboard and press **Ctrl+Alt+Super+W** (Super = the Windows key). A WiFi screen opens:
|
||||
|
||||
1. Enter the hidden-sites PIN (the same one as F10 - default `1234`; skipped if the PIN is turned off).
|
||||
1. Enter the kiosk's code:
|
||||
- **Password protection on** (Core Settings → Password Protection & Lockout): the unlock **PIN**, or the unlock **password** if it's a password. Same slowdown after wrong tries as the lock screen.
|
||||
- **Password protection off:** the hidden-sites PIN (the F10 one - default `1234`).
|
||||
- Neither (protection off and the hidden-sites PIN turned off): no code is asked.
|
||||
2. Pick a network from the scan (strongest first, 🔒 = needs a password), or type its name.
|
||||
3. Type the password (empty for an open network) and press **Connect**.
|
||||
|
||||
It writes the same netplan file as Core Settings → WiFi (`/etc/netplan/60-kiosk-wifi.yaml`), so the connection persists across reboots. If the kiosk gets no address within 30 seconds (usually a wrong password) the previous WiFi setting is put back. Esc or Close shuts the screen; it also closes itself after 5 minutes untouched, and five wrong PINs close it. The keys only work as a chord (typing W-S-F-H one at a time does nothing), and they don't interfere with the tab or power shortcuts.
|
||||
It writes the same netplan file as Core Settings → WiFi (`/etc/netplan/60-kiosk-wifi.yaml`), so the connection persists across reboots. If the kiosk gets no address within 30 seconds (usually a wrong password) the previous WiFi setting is put back. Esc or Close shuts the screen; it also closes itself after 5 minutes untouched. After 5 wrong codes in a row, entry pauses (30s, doubling up to 5 min).
|
||||
|
||||
**About Fn:** the Fn key is handled inside most keyboards and never reaches the computer, so it isn't part of the combo - holding it too does no harm where Fn+W is still W. On compact keyboards where Fn+W is something else (often Up arrow), leave Fn out.
|
||||
|
||||
The root part is `/usr/local/bin/kiosk-wifi-helper` (status/scan/connect only), which the kiosk user may run via sudo through `/etc/sudoers.d/kiosk-wifi` - nothing else. Existing kiosks get it with Advanced → Upgrade.
|
||||
|
||||
@@ -905,8 +910,8 @@ The PIN file controls access to hidden sites (duration = -1):
|
||||
- **Disable PIN:** Set content to `NOPIN` to allow any entry
|
||||
- **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:<hash>`, 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).
|
||||
- **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 shares this count with the WiFi screen when that uses this PIN.
|
||||
- Also guards the WiFi screen (Ctrl+Alt+Super+W) when password protection is off - otherwise that uses the unlock PIN/password.
|
||||
|
||||
```bash
|
||||
# Set custom PIN (the menu does this for you)
|
||||
@@ -1533,7 +1538,7 @@ full migration pass.
|
||||
- **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: WiFi from the kiosk screen** - press Ctrl+Alt+Super+W to scan for and join a WiFi network right on the kiosk, guarded by the unlock PIN/password (or the hidden-sites PIN when password protection is off), 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.
|
||||
- **Offline addons:** the `--offline` bundle now also carries LMS (Lyrion) + Squeezelite, Asterisk Intercom, VNC, WireGuard and the Emergency Hotspot; their normal installs use it automatically when there's no internet.
|
||||
- **Emergency Hotspot fix:** by default it now starts only when the local network is unreachable (router *and* internet), not merely when there's no internet - on a LAN without internet it used to start on every boot. Existing setups keep the old check until Advanced → Emergency Hotspot → Reconfigure.
|
||||
|
||||
+47
-56
@@ -218,17 +218,24 @@ function hiddenPinRequired(){
|
||||
return!readHiddenPinEntry().none;
|
||||
}
|
||||
|
||||
function checkHiddenPin(pin){
|
||||
const blocked=throttleWaitSeconds(hiddenPinThrottle);
|
||||
// checkCode(THROTTLE, HASH, CODE, LABEL) - CODE against the stored SHA-256
|
||||
// HASH, with THROTTLE's slowdown. {ok} or {ok:false, error, waitSeconds?}.
|
||||
function checkCode(throttle,hash,code,label){
|
||||
const blocked=throttleWaitSeconds(throttle);
|
||||
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;
|
||||
if(sha256Hex(code)===hash){
|
||||
throttle.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'};
|
||||
const wait=throttleFail(throttle);
|
||||
console.log('[CODE] Wrong',label+', attempt',throttle.failures);
|
||||
return wait?{ok:false,waitSeconds:wait,error:'Too many wrong tries - wait '+wait+'s'}:{ok:false,error:'Incorrect '+label};
|
||||
}
|
||||
|
||||
function checkHiddenPin(pin){
|
||||
const entry=readHiddenPinEntry();
|
||||
if(entry.none)return{ok:true};
|
||||
return checkCode(hiddenPinThrottle,entry.hash,pin,'PIN');
|
||||
}
|
||||
|
||||
// Password lockout functions
|
||||
@@ -1197,10 +1204,11 @@ function showHiddenTab(index){
|
||||
showingHidden=true;
|
||||
}
|
||||
|
||||
// WiFi screen (hold Shift+W+S+F+H, or Ctrl+Alt+Shift+Super+W): join a WiFi network from the kiosk
|
||||
// WiFi screen (Ctrl+Alt+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-sites PIN
|
||||
// (checkHiddenPin - same PIN and slowdown as F10). The root
|
||||
// admin menu can't be reached. Gated by the kiosk's unlock code - the
|
||||
// lock screen's PIN or password, with its slowdown - or, when password
|
||||
// protection is off, the hidden-sites PIN (see wifiGate). 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';
|
||||
@@ -1257,7 +1265,7 @@ function showWifiWindow(){
|
||||
wifiWindow.focus();
|
||||
return;
|
||||
}
|
||||
wifiUnlocked=!hiddenPinRequired();
|
||||
wifiUnlocked=!wifiGate();
|
||||
let width=720,height=820;
|
||||
if(mainWindow&&!mainWindow.isDestroyed()){
|
||||
const[w,h]=mainWindow.getContentSize();
|
||||
@@ -1283,37 +1291,23 @@ function showWifiWindow(){
|
||||
console.log('[WIFI] WiFi screen opened');
|
||||
}
|
||||
|
||||
// Shift+W+S+F+H, all held together, opens the WiFi screen. A globalShortcut
|
||||
// can only have one non-modifier key, so this watches the raw key events
|
||||
// of every page/window instead (before-input-event): a key counts as held
|
||||
// from its keyDown (autorepeat keeps refreshing it) until its keyUp, or
|
||||
// for 1.5s if the keyUp never arrives (focus moved mid-press). Physical
|
||||
// keys (input.code), so it's the same keys on any keyboard layout. The
|
||||
// key presses that complete the chord are swallowed, not typed into the
|
||||
// page.
|
||||
const WIFI_CHORD_KEYS=['KeyW','KeyS','KeyF','KeyH'];
|
||||
const WIFI_CHORD_HOLD_MS=1500;
|
||||
const wifiChordDown=new Map();
|
||||
|
||||
function wifiChordInput(event,input){
|
||||
if(!WIFI_CHORD_KEYS.includes(input.code))return;
|
||||
if(input.type==='keyUp'){wifiChordDown.delete(input.code);return;}
|
||||
if(input.type!=='keyDown')return;
|
||||
const now=Date.now();
|
||||
wifiChordDown.set(input.code,now);
|
||||
if(!input.shift||input.control||input.alt||input.meta)return;
|
||||
const all=WIFI_CHORD_KEYS.every(k=>now-(wifiChordDown.get(k)||0)<WIFI_CHORD_HOLD_MS);
|
||||
if(!all)return;
|
||||
event.preventDefault();
|
||||
if(input.isAutoRepeat)return;
|
||||
wifiChordDown.clear();
|
||||
console.log('[WIFI] Shift+W+S+F+H pressed');
|
||||
showWifiWindow();
|
||||
// Which code guards the WiFi screen: the unlock code (PIN or password)
|
||||
// when password protection is on, else the hidden-sites PIN; null if
|
||||
// neither is set (hidden PIN turned off and no lock code).
|
||||
function wifiGate(){
|
||||
if(enablePasswordProtection&&lockoutPassword)return{kind:'lock',codeType:lockoutCodeType};
|
||||
if(hiddenPinRequired())return{kind:'hidden',codeType:'pin'};
|
||||
return null;
|
||||
}
|
||||
|
||||
app.on('web-contents-created',(e,contents)=>{
|
||||
contents.on('before-input-event',wifiChordInput);
|
||||
});
|
||||
function checkWifiCode(code){
|
||||
const gate=wifiGate();
|
||||
if(!gate)return{ok:true};
|
||||
if(gate.kind==='lock'){
|
||||
return checkCode(lockoutThrottle,lockoutPassword,code,gate.codeType==='pin'?'PIN':'password');
|
||||
}
|
||||
return checkHiddenPin(code);
|
||||
}
|
||||
|
||||
ipcMain.handle('wifi-request',async(event,req)=>{
|
||||
if(!wifiWindow||wifiWindow.isDestroyed()||event.sender!==wifiWindow.webContents){
|
||||
@@ -1322,10 +1316,13 @@ ipcMain.handle('wifi-request',async(event,req)=>{
|
||||
resetWifiIdleTimer();
|
||||
const action=req&&req.action;
|
||||
if(action==='close'){closeWifiWindow();return{ok:true};}
|
||||
if(action==='needs-pin')return{ok:true,needsPin:!wifiUnlocked};
|
||||
if(action==='needs-pin'){
|
||||
const gate=wifiGate();
|
||||
return{ok:true,needsPin:!wifiUnlocked,codeType:gate?gate.codeType:'pin'};
|
||||
}
|
||||
if(action==='unlock'){
|
||||
if(wifiUnlocked)return{ok:true};
|
||||
const result=checkHiddenPin(req.pin);
|
||||
const result=checkWifiCode(req.pin);
|
||||
if(result.ok)wifiUnlocked=true;
|
||||
return result;
|
||||
}
|
||||
@@ -1607,20 +1604,13 @@ async function createWindow(){
|
||||
// (see createCodeThrottle).
|
||||
ipcMain.on('check-lockout-password',(event,code)=>{
|
||||
if(!lockoutWindow||lockoutWindow.isDestroyed()||event.sender!==lockoutWindow.webContents)return;
|
||||
const blocked=throttleWaitSeconds(lockoutThrottle);
|
||||
if(blocked){
|
||||
event.sender.send('password-incorrect',{waitSeconds:blocked});
|
||||
return;
|
||||
}
|
||||
const expected=(lockoutMode==='boot'&&bootPassword)?bootPassword:lockoutPassword;
|
||||
if(sha256Hex(code)===expected){
|
||||
lockoutThrottle.failures=0;
|
||||
const result=checkCode(lockoutThrottle,expected,code,'code');
|
||||
if(result.ok){
|
||||
unlockScreen();
|
||||
return;
|
||||
}
|
||||
const waitSeconds=throttleFail(lockoutThrottle);
|
||||
console.log('[LOCKOUT] Wrong code, attempt',lockoutThrottle.failures);
|
||||
event.sender.send('password-incorrect',{waitSeconds});
|
||||
event.sender.send('password-incorrect',{waitSeconds:result.waitSeconds||0});
|
||||
});
|
||||
|
||||
ipcMain.handle('check-hidden-pin',(event,pin)=>{
|
||||
@@ -1798,9 +1788,10 @@ async function createWindow(){
|
||||
globalShortcut.register('Control+Alt+Delete',showPowerMenu);
|
||||
globalShortcut.register('Control+Alt+P',showPowerMenu);
|
||||
globalShortcut.register('Control+Alt+Escape',showPowerMenu);
|
||||
// Five keys on purpose - not something a visitor hits by accident.
|
||||
if(!globalShortcut.register('Control+Alt+Shift+Super+W',showWifiWindow)){
|
||||
console.error('[WIFI] Could not register Ctrl+Alt+Shift+Super+W');
|
||||
// WiFi screen - code-gated (wifiGate), so an easy-to-press combo is fine.
|
||||
// Fn isn't part of it: it never reaches the computer on most keyboards.
|
||||
if(!globalShortcut.register('Control+Alt+Super+W',showWifiWindow)){
|
||||
console.error('[WIFI] Could not register Ctrl+Alt+Super+W');
|
||||
}
|
||||
globalShortcut.register('Control+K',()=>{
|
||||
if(htmlKeyboardWindow&&!htmlKeyboardWindow.isDestroyed()){
|
||||
|
||||
+5
-1
@@ -82,7 +82,7 @@
|
||||
<h2>📶 WiFi</h2>
|
||||
|
||||
<div id="pin-step" class="hidden">
|
||||
<label for="pin">Enter the PIN</label>
|
||||
<label for="pin" id="pin-label">Enter the PIN</label>
|
||||
<input id="pin" type="password" inputmode="numeric" autocomplete="off">
|
||||
<div class="actions">
|
||||
<button class="primary" id="pin-ok">✓ OK</button>
|
||||
@@ -242,6 +242,10 @@
|
||||
|
||||
request('needs-pin').then(r => {
|
||||
if (r.ok && r.needsPin) {
|
||||
if (r.codeType === 'password') {
|
||||
$('pin-label').textContent = 'Enter the kiosk password';
|
||||
$('pin').removeAttribute('inputmode');
|
||||
}
|
||||
$('pin-step').classList.remove('hidden');
|
||||
$('pin').focus();
|
||||
} else {
|
||||
|
||||
+2
-2
@@ -221,7 +221,7 @@ provision_configure_display() {
|
||||
log_success "Display configured"
|
||||
}
|
||||
|
||||
# The kiosk app's WiFi screen (Ctrl+Alt+Shift+Super+W) - joining WiFi from
|
||||
# The kiosk app's WiFi screen (Ctrl+Alt+Super+W) - joining WiFi from
|
||||
# the kiosk itself when the console is switched off. The app runs as the
|
||||
# kiosk user; the netplan write is done by this one root helper, which
|
||||
# the kiosk user may run through sudo and nothing else (it only does
|
||||
@@ -239,7 +239,7 @@ provision_install_wifi_helper() {
|
||||
sudo mkdir -p "$SUDOERS_D_DIR"
|
||||
sudo install -m 0440 -o root -g root "$tmp" "$SUDOERS_D_DIR/kiosk-wifi"
|
||||
else
|
||||
log_warning "Couldn't set up the kiosk WiFi screen's permission - Ctrl+Alt+Shift+Super+W won't be able to connect"
|
||||
log_warning "Couldn't set up the kiosk WiFi screen's permission - Ctrl+Alt+Super+W won't be able to connect"
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
################################################################################
|
||||
# kiosk-wifi-helper - the root side of the kiosk app's WiFi screen
|
||||
# (Ctrl+Alt+Shift+Super+W), for joining a WiFi network from the kiosk
|
||||
# (Ctrl+Alt+Super+W), for joining a WiFi network from the kiosk
|
||||
# itself when the console (Ctrl+Alt+F1..F6) is switched off.
|
||||
#
|
||||
# The kiosk user may run this one script via passwordless sudo
|
||||
|
||||
Reference in New Issue
Block a user