'use strict'; // ---- Passwords: your Vaultwarden vault. The vault stays end-to-end encrypted; unlocking asks for your master password, which goes to the server's // Bitwarden tool only to open it for a few idle minutes. Lists never carry passwords: a password, code or note is fetched only when you open that entry. const vtReq = async (p, body) => { const r = await fetch('/api/vault' + p, body === undefined ? {} : { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }); const j = await r.json().catch(() => ({})); if (!r.ok) throw Object.assign(new Error(j.error || 'Request failed (' + r.status + ')'), j, { status: r.status }); return j; }; const vtState = { q: '', folder: '', items: null, folders: [] }; const vtType = { 1: 'Login', 2: 'Note', 3: 'Card', 4: 'Identity' }; let vtClip = 0; /** Copy something private, and clear the clipboard again after half a minute if it is still ours. */ async function vtCopy(text, what) { try { await navigator.clipboard.writeText(text); toast(what + ' copied (cleared in 30 seconds)'); clearTimeout(vtClip); vtClip = setTimeout(async () => { try { if ((await navigator.clipboard.readText()) === text) await navigator.clipboard.writeText(''); } catch { try { await navigator.clipboard.writeText(''); } catch {} } }, 30000); } catch { toast('Could not copy'); } } const vtHost = u => { try { return new URL(/^[a-z]+:\/\//i.test(u) ? u : 'https://' + u).hostname.replace(/^www\./, ''); } catch { return ''; } }; const vtSafeUrl = u => { try { const x = new URL(/^[a-z]+:\/\//i.test(u) ? u : 'https://' + u); return /^https?:$/.test(x.protocol) ? x.href : ''; } catch { return ''; } }; async function viewVault() { const v = $('#view'); v.innerHTML = '

Passwords

Loading…

'; let st; try { st = await vtReq('/status'); } catch (e) { v.innerHTML = `

Passwords

${esc(e.message)}

`; return; } if (!location.hash.startsWith('#/vault')) return; st.unlocked ? vtList(st) : vtUnlock(st); } function vtUnlock(st, need2fa) { const v = $('#view'), host = vtHost((typeof cfg !== 'undefined' && cfg.vault) || st.url); v.innerHTML = `

Passwords

Unlock your vault on ${esc(host)}. Your master password is only used to open it and is never saved.

${st.loggedIn ? `

Signed in as ${esc(st.email)}

` : ''} ${need2fa ? '' : ''}
${st.loggedIn ? '' : ''}

No account yet? Create one on ${esc(host)}, then come back here to unlock it.

`; const f = $('#vtf'); setTimeout(() => (f.querySelector(need2fa ? '#vtc' : st.loggedIn ? '#vtp' : '#vte') || {}).focus?.(), 0); if ($('#vtout')) $('#vtout').onclick = () => confirmSheet('Use a different account?', 'This signs the hub out of the vault. Nothing is deleted from the vault itself.', 'Sign out of vault', async () => { try { await vtReq('/forget', {}); viewVault(); } catch (e) { toast(e.message); } }); f.onsubmit = async e => { e.preventDefault(); $('#vterr').textContent = ''; $('#vtgo').disabled = true; try { const body = { password: $('#vtp').value }; if ($('#vte')) body.email = $('#vte').value; if ($('#vtc')) body.code = $('#vtc').value; const r = await vtReq('/unlock', body); $('#vtp').value = ''; vtState.items = null; vtList(r); } catch (err) { if (err.need2fa) return vtUnlock({ ...st, email: st.email || ($('#vte') && $('#vte').value) }, true) || void 0; $('#vterr').textContent = err.message; $('#vtgo').disabled = false; } }; } async function vtList(st) { const v = $('#view'); v.innerHTML = `

Passwords

Loading…

Locks itself after a few idle minutes. Open the full vault for cards, identities and the trash.

`; const lockedOut = () => { vtState.items = null; vtUnlock({ ...st, unlocked: false }); }; $('#vtnew').onclick = () => vtEdit(null, () => load(), lockedOut); $('#vtgen').onclick = () => vtGenerate(); $('#vtsend').onclick = () => vtSend(lockedOut); $('#vtlock').onclick = async () => { try { await vtReq('/lock', {}); } catch {} lockedOut(); toast('Vault locked'); }; $('#vtsync').onclick = async () => { try { await vtReq('/sync', {}); await load(); toast('Refreshed'); } catch (e) { e.locked ? lockedOut() : toast(e.message); } }; let timer; $('#vtq').oninput = e => { vtState.q = e.target.value; clearTimeout(timer); timer = setTimeout(load, 250); }; async function load() { try { const r = await vtReq('/items?q=' + encodeURIComponent(vtState.q.trim())); if (!location.hash.startsWith('#/vault')) return; vtState.items = r.items; vtState.folders = r.folders; draw(); } catch (e) { if (e.locked) return lockedOut(); $('#vtl').innerHTML = `

