/** * LockScreen — master password setup + unlock UI. */ import { Component } from './component.js' import { app } from '../lib/stores/app.js' import { deriveKey, createTestPayload, verifyPassword } from '../lib/crypto/crypto.js' import { saveVaultMeta, loadVaultMeta, isVaultInitialized, ensureTrashGroup } from '../lib/storage/db.js' import { startAutoLock } from '../lib/stores/security.js' import { settings } from '../lib/stores/settings.js' import { autofocus } from '../lib/autofocus.js' export class LockScreen extends Component { masterPassword = '' confirmPassword = '' error = '' loading = false isSetup = false mount() { super.mount() this.#checkVault() return this } render() { const notLocal = typeof window !== 'undefined' && window.location.protocol !== 'file:' this.el = this.ce('div', { className: 'lock-screen' }, this.ce('div', { className: 'lock-card' }, this.ce('div', { className: 'lock-icon', textContent: '🔐' }), this.ce('h1', { textContent: 'Password Vault' }), this.ce('p', { className: 'subtitle', textContent: 'Unlock your vault' }), notLocal ? this.ce('div', { className: 'warning-banner', role: 'alert', textContent: 'This HTML file is intended for offline use.' }) : null, null, // error-banner placeholder this.#buildForm(), this.ce('p', { className: 'hint', textContent: 'Your data is encrypted with AES-256-GCM. Key is stored only in memory.' }), ), ) // Store references to dynamic elements this._subtitle = this.q('.subtitle') this._confirmGroup = this.q('.confirm-group') this._submitBtn = this.q('.submit-btn') this._passwordInput = this.q('#master-password') this._confirmInput = this.q('#confirm-password') this._hint = this.q('.hint') autofocus(this._passwordInput, true) // Wire input listeners so this.masterPassword / this.confirmPassword stay in sync if (this._passwordInput) { this.on(this._passwordInput, 'input', (e) => { this.masterPassword = e.target.value }) } if (this._confirmInput) { this.on(this._confirmInput, 'input', (e) => { this.confirmPassword = e.target.value }) } return this.el } #buildForm() { return this.ce('form', { className: 'lock-form', id: 'lock-form' }, this.ce('div', { className: 'form-group' }, this.ce('label', { htmlFor: 'master-password', textContent: 'Master Password' }), this.ce('input', { id: 'master-password', type: 'password', placeholder: 'Enter master password', autocomplete: 'current-password' }), ), this.ce('div', { className: 'form-group confirm-group' }, this.ce('label', { htmlFor: 'confirm-password', textContent: 'Confirm Password' }), this.ce('input', { id: 'confirm-password', type: 'password', placeholder: 'Confirm master password', autocomplete: 'new-password' }), ), this.ce('button', { type: 'submit', className: 'btn btn-primary w-full submit-btn', textContent: 'Unlock' }), ) } #updateUI() { if (this._subtitle) { this._subtitle.textContent = this.isSetup ? 'Create your vault' : 'Unlock your vault' } if (this._confirmGroup) { this._confirmGroup.style.display = this.isSetup ? '' : 'none' } if (this._submitBtn) { this._submitBtn.textContent = this.loading ? 'Processing...' : (this.isSetup ? 'Create Vault' : 'Unlock') this._submitBtn.disabled = this.loading } if (this._passwordInput) { this._passwordInput.disabled = this.loading } if (this._confirmInput) { this._confirmInput.disabled = this.loading } // Error banner if (this.error) { if (!this._errorBanner) { const banner = this.ce('div', { className: 'error-banner', role: 'alert', textContent: this.error }) const form = this.q('#lock-form') form?.parentNode?.insertBefore(banner, form) this._errorBanner = banner } else { this._errorBanner.textContent = this.error } } else if (this._errorBanner) { this._errorBanner.remove() this._errorBanner = null } if (this._hint) { this._hint.textContent = this.isSetup ? 'Your master password encrypts all data locally. It cannot be recovered if lost.' : 'Your data is encrypted with AES-256-GCM. Key is stored only in memory.' } } #checkVault() { isVaultInitialized().then(init => { this.isSetup = !init this.#updateUI() }) } #handleSubmit = async (e) => { e.preventDefault() this.error = '' this.loading = true this.#updateUI() try { if (this.isSetup) { if (!this.masterPassword || this.masterPassword.length < 4) { this.error = 'Password must be at least 4 characters' this.loading = false this.#updateUI() return } if (this.masterPassword !== this.confirmPassword) { this.error = 'Passwords do not match' this.loading = false this.#updateUI() return } const { salt, testEncrypted, testPlaintext } = await createTestPayload(this.masterPassword) app.salt = salt const key = await deriveKey(this.masterPassword, salt) app.encryptionKey = key await saveVaultMeta(salt, testEncrypted, testPlaintext) await ensureTrashGroup() await settings.load() app.isUnlocked = true startAutoLock() } else { const meta = await loadVaultMeta() if (!meta.salt || !meta.testEncrypted || !meta.testPlaintext) { this.error = 'Vault data corrupted' this.loading = false this.#updateUI() return } const key = await deriveKey(this.masterPassword, meta.salt) const isValid = await verifyPassword(this.masterPassword, meta.salt, meta.testEncrypted, meta.testPlaintext) if (!isValid) { this.error = 'Incorrect password' this.loading = false this.#updateUI() return } app.salt = meta.salt app.encryptionKey = key await settings.load() app.isUnlocked = true startAutoLock() } } catch (err) { console.error(err) this.error = 'An error occurred: ' + err.message } this.loading = false this.masterPassword = '' this.confirmPassword = '' if (this._passwordInput) this._passwordInput.value = '' if (this._confirmInput) this._confirmInput.value = '' this.#updateUI() } afterMount() { const form = this.q('#lock-form') if (form) this.on(form, 'submit', this.#handleSubmit) } }