195 lines
4.9 KiB
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>
|