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
+7 -3
View File
@@ -903,11 +903,14 @@ The PIN file controls access to hidden sites (duration = -1):
- **Default:** `1234` - **Default:** `1234`
- **Configure via:** Main Menu → Core Settings → Sites → Configure Hidden Sites PIN - **Configure via:** Main Menu → Core Settings → Sites → Configure Hidden Sites PIN
- **Disable PIN:** Set content to `NOPIN` to allow any entry - **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:<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).
```bash ```bash
# Set custom PIN # Set custom PIN (the menu does this for you)
echo "5678" | sudo -u kiosk tee /home/kiosk/kiosk-app/.jitsi-pin echo "sha256:$(echo -n 5678 | sha256sum | cut -d' ' -f1)" | sudo -u kiosk tee /home/kiosk/kiosk-app/.jitsi-pin
# Disable PIN protection # Disable PIN protection
echo "NOPIN" | sudo -u kiosk tee /home/kiosk/kiosk-app/.jitsi-pin 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. - **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. - **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. - **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). - **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** - 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. - **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.
+93 -40
View File
@@ -73,8 +73,7 @@ let bootCodeType='password';
// Password lockout state // Password lockout state
let isLockedOut=false; let isLockedOut=false;
let lockoutMode='unlock'; // 'boot' while the boot lock screen is up let lockoutMode='unlock'; // 'boot' while the boot lock screen is up
let lockoutFailures=0; const lockoutThrottle=createCodeThrottle();
let lockoutBlockedUntil=0;
let lockoutWindow=null; let lockoutWindow=null;
let lockoutTimer=null; let lockoutTimer=null;
let lockoutActivityTime=Date.now(); let lockoutActivityTime=Date.now();
@@ -163,6 +162,75 @@ function markKeyboardActivity(){
keyboardClosePending=false; 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:<hex>", 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 // Password lockout functions
function showLockoutScreen(mode){ function showLockoutScreen(mode){
if(isLockedOut||!enablePasswordProtection||!lockoutPassword)return; 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 // 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 // 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, // admin menu can't be reached. Gated by the hidden-sites PIN
// same rules as pin-entry.html: no file = 1234, NOPIN = none). The root // (checkHiddenPin - same PIN and slowdown as F10). The root
// work is done by /usr/local/bin/kiosk-wifi-helper, which the kiosk user // 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). // may run through sudo and nothing else (/etc/sudoers.d/kiosk-wifi).
const WIFI_HELPER='/usr/local/bin/kiosk-wifi-helper'; const WIFI_HELPER='/usr/local/bin/kiosk-wifi-helper';
const WIFI_IDLE_TIMEOUT=5*60000; // closes after 5 minutes untouched const WIFI_IDLE_TIMEOUT=5*60000; // closes after 5 minutes untouched
const WIFI_MAX_PIN_TRIES=5; let wifiWindow=null,wifiUnlocked=false,wifiIdleTimer=null;
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';
}
function runWifiHelper(action,input){ function runWifiHelper(action,input){
return new Promise(resolve=>{ return new Promise(resolve=>{
@@ -1199,8 +1257,7 @@ function showWifiWindow(){
wifiWindow.focus(); wifiWindow.focus();
return; return;
} }
wifiUnlocked=(readHiddenPin()===null); wifiUnlocked=!hiddenPinRequired();
wifiPinTries=0;
let width=720,height=820; let width=720,height=820;
if(mainWindow&&!mainWindow.isDestroyed()){ if(mainWindow&&!mainWindow.isDestroyed()){
const[w,h]=mainWindow.getContentSize(); 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==='needs-pin')return{ok:true,needsPin:!wifiUnlocked};
if(action==='unlock'){ if(action==='unlock'){
if(wifiUnlocked)return{ok:true}; if(wifiUnlocked)return{ok:true};
if(String(req.pin||'')===readHiddenPin()){ const result=checkHiddenPin(req.pin);
wifiUnlocked=true; if(result.ok)wifiUnlocked=true;
return{ok:true}; return result;
}
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'};
} }
if(!wifiUnlocked)return{ok:false,error:'Enter the PIN first'}; if(!wifiUnlocked)return{ok:false,error:'Enter the PIN first'};
if(action==='status'||action==='scan')return runWifiHelper(action); if(action==='status'||action==='scan')return runWifiHelper(action);
@@ -1394,6 +1443,7 @@ async function autheliaAuthenticate(){
async function createWindow(){ async function createWindow(){
tabs=loadConfig(); tabs=loadConfig();
migrateHiddenPin();
await autheliaAuthenticate(); await autheliaAuthenticate();
mainWindow=new BrowserWindow({ 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 // 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 // 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. // 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)=>{ ipcMain.on('check-lockout-password',(event,code)=>{
if(!lockoutWindow||lockoutWindow.isDestroyed()||event.sender!==lockoutWindow.webContents)return; if(!lockoutWindow||lockoutWindow.isDestroyed()||event.sender!==lockoutWindow.webContents)return;
if(Date.now()<lockoutBlockedUntil){ const blocked=throttleWaitSeconds(lockoutThrottle);
event.sender.send('password-incorrect',{waitSeconds:Math.ceil((lockoutBlockedUntil-Date.now())/1000)}); if(blocked){
event.sender.send('password-incorrect',{waitSeconds:blocked});
return; return;
} }
const expected=(lockoutMode==='boot'&&bootPassword)?bootPassword:lockoutPassword; const expected=(lockoutMode==='boot'&&bootPassword)?bootPassword:lockoutPassword;
const hash=crypto.createHash('sha256').update(String(code||'')).digest('hex'); if(sha256Hex(code)===expected){
if(hash===expected){ lockoutThrottle.failures=0;
lockoutFailures=0;
unlockScreen(); unlockScreen();
return; return;
} }
lockoutFailures++; const waitSeconds=throttleFail(lockoutThrottle);
console.log('[LOCKOUT] Wrong code, attempt',lockoutFailures); console.log('[LOCKOUT] Wrong code, attempt',lockoutThrottle.failures);
let waitSeconds=0;
if(lockoutFailures%5===0){
waitSeconds=Math.min(300,30*Math.pow(2,lockoutFailures/5-1));
lockoutBlockedUntil=Date.now()+waitSeconds*1000;
}
event.sender.send('password-incorrect',{waitSeconds}); event.sender.send('password-incorrect',{waitSeconds});
}); });
ipcMain.handle('check-hidden-pin',(event,pin)=>{
if(!pinWindow||pinWindow.isDestroyed()||event.sender!==pinWindow.webContents)return{ok:false,error:'Not allowed'};
return checkHiddenPin(pin);
});
ipcMain.on('get-config',(event)=>{ ipcMain.on('get-config',(event)=>{
try{ try{
if(fs.existsSync(CONFIG_FILE)){ if(fs.existsSync(CONFIG_FILE)){
+32 -18
View File
@@ -78,20 +78,13 @@
<button class="btn" id="submit" onclick="submitPin()">✓ Submit</button> <button class="btn" id="submit" onclick="submitPin()">✓ Submit</button>
<button class="btn" id="cancel" onclick="cancel()">✗ Cancel</button> <button class="btn" id="cancel" onclick="cancel()">✗ Cancel</button>
</div> </div>
<div class="info">Default PIN: 1234 (4-8 digits)</div> <div class="info">4-8 digits</div>
</div> </div>
<script> <script>
const {ipcRenderer} = require('electron'); const {ipcRenderer} = require('electron');
const fs = require('fs');
const path = require('path');
const pinFile = path.join(__dirname, '.jitsi-pin');
let correctPin = '1234';
let enteredPin = ''; let enteredPin = '';
try { let blockedUntil = 0;
const stored = fs.readFileSync(pinFile, 'utf8').trim(); let blockTimer = null;
if (stored !== 'NOPIN') correctPin = stored;
else correctPin = null;
} catch(e) {}
function updateDisplay() { function updateDisplay() {
const display = document.getElementById('pin-display'); const display = document.getElementById('pin-display');
if (enteredPin.length === 0) { if (enteredPin.length === 0) {
@@ -103,6 +96,7 @@
} }
} }
function addDigit(digit) { function addDigit(digit) {
if (Date.now() < blockedUntil) return;
if (enteredPin.length < 8) { if (enteredPin.length < 8) {
enteredPin += digit; enteredPin += digit;
updateDisplay(); updateDisplay();
@@ -111,20 +105,40 @@
} }
function backspace() { enteredPin = enteredPin.slice(0, -1); updateDisplay(); } function backspace() { enteredPin = enteredPin.slice(0, -1); updateDisplay(); }
function clearPin() { enteredPin = ''; updateDisplay(); document.getElementById('error').style.display = 'none'; } function clearPin() { enteredPin = ''; updateDisplay(); document.getElementById('error').style.display = 'none'; }
function submitPin() { function showError(text) {
if (enteredPin.length < 4) { document.getElementById('error').textContent = '❌ ' + text;
document.getElementById('error').textContent = '❌ PIN must be 4-8 digits';
document.getElementById('error').style.display = 'block'; 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) {
showError('PIN must be 4-8 digits');
return; return;
} }
if (correctPin === null || enteredPin === correctPin) { const r = await ipcRenderer.invoke('check-hidden-pin', enteredPin)
ipcRenderer.send('pin-correct'); .catch(() => ({ok: false, error: 'PIN check failed'}));
} else {
document.getElementById('error').textContent = '❌ Incorrect PIN';
document.getElementById('error').style.display = 'block';
enteredPin = ''; enteredPin = '';
updateDisplay(); updateDisplay();
if (r.ok) {
ipcRenderer.send('pin-correct');
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'); } function cancel() { ipcRenderer.send('pin-cancelled'); }
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
+19 -4
View File
@@ -6,6 +6,12 @@
# flat PIN file rather than config.json - a fourth shape for the framework # flat PIN file rather than config.json - a fourth shape for the framework
# to prove out (plain file, not JSON at all). # to prove out (plain file, not JSON at all).
# #
# The file holds "NOPIN", or "sha256:<hex>" - the PIN is never stored as
# plain digits (a file from before that is converted by the kiosk app at
# startup; plain digits are still accepted until then). No file = the
# default PIN 1234. The kiosk app reads it on every check, so changes
# apply immediately, and slows down repeated wrong guesses.
#
# Depends on: lib/menu.sh, lib/config.sh being sourced first. # Depends on: lib/menu.sh, lib/config.sh being sourced first.
################################################################################ ################################################################################
@@ -22,8 +28,10 @@ hidden_pin_status() {
current_pin=$(sudo -u "$KIOSK_USER" cat "$pin_file" 2>/dev/null) current_pin=$(sudo -u "$KIOSK_USER" cat "$pin_file" 2>/dev/null)
if [[ "$current_pin" == "NOPIN" ]]; then if [[ "$current_pin" == "NOPIN" ]]; then
echo "Current: no PIN (hidden pages open to anyone)" echo "Current: no PIN (hidden pages open to anyone)"
elif [[ "$current_pin" == "sha256:$(hidden_pin_hash 1234)" || "$current_pin" == "1234" ]]; then
echo "Current: default PIN (1234) - set your own"
else else
echo "Current: PIN set (${#current_pin} digits)" echo "Current: PIN set"
fi fi
else else
echo "Current: not configured (default: 1234)" echo "Current: not configured (default: 1234)"
@@ -43,29 +51,36 @@ hidden_pin_menu() {
# Actions # Actions
################################################################################ ################################################################################
hidden_pin_hash() {
echo -n "$1" | sha256sum | cut -d' ' -f1
}
# write_pin VALUE - VALUE is "NOPIN" or the PIN digits (stored hashed).
write_pin() { write_pin() {
local value="$1" local value="$1"
local pin_file local pin_file
pin_file=$(hidden_pin_file) pin_file=$(hidden_pin_file)
[[ "$value" == "NOPIN" ]] || value="sha256:$(hidden_pin_hash "$value")"
sudo mkdir -p "$KIOSK_DIR" sudo mkdir -p "$KIOSK_DIR"
echo "$value" | sudo -u "$KIOSK_USER" tee "$pin_file" > /dev/null echo "$value" | sudo -u "$KIOSK_USER" tee "$pin_file" > /dev/null
sudo -u "$KIOSK_USER" chmod 600 "$pin_file" sudo -u "$KIOSK_USER" chmod 600 "$pin_file"
log_warning "Restart the kiosk display for this to take effect"
} }
action_set_pin() { action_set_pin() {
echo echo
local new_pin confirm_pin local new_pin confirm_pin
while true; do while true; do
read -r -p "Enter new PIN (4-8 digits): " new_pin read -r -s -p "Enter new PIN (4-8 digits): " new_pin
echo
if [[ ! "$new_pin" =~ ^[0-9]{4,8}$ ]]; then if [[ ! "$new_pin" =~ ^[0-9]{4,8}$ ]]; then
echo "❌ PIN must be 4-8 digits" echo "❌ PIN must be 4-8 digits"
continue continue
fi fi
read -r -p "Confirm PIN: " confirm_pin read -r -s -p "Confirm PIN: " confirm_pin
echo
if [[ "$new_pin" == "$confirm_pin" ]]; then if [[ "$new_pin" == "$confirm_pin" ]]; then
write_pin "$new_pin" write_pin "$new_pin"