${esc(e.message)}

`; } } function draw() { const fo = vtState.folders; $('#vtfo').innerHTML = fo.length ? `
All${fo.map(f => `${esc(f.name)}`).join('')}
` : ''; $('#vtfo').querySelectorAll('a').forEach(a => a.onclick = e => { e.preventDefault(); vtState.folder = a.dataset.f; draw(); }); const rows = vtState.items.filter(i => !vtState.folder || i.folderId === vtState.folder); $('#vtl').innerHTML = rows.length ? `
${rows.map(i => ``).join('')}
` : `

${vtState.q ? 'Nothing matches.' : 'Your vault is empty.'}

`; $('#vtl').querySelectorAll('.row').forEach(b => b.onclick = () => vtOpen(b.dataset.id, lockedOut, load)); } if (vtState.items) draw(); load(); } async function vtOpen(id, lockedOut, reload) { let it; try { it = await vtReq('/item/' + encodeURIComponent(id)); } catch (e) { return e.locked ? lockedOut() : toast(e.message); } const field = (label, value, key, secret) => value ? `
${esc(label)}
${secret ? '••••••••••' : esc(value)}${secret ? `` : ''}
` : ''; const vals = { user: it.username, pass: it.password, totp: it.totp }; const extra = []; if (it.card) { vals.cnum = it.card.number; vals.ccode = it.card.code; } (it.fields || []).forEach((f, n) => { vals['f' + n] = f.value; }); const links = it.uris.map(vtSafeUrl).filter(Boolean); const el = openSheet(it.name || 'Entry', vtType[it.type] || '', `${field('Username', it.username, 'user')}${field('Password', it.password, 'pass', true)}${field('One-time code', it.totp, 'totp')} ${it.card ? field('Cardholder', it.card.holder, 'chold') + field('Card number', it.card.number, 'cnum', true) + field('Security code', it.card.code, 'ccode', true) + field('Expires', [it.card.expMonth, it.card.expYear].filter(Boolean).join('/'), 'cexp') : ''} ${(it.fields || []).map((f, n) => field(f.name || 'Field', f.value, 'f' + n, f.hidden)).join('')} ${links.map(u => `
Website
${esc(u)}
`).join('')} ${it.notes ? `
Notes
${esc(it.notes)}
` : ''}`); if (it.type === 1 || it.type === 2) el.insertAdjacentHTML('beforeend', '
'); if ($('#vted')) $('#vted').onclick = () => { closeSheet(); vtEdit(it, reload, lockedOut); }; Object.assign(vals, { chold: it.card && it.card.holder, cexp: it.card && [it.card.expMonth, it.card.expYear].filter(Boolean).join('/') }); el.querySelectorAll('.vtcp').forEach(b => b.onclick = () => vtCopy(vals[b.dataset.k], b.dataset.w)); el.querySelectorAll('.vtshow').forEach(b => b.onclick = () => { const s = el.querySelector(`.vtv[data-k="${b.dataset.k}"]`), on = b.textContent === 'Show'; s.textContent = on ? vals[b.dataset.k] : '••••••••••'; b.textContent = on ? 'Hide' : 'Show'; }); } /** Make a random password or passphrase. */ function vtGenerate() { const o = { type: 'password', length: 16, upper: 1, lower: 1, number: 1, special: 1, words: 4, capitalize: 1, inum: 0 }; const el = openSheet('Generate a password', 'Made on the server by the Bitwarden tool. Nothing is saved.', `
PasswordPassphrase
`); let value = ''; const gen = async () => { const q = new URLSearchParams(o.type === 'password' ? { type: 'password', length: o.length, upper: o.upper, lower: o.lower, number: o.number, special: o.special } : { type: 'passphrase', words: o.words, capitalize: o.capitalize, number: o.inum }); try { value = (await vtReq('/generate?' + q)).value; el.querySelector('#vgv').textContent = value; el.querySelector('#vge').textContent = ''; } catch (e) { el.querySelector('#vge').textContent = e.message; } }; const opts = () => { el.querySelector('#vgo').innerHTML = o.type === 'password' ? `${[['upper', 'A-Z'], ['lower', 'a-z'], ['number', '0-9'], ['special', '!@#$']].map(([k, l]) => ``).join('')}` : ``; const r = el.querySelector('#vgl') || el.querySelector('#vgw'); r.oninput = () => { o[o.type === 'password' ? 'length' : 'words'] = +r.value; r.previousElementSibling.querySelector('b').textContent = r.value; }; r.onchange = gen; el.querySelectorAll('#vgo input[type=checkbox]').forEach(c => c.onchange = () => { o[c.dataset.k] = c.checked ? 1 : 0; gen(); }); }; el.querySelectorAll('.seg a').forEach(a => a.onclick = e => { e.preventDefault(); o.type = a.dataset.t; el.querySelectorAll('.seg a').forEach(x => x.classList.toggle('on', x === a)); opts(); gen(); }); el.querySelector('#vgc').onclick = () => value && vtCopy(value, 'Password'); el.querySelector('#vgr').onclick = gen; opts(); gen(); } /** Bitwarden Send: a link that shows some text once (or a few times) and then goes away. */ async function vtSend(lockedOut) { const el = openSheet('Send', 'Share some text through a private link that deletes itself.', `
0 for no limit on views.

