Add WebAuthn passkey biometric unlock + dark toggle on auth screen

- Dark/light theme toggle now always visible (moved outside auth/vault sections)
- New passkey_challenges and passkey_credentials tables
- /passkey/register/begin + /passkey/register/complete endpoints
- /passkey/login/begin + /passkey/login/complete endpoints
- CBOR decoder + COSE key parser for WebAuthn attestation/assertion
- ES256 (P-256) signature verification via OpenSSL
- Client-side: register passkey button in settings, passkey login on auth screen
- First passkey login prompts for master password once to derive AES-GCM key
- Passkey login requires username input before biometric prompt
This commit is contained in:
2026-05-09 13:01:59 +01:00
parent 8dd2fc71cf
commit 367b85e991
3 changed files with 288 additions and 3 deletions
+187
View File
@@ -90,9 +90,28 @@ try { $db->exec("ALTER TABLE users ADD COLUMN hash_algo TEXT DEFAULT 'pbkdf2'");
try { $db->exec("ALTER TABLE sessions ADD COLUMN csrf_token TEXT"); } catch (Exception $e) {} try { $db->exec("ALTER TABLE sessions ADD COLUMN csrf_token TEXT"); } catch (Exception $e) {}
try { $db->exec("ALTER TABLE vault_entries ADD COLUMN favorite INTEGER DEFAULT 0"); } catch (Exception $e) {} try { $db->exec("ALTER TABLE vault_entries ADD COLUMN favorite INTEGER DEFAULT 0"); } catch (Exception $e) {}
$db->exec("
CREATE TABLE IF NOT EXISTS passkey_challenges (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
challenge BLOB NOT NULL,
type TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS passkey_credentials (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
credential_id BLOB NOT NULL UNIQUE,
public_key BLOB NOT NULL,
counter INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
");
$db->exec("DELETE FROM sessions WHERE expires_at < datetime('now')"); $db->exec("DELETE FROM sessions WHERE expires_at < datetime('now')");
$db->exec("DELETE FROM login_attempts WHERE attempted_at < datetime('now', '-15 minutes')"); $db->exec("DELETE FROM login_attempts WHERE attempted_at < datetime('now', '-15 minutes')");
$db->exec("DELETE FROM audit_log WHERE created_at < datetime('now', '-30 days')"); $db->exec("DELETE FROM audit_log WHERE created_at < datetime('now', '-30 days')");
$db->exec("DELETE FROM passkey_challenges WHERE created_at < datetime('now', '-10 minutes')");
function getClientIP() { function getClientIP() {
$headers = getallheaders(); $headers = getallheaders();
@@ -145,6 +164,51 @@ function requireCSRF($db, $userId) {
} }
} }
// ==================== WebAuthn helpers ====================
function b64url_encode($d) { return rtrim(strtr(base64_encode($d), '+/', '-_'), '='); }
function b64url_decode($d) { return base64_decode(strtr($d, '-_', '+/')); }
function cbor_decode(&$d, &$o=0) {
$b = ord($d[$o]); $m = ($b>>5)&7; $a = $b&0x1f; $o++;
if ($a < 24) $v = $a;
elseif ($a === 24) { $v = ord($d[$o]); $o++; }
elseif ($a === 25) { $v = unpack('n', substr($d,$o,2))[1]; $o+=2; }
elseif ($a === 26) { $v = unpack('N', substr($d,$o,4))[1]; $o+=4; }
elseif ($a === 27) { $v = unpack('J', substr($d,$o,8))[1]; $o+=8; }
else throw new Exception('CBOR: unsupported additional info');
switch ($m) {
case 0: return $v;
case 1: return -1-$v;
case 2: $s = substr($d,$o,$v); $o+=$v; return $s;
case 3: $s = substr($d,$o,$v); $o+=$v; return $s;
case 4: $r=[]; for($i=0;$i<$v;$i++) $r[]=cbor_decode($d,$o); return $r;
case 5: $r=[]; for($i=0;$i<$v;$i++){$k=cbor_decode($d,$o); $r[$k]=cbor_decode($d,$o);} return $r;
case 7: if($a===20)return false; if($a===21)return true; if($a===22)return null; return $v;
default: throw new Exception('CBOR: unsupported major type '.$m);
}
}
function derLen($l) {
if ($l < 128) return chr($l);
$b = ''; while ($l > 0) { $b = chr($l&0xff).$b; $l>>=8; }
return chr(0x80|strlen($b)).$b;
}
function coseToPem($key) {
$alg = $key[3] ?? 0;
if ($alg !== -7) return null; // only ES256
$x = $key[-2] ?? ''; $y = $key[-3] ?? '';
if (strlen($x) !== 32 || strlen($y) !== 32) return null;
$point = "\x04".$x.$y;
$algId = "\x06\x07\x2a\x86\x48\xce\x3d\x02\x01". // ecPublicKey OID
"\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07"; // P-256 OID
$bs = "\x03".derLen(strlen($point)+1)."\x00".$point;
$inner = "\x30".derLen(strlen($algId)).$algId;
$outer = $inner.$bs;
$der = "\x30".derLen(strlen($outer)).$outer;
return "-----BEGIN PUBLIC KEY-----\n".chunk_split(base64_encode($der), 64, "\n")."-----END PUBLIC KEY-----";
}
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$path = str_replace('/password-manager/api.php', '', $path); $path = str_replace('/password-manager/api.php', '', $path);
$path = str_replace('/api.php', '', $path); $path = str_replace('/api.php', '', $path);
@@ -463,6 +527,129 @@ try {
echo json_encode(['message'=>'OK']); echo json_encode(['message'=>'OK']);
break; break;
// ==================== WebAuthn passkey ====================
case ($path === '/passkey/register/begin' && $method === 'POST'):
$auth = authenticate($db);
requireCSRF($db, $auth['userId']);
$challenge = random_bytes(32);
$stC = $db->prepare('INSERT INTO passkey_challenges (user_id, challenge, type) VALUES (:uid, :ch, :t)');
$stC->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER);
$stC->bindValue(':ch', $challenge, SQLITE3_BLOB);
$stC->bindValue(':t', 'register', SQLITE3_TEXT);
$stC->execute();
$stU = $db->prepare('SELECT username FROM users WHERE id=:uid');
$stU->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER);
$uRow = $stU->execute()->fetchArray(SQLITE3_ASSOC);
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
echo json_encode([
'challenge' => b64url_encode($challenge),
'rp' => ['id' => parse_url($host, PHP_URL_HOST) ?: $host, 'name' => 'Vault'],
'user' => ['id' => b64url_encode(pack('N', $auth['userId'])), 'name' => $uRow['username'], 'displayName' => $uRow['username']],
'pubKeyCredParams' => [['type'=>'public-key','alg'=>-7],['type'=>'public-key','alg'=>-257]],
'authenticatorSelection' => ['authenticatorAttachment'=>'platform','residentKey'=>'required','userVerification'=>'required']
]);
break;
case ($path === '/passkey/register/complete' && $method === 'POST'):
$auth = authenticate($db);
requireCSRF($db, $auth['userId']);
$credId = b64url_decode($input['id'] ?? '');
$cdj = b64url_decode($input['response']['clientDataJSON'] ?? '');
$ao = b64url_decode($input['response']['attestationObject'] ?? '');
if (!$credId || !$cdj || !$ao) { http_response_code(400); echo json_encode(['error'=>'Missing data']); break; }
$cd = json_decode($cdj, true);
$stCh = $db->prepare("SELECT id, challenge FROM passkey_challenges WHERE user_id=:uid AND type='register' ORDER BY created_at DESC LIMIT 1");
$stCh->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER);
$chRow = $stCh->execute()->fetchArray(SQLITE3_ASSOC);
if (!$chRow) { http_response_code(400); echo json_encode(['error'=>'No challenge']); break; }
if (!hash_equals(b64url_encode($chRow['challenge']), $cd['challenge'] ?? '')) { http_response_code(400); echo json_encode(['error'=>'Challenge mismatch']); break; }
$att = cbor_decode($ao);
$ad = $att['authData'] ?? '';
if (strlen($ad) < 37) { http_response_code(400); echo json_encode(['error'=>'Invalid authData']); break; }
$off = 32; $flags = ord($ad[$off]); $off += 5;
if (!($flags & 0x40)) { http_response_code(400); echo json_encode(['error'=>'No attested data']); break; }
$off += 16; $cidLen = unpack('n', substr($ad,$off,2))[1]; $off += 2;
$storedCredId = substr($ad,$off,$cidLen); $off += $cidLen;
if (!hash_equals($credId, $storedCredId)) { http_response_code(400); echo json_encode(['error'=>'Credential ID mismatch']); break; }
$pkCOSE = substr($ad, $off);
$stIns = $db->prepare('INSERT INTO passkey_credentials (user_id, credential_id, public_key) VALUES (:uid, :cid, :pk)');
$stIns->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER);
$stIns->bindValue(':cid', $credId, SQLITE3_BLOB);
$stIns->bindValue(':pk', $pkCOSE, SQLITE3_BLOB);
try { $stIns->execute(); } catch (Exception $e) { http_response_code(409); echo json_encode(['error'=>'Passkey already registered']); break; }
$del = $db->prepare('DELETE FROM passkey_challenges WHERE id=:id');
$del->bindValue(':id', $chRow['id'], SQLITE3_INTEGER);
$del->execute();
logAudit($db, $auth['userId'], 'register_passkey');
echo json_encode(['message'=>'OK']);
break;
case ($path === '/passkey/login/begin' && $method === 'POST'):
$u = trim($input['username'] ?? '');
if (!$u) { http_response_code(400); echo json_encode(['error'=>'Username required']); break; }
$stU = $db->prepare('SELECT id FROM users WHERE username=:u');
$stU->bindValue(':u', $u, SQLITE3_TEXT);
$user = $stU->execute()->fetchArray(SQLITE3_ASSOC);
if (!$user) { http_response_code(404); echo json_encode(['error'=>'User not found']); break; }
$stCr = $db->prepare('SELECT credential_id FROM passkey_credentials WHERE user_id=:uid');
$stCr->bindValue(':uid', $user['id'], SQLITE3_INTEGER);
$res = $stCr->execute();
$allow = [];
while ($row = $res->fetchArray(SQLITE3_ASSOC)) $allow[] = ['type'=>'public-key','id'=>b64url_encode($row['credential_id'])];
if (!$allow) { http_response_code(404); echo json_encode(['error'=>'No passkey registered']); break; }
$challenge = random_bytes(32);
$stCh = $db->prepare('INSERT INTO passkey_challenges (user_id, challenge, type) VALUES (:uid, :ch, :t)');
$stCh->bindValue(':uid', $user['id'], SQLITE3_INTEGER);
$stCh->bindValue(':ch', $challenge, SQLITE3_BLOB);
$stCh->bindValue(':t', 'login', SQLITE3_TEXT);
$stCh->execute();
echo json_encode(['challenge'=>b64url_encode($challenge),'allowCredentials'=>$allow,'userVerification'=>'required']);
break;
case ($path === '/passkey/login/complete' && $method === 'POST'):
$credId = b64url_decode($input['id'] ?? '');
$cdj = b64url_decode($input['response']['clientDataJSON'] ?? '');
$ad = b64url_decode($input['response']['authenticatorData'] ?? '');
$sig = b64url_decode($input['response']['signature'] ?? '');
if (!$credId || !$cdj || !$ad || !$sig) { http_response_code(400); echo json_encode(['error'=>'Missing assertion data']); break; }
$stCr = $db->prepare('SELECT id, user_id, public_key FROM passkey_credentials WHERE credential_id=:cid');
$stCr->bindValue(':cid', $credId, SQLITE3_BLOB);
$cred = $stCr->execute()->fetchArray(SQLITE3_ASSOC);
if (!$cred) { http_response_code(401); echo json_encode(['error'=>'Credential not found']); break; }
$stCh = $db->prepare("SELECT id, challenge FROM passkey_challenges WHERE user_id=:uid AND type='login' ORDER BY created_at DESC LIMIT 1");
$stCh->bindValue(':uid', $cred['user_id'], SQLITE3_INTEGER);
$chRow = $stCh->execute()->fetchArray(SQLITE3_ASSOC);
if (!$chRow) { http_response_code(400); echo json_encode(['error'=>'No challenge']); break; }
$cd = json_decode($cdj, true);
if (!hash_equals(b64url_encode($chRow['challenge']), $cd['challenge'] ?? '')) { http_response_code(400); echo json_encode(['error'=>'Challenge mismatch']); break; }
$coseKey = cbor_decode($cred['public_key']);
$pem = coseToPem($coseKey);
if (!$pem) { http_response_code(500); echo json_encode(['error'=>'Unsupported key type']); break; }
$pk = openssl_get_publickey($pem);
if (!$pk) { http_response_code(500); echo json_encode(['error'=>'Failed to parse public key']); break; }
$signedData = $ad . hash('sha256', $cdj, true);
$ok = openssl_verify($signedData, $sig, $pk, OPENSSL_ALGO_SHA256);
if (!$ok) { http_response_code(401); echo json_encode(['error'=>'Invalid signature']); break; }
$del = $db->prepare('DELETE FROM passkey_challenges WHERE id=:id');
$del->bindValue(':id', $chRow['id'], SQLITE3_INTEGER);
$del->execute();
$token = bin2hex(random_bytes(32));
$tokenHash = hash('sha256', $token);
$csrfToken = bin2hex(random_bytes(32));
$expires = date('Y-m-d H:i:s', strtotime('+24 hours'));
$stS = $db->prepare('INSERT INTO sessions (user_id, token_hash, csrf_token, expires_at) VALUES (:uid, :th, :csrf, :exp)');
$stS->bindValue(':uid', $cred['user_id'], SQLITE3_INTEGER);
$stS->bindValue(':th', $tokenHash, SQLITE3_TEXT);
$stS->bindValue(':csrf', $csrfToken, SQLITE3_TEXT);
$stS->bindValue(':exp', $expires, SQLITE3_TEXT);
$stS->execute();
$stU = $db->prepare('SELECT username, salt FROM users WHERE id=:uid');
$stU->bindValue(':uid', $cred['user_id'], SQLITE3_INTEGER);
$user = $stU->execute()->fetchArray(SQLITE3_ASSOC);
logAudit($db, $cred['user_id'], 'passkey_login');
echo json_encode(['message'=>'OK','token'=>$token,'csrfToken'=>$csrfToken,'userId'=>$cred['user_id'],'username'=>$user['username'],'salt'=>$user['salt']]);
break;
default: default:
http_response_code(404); http_response_code(404);
echo json_encode(['error'=>'Not found']); echo json_encode(['error'=>'Not found']);
+9 -3
View File
@@ -57,9 +57,10 @@
<!-- Floating action button --> <!-- Floating action button -->
<button class="fab" onclick="openAdd()" title="Add password">+</button> <button class="fab" onclick="openAdd()" title="Add password">+</button>
<div class="vault"> <div class="vault">
<h1>🔐 Vault <span>XAMPP</span> <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.6rem">
<button class="btn btn-outline btn-xs" onclick="toggleTheme()" style="margin-left:auto">🌓</button> <h1 style="margin-bottom:0">🔐 Vault <span>XAMPP</span></h1>
</h1> <button class="btn btn-outline btn-xs" onclick="toggleTheme()" title="Toggle theme">🌓</button>
</div>
<!-- Auth Section --> <!-- Auth Section -->
<div id="authSection" class="auth-section"> <div id="authSection" class="auth-section">
@@ -72,6 +73,7 @@
<input type="text" id="loginUsername" placeholder="Username" autocomplete="off"> <input type="text" id="loginUsername" placeholder="Username" autocomplete="off">
<input type="password" id="loginPassword" placeholder="Master Password" autocomplete="off"> <input type="password" id="loginPassword" placeholder="Master Password" autocomplete="off">
<button class="btn" id="loginBtn" onclick="login()">🔓 Unlock</button> <button class="btn" id="loginBtn" onclick="login()">🔓 Unlock</button>
<button class="btn btn-outline btn-sm" onclick="loginWithPasskey()" title="Use passkey" style="white-space:nowrap">🔐 Passkey</button>
</div> </div>
</div> </div>
<div id="registerForm" class="hidden"> <div id="registerForm" class="hidden">
@@ -122,6 +124,10 @@
<span>📧 Show email</span> <span>📧 Show email</span>
<div class="toggle-switch active" id="showEmailToggle"></div> <div class="toggle-switch active" id="showEmailToggle"></div>
</div> </div>
<div class="settings-item" onclick="if(window.PublicKeyCredential){registerPasskey()}else{toast('Passkeys not supported','error')};event.stopPropagation();">
<span>🔐 Set up passkey</span>
<span style="color:var(--accent);font-size:0.7rem"></span>
</div>
</div> </div>
</div> </div>
<button class="btn btn-outline btn-sm" onclick="doLogout()">🔒 Lock</button> <button class="btn btn-outline btn-sm" onclick="doLogout()">🔒 Lock</button>
+92
View File
@@ -2,6 +2,8 @@ const API = '/password-manager/api.php';
let token = sessionStorage.getItem('authToken'); let token = sessionStorage.getItem('authToken');
let csrfToken = sessionStorage.getItem('csrfToken') || ''; let csrfToken = sessionStorage.getItem('csrfToken') || '';
let curUser = sessionStorage.getItem('currentUsername'); let curUser = sessionStorage.getItem('currentUsername');
function a2b64(arr) { return btoa(String.fromCharCode(...new Uint8Array(arr))).replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,''); }
function b642ab(s) { return Uint8Array.from(atob(s.replace(/-/g,'+').replace(/_/g,'/')), c=>c.charCodeAt(0)).buffer; }
let view = localStorage.getItem('vaultView') || 'grid'; let view = localStorage.getItem('vaultView') || 'grid';
let showView = localStorage.getItem('showViewBtn') !== 'false'; let showView = localStorage.getItem('showViewBtn') !== 'false';
let showMail = localStorage.getItem('showEmail') !== 'false'; let showMail = localStorage.getItem('showEmail') !== 'false';
@@ -317,6 +319,34 @@ function syncSettingsUI() {
document.getElementById('showEmailToggle').classList.toggle('active', showMail); document.getElementById('showEmailToggle').classList.toggle('active', showMail);
document.getElementById('autoLockTimer').value = lockMin; document.getElementById('autoLockTimer').value = lockMin;
} }
async function registerPasskey() {
try {
const r = await fetch(API + '/passkey/register/begin', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken }
});
if (!r.ok) { const d = await r.json(); toast('❌ ' + (d.error || 'Failed'), 'error'); return; }
const opts = await r.json();
opts.challenge = b642ab(opts.challenge);
opts.user.id = b642ab(opts.user.id);
if (!window.PublicKeyCredential) { toast('❌ Passkeys not supported', 'error'); return; }
const cred = await navigator.credentials.create({ publicKey: opts });
const result = {
id: cred.id,
response: {
clientDataJSON: a2b64(cred.response.clientDataJSON),
attestationObject: a2b64(cred.response.attestationObject)
}
};
const r2 = await fetch(API + '/passkey/register/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken },
body: JSON.stringify(result)
});
if (r2.ok) { toast('✅ Passkey registered!'); playSound('success'); }
else { const d = await r2.json(); toast('❌ ' + (d.error || 'Failed'), 'error'); }
} catch (e) { toast('⚠️ Passkey setup failed: ' + e.message, 'error'); }
}
// ==================== INIT ==================== // ==================== INIT ====================
function init() { function init() {
@@ -438,6 +468,68 @@ function checkAddStrength() {
// ==================== AUTH ==================== // ==================== AUTH ====================
function switchTab(t) { document.querySelectorAll('.auth-tab').forEach(x => x.classList.remove('active')); event.target.classList.add('active'); document.getElementById('loginForm').classList.toggle('hidden', t !== 'login'); document.getElementById('registerForm').classList.toggle('hidden', t !== 'register'); } function switchTab(t) { document.querySelectorAll('.auth-tab').forEach(x => x.classList.remove('active')); event.target.classList.add('active'); document.getElementById('loginForm').classList.toggle('hidden', t !== 'login'); document.getElementById('registerForm').classList.toggle('hidden', t !== 'register'); }
async function loginWithPasskey() {
if (!window.PublicKeyCredential) { toast('❌ Passkeys not supported', 'error'); return; }
const u = document.getElementById('loginUsername').value.trim();
if (!u) { toast('Enter username first', 'error'); return; }
document.getElementById('loginBtn').disabled = true;
try {
const r = await fetch(API + '/passkey/login/begin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: u })
});
if (!r.ok) { const d = await r.json(); toast('❌ ' + (d.error || 'Failed'), 'error'); document.getElementById('loginBtn').disabled = false; return; }
const opts = await r.json();
opts.challenge = b642ab(opts.challenge);
opts.allowCredentials.forEach(c => { c.id = b642ab(c.id); });
const cred = await navigator.credentials.get({ publicKey: opts });
const result = {
id: cred.id,
response: {
clientDataJSON: a2b64(cred.response.clientDataJSON),
authenticatorData: a2b64(cred.response.authenticatorData),
signature: a2b64(cred.response.signature),
userHandle: cred.response.userHandle ? a2b64(cred.response.userHandle) : null
}
};
const r2 = await fetch(API + '/passkey/login/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(result)
});
const d = await r2.json();
if (r2.ok) {
token = d.token; csrfToken = d.csrfToken || ''; curUser = d.username || u;
sessionStorage.setItem('authToken', token);
sessionStorage.setItem('csrfToken', csrfToken);
sessionStorage.setItem('currentUsername', curUser);
// Try to restore crypto key from sessionStorage
const restored = await restoreCryptoKey();
if (!restored) {
// Need master password once to derive crypto key
const mp = await new Promise(resolve => {
const overlay = document.createElement('div');
overlay.className = 'custom-modal-overlay show';
overlay.innerHTML = `<div class="custom-modal"><h3>🔑 One more step</h3><p style="color:var(--text2);margin-bottom:1rem">Enter your master password to unlock the vault</p><input type="password" id="passkeyTempPwd" placeholder="Master password" style="width:100%"><div class="modal-actions" style="margin-top:1rem"><button class="btn btn-sm" id="passkeyTempBtn">Unlock</button></div></div>`;
document.body.appendChild(overlay);
document.getElementById('passkeyTempBtn').onclick = () => resolve(document.getElementById('passkeyTempPwd').value);
overlay.addEventListener('keydown', function handler(e) { if (e.key === 'Enter') { resolve(document.getElementById('passkeyTempPwd').value); overlay.remove(); document.removeEventListener('keydown', handler); } });
});
const m = document.querySelector('.custom-modal-overlay.show');
if (m) m.remove();
cryptoKey = await deriveKey(mp, d.salt);
persistCryptoKey();
}
await loadFolders();
toast('✅ Biometric login!');
playSound('login');
showVault();
loadEntries();
} else { toast('❌ ' + (d.error || 'Failed'), 'error'); }
} catch (e) { toast('⚠️ Passkey login failed: ' + e.message, 'error'); }
finally { document.getElementById('loginBtn').disabled = false; }
}
async function login() { async function login() {
const u = document.getElementById('loginUsername').value.trim(); const u = document.getElementById('loginUsername').value.trim();
const p = document.getElementById('loginPassword').value; const p = document.getElementById('loginPassword').value;