password_manager/src/components/LockScreen.svelte

195 lines
4.9 KiB
Svelte

<script>
import { app } from '../lib/stores/app.svelte.js'
import { deriveKey, createTestPayload, verifyPassword } from '../lib/crypto/crypto.js'
import { saveVaultMeta, loadVaultMeta, isVaultInitialized } from '../lib/storage/db.js'
import { startAutoLock } from '../lib/stores/security.svelte.js'
let masterPassword = $state('')
let confirmPassword = $state('')
let error = $state('')
let loading = $state(false)
let isSetup = $state(false)
// Check if vault was already created
async function checkVault() {
isSetup = !(await isVaultInitialized())
}
checkVault()
async function handleSubmit() {
error = ''
loading = true
try {
if (isSetup) {
// First-time setup: create vault
if (!masterPassword || masterPassword.length < 4) {
error = 'Password must be at least 4 characters'
loading = false
return
}
if (masterPassword !== confirmPassword) {
error = 'Passwords do not match'
loading = false
return
}
const { salt, testEncrypted, testPlaintext } = await createTestPayload(masterPassword)
app.salt = salt
const key = await deriveKey(masterPassword, salt)
app.encryptionKey = key
await saveVaultMeta(salt, testEncrypted, testPlaintext)
app.isUnlocked = true
startAutoLock()
} else {
// Unlock: verify password against stored test payload
const meta = await loadVaultMeta()
if (!meta.salt || !meta.testEncrypted || !meta.testPlaintext) {
error = 'Vault data corrupted'
loading = false
return
}
const key = await deriveKey(masterPassword, meta.salt)
const isValid = await verifyPassword(masterPassword, meta.salt, meta.testEncrypted, meta.testPlaintext)
if (!isValid) {
error = 'Incorrect password'
loading = false
return
}
app.salt = meta.salt
app.encryptionKey = key
app.isUnlocked = true
startAutoLock()
}
} catch (e) {
console.error(e)
error = 'An error occurred: ' + e.message
}
loading = false
masterPassword = ''
confirmPassword = ''
}
</script>
<div class="lock-screen">
<div class="lock-card">
<div class="lock-icon">🔐</div>
<h1>Password Vault</h1>
<p class="subtitle">{isSetup ? 'Create your vault' : 'Unlock your vault'}</p>
{#if error}
<div class="error-banner" role="alert">{error}</div>
{/if}
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="lock-form">
<div class="form-group">
<label for="master-password">Master Password</label>
<input
id="master-password"
type="password"
bind:value={masterPassword}
placeholder="Enter master password"
autocomplete="current-password"
autofocus
disabled={loading}
/>
</div>
{#if isSetup}
<div class="form-group">
<label for="confirm-password">Confirm Password</label>
<input
id="confirm-password"
type="password"
bind:value={confirmPassword}
placeholder="Confirm master password"
autocomplete="new-password"
disabled={loading}
/>
</div>
{/if}
<button type="submit" class="btn btn-primary w-full" disabled={loading}>
{loading ? 'Processing...' : (isSetup ? 'Create Vault' : 'Unlock')}
</button>
</form>
<p class="hint">
{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.'}
</p>
</div>
</div>
<style>
.lock-screen {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 1rem;
}
.lock-card {
width: 100%;
max-width: 400px;
padding: 2.5rem 2rem;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.lock-icon {
font-size: 3rem;
line-height: 1;
}
h1 {
font-size: 1.5rem;
text-align: center;
}
.subtitle {
color: var(--color-text-muted);
font-size: 0.9rem;
text-align: center;
}
.lock-form {
width: 100%;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.error-banner {
width: 100%;
padding: 10px 14px;
background: rgba(229, 72, 77, 0.15);
border: 1px solid rgba(229, 72, 77, 0.4);
border-radius: var(--radius-md);
color: var(--color-danger);
font-size: 0.85rem;
text-align: center;
}
.hint {
font-size: 0.75rem;
color: var(--color-text-muted);
text-align: center;
line-height: 1.4;
margin-top: 0.5rem;
}
</style>