Your sends

Loading…

`); const drawList = async () => { try { const r = await vtReq('/sends'); el.querySelector('#vsl').innerHTML = r.sends.length ? `
${r.sends.map(x => `
${esc(x.name)}${x.max ? x.views + ' of ' + x.max + ' views' : x.views + ' views'}${x.expires ? ' · deletes ' + esc(new Date(x.expires).toLocaleDateString()) : ''}
`).join('')}
` : '

No sends yet.

'; el.querySelectorAll('[data-c]').forEach(b => b.onclick = () => vtCopy(b.dataset.c, 'Link')); el.querySelectorAll('[data-d]').forEach(b => b.onclick = async () => { try { await vtReq('/sends/delete', { id: b.dataset.d }); drawList(); } catch (e) { toast(e.message); } }); } catch (e) { if (e.locked) { closeSheet(); return lockedOut(); } el.querySelector('#vsl').innerHTML = `

${esc(e.message)}

`; } }; el.querySelector('#vsf').onsubmit = async e => { e.preventDefault(); el.querySelector('#vse').textContent = ''; el.querySelector('#vsg').disabled = true; try { const r = await vtReq('/sends', { name: el.querySelector('#vsn').value, text: el.querySelector('#vst').value, days: el.querySelector('#vsd').value, max: el.querySelector('#vsm').value, password: el.querySelector('#vsp').value }); el.querySelector('#vst').value = ''; el.querySelector('#vsp').value = ''; el.querySelector('#vsr').innerHTML = `
Your link
${esc(r.url)}
`; el.querySelector('#vscp').onclick = () => vtCopy(r.url, 'Link'); vtCopy(r.url, 'Link'); drawList(); } catch (err) { if (err.locked) { closeSheet(); return lockedOut(); } el.querySelector('#vse').textContent = err.message; } el.querySelector('#vsg').disabled = false; }; drawList(); } /** Add an entry (it === null) or change one. Logins and secure notes; cards and identities are changed on the Vaultwarden site. */ function vtEdit(it, reload, lockedOut) { const isNew = !it, type = it ? it.type : 1; const el = openSheet(isNew ? 'New entry' : 'Edit ' + (it.name || 'entry'), '', `
${isNew ? '
LoginSecure note
' : ''}
${isNew ? '' : ''}
`); let t = type; const sync = () => { el.querySelector('#vel').hidden = t === 2; }; el.querySelectorAll('#vet a').forEach(a => a.onclick = e => { e.preventDefault(); t = +a.dataset.t; el.querySelectorAll('#vet a').forEach(x => x.classList.toggle('on', x === a)); sync(); }); sync(); if (it && it.type !== 1) el.querySelector('#vel').hidden = true; el.querySelector('#veg').onclick = async () => { try { el.querySelector('#vep').value = (await vtReq('/generate?type=password&length=20&upper=1&lower=1&number=1&special=1')).value; } catch (e) { toast(e.message); } }; el.querySelector('#vef').onsubmit = async e => { e.preventDefault(); el.querySelector('#vee').textContent = ''; el.querySelector('#ves').disabled = true; const q = id => el.querySelector(id).value; try { await vtReq('/item', { id: it && it.id, type: t, name: q('#ven'), username: q('#veu'), password: q('#vep'), uris: [q('#vew')], totp: q('#vek'), notes: q('#vex'), folderId: q('#vef2'), favorite: el.querySelector('#vefv').checked }); closeSheet(); toast(isNew ? 'Added' : 'Saved'); reload && reload(); } catch (err) { if (err.locked) { closeSheet(); return lockedOut(); } el.querySelector('#vee').textContent = err.message; el.querySelector('#ves').disabled = false; } }; const del = el.querySelector('#ved'); if (del) del.onclick = () => confirmSheet('Delete this entry?', 'It moves to the vault\'s trash on the Vaultwarden site, where it can be restored.', 'Delete', async () => { try { await vtReq('/item/delete', { id: it.id }); toast('Deleted'); reload && reload(); } catch (err) { toast(err.message); } }); }