05/18 Enhanced codes and functionalities 5

This commit is contained in:
2026-05-18 18:27:24 -04:00
parent 09d3bbdc15
commit 46765b9448
11 changed files with 1042 additions and 4 deletions
+243
View File
@@ -316,6 +316,44 @@ const Auth = (() => {
const notice = document.getElementById("register-notice");
if (notice) notice.classList.remove("hidden");
}
// Passkey login button (only present on login.html)
const passkeyBtn = document.getElementById("btn-passkey-login");
if (passkeyBtn) {
passkeyBtn.addEventListener("click", async () => {
const errEl = document.getElementById("passkey-error");
errEl.classList.add("hidden");
passkeyBtn.disabled = true;
passkeyBtn.textContent = "Waiting for passkey…";
const email = (document.getElementById("email")?.value || "").trim().toLowerCase();
const result = await PasskeyAuth.loginWithPasskey(email);
passkeyBtn.disabled = false;
passkeyBtn.textContent = "🔑 Sign in with Passkey";
if (result.error) {
errEl.textContent = result.error;
errEl.classList.remove("hidden");
return;
}
// Tokens received — store them, then prompt for master password to unlock vault.
sessionStorage.setItem("access_token", result.data.access_token);
localStorage.setItem("refresh_token", result.data.refresh_token);
sessionStorage.setItem("enc_key_salt", result.data.enc_key_salt);
// Derive vault key from master password.
// Re-use the same master-password input field; if empty, show unlock overlay.
const pw = document.getElementById("password")?.value;
if (pw) {
const vaultKey = await Crypto.deriveVaultKey(pw, result.data.enc_key_salt);
VaultSession.setKey(vaultKey);
}
// Navigate to vault — if vault key wasn't derived, unlock overlay will show.
window.location.href = "/vault";
});
}
}
return { init };
@@ -337,3 +375,208 @@ const VaultSession = (() => {
}
return { setKey, getKey, clear };
})();
// ── PasskeyAuth — WebAuthn / Passkey login and registration management ────────
const PasskeyAuth = (() => {
// ── Helpers ─────────────────────────────────────────────────────────────────
/** Convert ArrayBuffer → base64url string (no padding). */
function _bufToB64url(buf) {
const bytes = new Uint8Array(buf);
let bin = '';
bytes.forEach((b) => (bin += String.fromCharCode(b)));
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
/** Convert base64url string → Uint8Array. */
function _b64urlToBuf(str) {
str = str.replace(/-/g, '+').replace(/_/g, '/');
while (str.length % 4) str += '=';
const bin = atob(str);
const arr = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
return arr;
}
/**
* Convert a PublicKeyCredentialCreationOptions or RequestOptions object
* returned by the server (JSON) into the format expected by navigator.credentials.
* The browser API requires ArrayBuffers for challenge and user.id; the server
* sends base64url strings.
*/
function _prepareCreationOptions(opts) {
opts.challenge = _b64urlToBuf(opts.challenge);
if (opts.user?.id) opts.user.id = _b64urlToBuf(opts.user.id);
if (opts.excludeCredentials) {
opts.excludeCredentials = opts.excludeCredentials.map((c) => ({
...c,
id: _b64urlToBuf(c.id),
}));
}
return opts;
}
function _prepareRequestOptions(opts) {
opts.challenge = _b64urlToBuf(opts.challenge);
if (opts.allowCredentials) {
opts.allowCredentials = opts.allowCredentials.map((c) => ({
...c,
id: _b64urlToBuf(c.id),
}));
}
return opts;
}
/**
* Serialise a PublicKeyCredential returned by navigator.credentials.create()
* or navigator.credentials.get() into a plain JSON-serialisable object that
* the server can accept.
*/
function _credentialToJson(cred) {
const resp = cred.response;
const obj = {
id: cred.id,
rawId: _bufToB64url(cred.rawId),
type: cred.type,
response: {},
};
if (resp.clientDataJSON !== undefined)
obj.response.clientDataJSON = _bufToB64url(resp.clientDataJSON);
if (resp.attestationObject !== undefined)
obj.response.attestationObject = _bufToB64url(resp.attestationObject);
if (resp.authenticatorData !== undefined)
obj.response.authenticatorData = _bufToB64url(resp.authenticatorData);
if (resp.signature !== undefined)
obj.response.signature = _bufToB64url(resp.signature);
if (resp.userHandle !== undefined && resp.userHandle !== null)
obj.response.userHandle = _bufToB64url(resp.userHandle);
// Include transport hints if available (registration only).
if (typeof resp.getTransports === 'function') {
obj.response.transports = resp.getTransports();
}
if (cred.authenticatorAttachment) {
obj.authenticatorAttachment = cred.authenticatorAttachment;
}
if (cred.clientExtensionResults) {
obj.clientExtensionResults = cred.getClientExtensionResults?.() ?? {};
}
return obj;
}
// ── Login flow ───────────────────────────────────────────────────────────────
/**
* Initiate a passkey login.
* 1. Call /api/webauthn/authenticate/begin (optionally with email hint).
* 2. Invoke navigator.credentials.get() — browser shows passkey picker.
* 3. Send the assertion to /api/webauthn/authenticate/complete.
* 4. On success: store tokens and derive vault key from master password.
* The master password is still required to unlock the vault (ZK preserved).
*/
async function loginWithPasskey(email) {
if (!window.PublicKeyCredential) {
return { error: 'Passkeys are not supported in this browser.' };
}
// Step 1: get options from server.
const beginRes = await fetch('/api/webauthn/authenticate/begin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email || '' }),
});
if (!beginRes.ok) {
const d = await beginRes.json().catch(() => ({}));
return { error: d.error || 'Could not start passkey authentication.' };
}
const options = await beginRes.json();
// Step 2: browser passkey picker.
let assertion;
try {
assertion = await navigator.credentials.get({
publicKey: _prepareRequestOptions(options),
});
} catch (e) {
if (e.name === 'NotAllowedError') return { error: 'Passkey cancelled.' };
return { error: e.message || 'Passkey authentication failed.' };
}
// Step 3: send assertion to server.
const completeRes = await fetch('/api/webauthn/authenticate/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(_credentialToJson(assertion)),
});
const completeData = await completeRes.json().catch(() => ({}));
if (!completeRes.ok) {
return { error: completeData.error || 'Passkey authentication failed.' };
}
return { ok: true, data: completeData };
}
// ── Registration flow (settings page) ────────────────────────────────────────
/**
* Register a new passkey for the currently logged-in user.
* Requires an active access_token in sessionStorage (set by vault.js on login).
* @param {string} name User-friendly name for the passkey (e.g. "iPhone 15").
*/
async function registerPasskey(name) {
if (!window.PublicKeyCredential) {
return { error: 'Passkeys are not supported in this browser.' };
}
const token = sessionStorage.getItem('access_token');
if (!token) return { error: 'Not authenticated.' };
// Step 1: get creation options from server.
const beginRes = await fetch('/api/webauthn/register/begin', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
});
if (!beginRes.ok) {
const d = await beginRes.json().catch(() => ({}));
return { error: d.error || 'Could not start passkey registration.' };
}
const options = await beginRes.json();
// Step 2: create credential.
let credential;
try {
credential = await navigator.credentials.create({
publicKey: _prepareCreationOptions(options),
});
} catch (e) {
if (e.name === 'NotAllowedError') return { error: 'Passkey registration cancelled.' };
return { error: e.message || 'Passkey creation failed.' };
}
// Step 3: send attestation to server.
const payload = _credentialToJson(credential);
payload.name = name || 'Passkey';
const completeRes = await fetch('/api/webauthn/register/complete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(payload),
});
const completeData = await completeRes.json().catch(() => ({}));
if (!completeRes.ok) {
return { error: completeData.error || 'Passkey registration failed.' };
}
return { ok: true, credential: completeData.credential };
}
return { loginWithPasskey, registerPasskey };
})();