- totp.js: add validateTotpSecret(input, { minBytes = 10 }). Secret is
optional (blank ok); when provided it must decode to a base32 key of at
least 80 bits, rejecting bad characters, typos, wrong format, and
too-short secrets.
- EntryForm: validate the TOTP secret on submit; if invalid, show an inline
error (⚠) and a red input border and block saving. Error clears as the
user types.
- Tests: blank ok, valid bare/grouped/otpauth URIs, invalid chars, too-short,
URI without a secret. 163 total pass.
- Settings dialog gains a 'Show web-server warning' toggle that maps to the
persisted dismissedLocalWarning flag, letting the user re-show the banner
after dismissing it (or hide it proactively).
- The security banner (shown when opened over http(s), not file://) now has a
close button.
- Dismissal persists via the settings store (dismissedLocalWarning), so it
stays hidden across reloads on this browser, while still appearing fresh by
default.
- Rephrased text kept; banner made flex with a hoverable, accessible close
button.
- New src/lib/crypto/totp.js: native RFC 6238 TOTP using Web Crypto HMAC-SHA1
(no external crypto). base32Decode, extractSecret (bare base32 or otpauth://
URI), generateTotp, totpRemainingSeconds.
- Entries gain an optional encryptedTotpSecret, stored AES-GCM-encrypted like
passwords. schema.js createEntry/updateEntry/docs updated.
- Export/import re-key the TOTP secret alongside passwords when sealing with a
separate password, and decrypt/re-encrypt on import so TOTP survives moves.
- EntryForm: optional 'TOTP Secret (2FA)' field (base32 or otpauth:// URI).
- EntryDetail: live 6-digit TOTP display updating every second with a countdown
and urgency indicator, plus copy; guarded cleanup timer on unmount.
- Tests: RFC 6238 SHA-1 vectors (6 & 8 digit), base32/extractSecret, remaining
seconds, schema round-trip. 157 total pass.
- validateEntry no longer requires encryptedPassword; only title is required
(schema.js). encryptedPassword documented as optional.
- EntryForm: decrypt-guards empty password on edit; stores empty string (no
encryption) when the password field is blank; label is 'Password' (no *).
- EntryDetail: only renders the Password field when the entry has one;
decrypt guards empty.
- Tests: schema validation updated (password optional); 143 total pass.
- exportSelected(groupIds, { vaultKey, password, useExistingPassword }) replaces the
positional (groupIds, vaultKey, exportPassword) form. Protection is now an explicit
choice, never an ambiguous optional field.
- password mode: re-key entries under a fresh export-derived key (unchanged semantics).
- reuseExistingPassword mode: seal with the vault's own key, keep vault salt embedded,
so import derives the key from the vault master password. No second password required.
- ImportExport.svelte: replaces the long-placeholder free-text field with two radio
options (Use a new password / Reuse my vault password); short placeholder, with
client-side validation that a new password isn't empty.
- Cryptography importAll unchanged: derives the envelope key from the supplied password +
embedded salt, which covers both sealed modes; wrong password still rejects import.
- Tests: updated call sites to options object; added round-trip + wrong-password tests for
reuseExistingPassword.