e4ce0773cbbc
add zxcvbn strength gate (score ≥ 3) to PeachScript backup password export — prevents weak backup passwords
- time
- 2026-05-17 13:36 UTC · 5 months ago
- machine
- koh2
- parent
- 98fd3c5d6af4
↓ peach-browser-e4ce0773.tar.gzatlas at this save
files changed2 files
src/popup/styles/popup.css+26 −0src/popup/views/Export.tsx+57 −9src/popup/styles/popup.css+26 −0
@@ -1714,6 +1714,32 @@17141714 line-height: 1.5;17151715 color: var(--peach-t2);17161716 }1717+.peach-export-strength-track {1718+ height: 3px;1719+ background: var(--peach-border);1720+ border-radius: 2px;1721+ margin: 4px 0 2px;1722+ overflow: hidden;1723+}1724+.peach-export-strength-fill {1725+ height: 100%;1726+ border-radius: 2px;1727+ transition: width 250ms ease, background 250ms ease;1728+}1729+.peach-export-strength-warn {1730+ margin: 0;1731+ font-size: 0.625rem;1732+ line-height: 1.5;1733+ color: #e84040;1734+ font-family: "IBM Plex Mono", monospace;1735+}1736+.peach-export-strength-hint {1737+ margin: 0;1738+ font-size: 0.625rem;1739+ line-height: 1.5;1740+ color: var(--peach-t2);1741+ font-family: "IBM Plex Mono", monospace;1742+}17171743 .peach-export-pill {17181744 display: inline-flex;17191745 align-items: center;src/popup/views/Export.tsx+57 −9
@@ -16,8 +16,23 @@1616 onBack: () => void;1717 }1818 19-const MIN_BACKUP_PASSWORD = 8;19+// Lazy-load zxcvbn — it's heavy (~800 kB) and only needed when the user20+// starts typing a backup password. Shared module-level cache so it is only21+// fetched once across multiple PeachScriptExport mounts.22+type ZxcvbnFn = (pw: string) => { score: number; feedback: { warning: string; suggestions: string[] } };23+let _zxcvbn: ZxcvbnFn | null = null;24+async function getZxcvbn(): Promise<ZxcvbnFn> {25+ if (_zxcvbn) return _zxcvbn;26+ const mod = await import("zxcvbn");27+ _zxcvbn = mod.default as ZxcvbnFn;28+ return _zxcvbn;29+}2030 31+// Backup password must score at least 3 (strong) on the zxcvbn scale (0–4).32+// This prevents a targeted wordlist attack from cracking a weak backup in33+// seconds even though Argon2id raises the per-guess cost.34+const STRENGTH_REQUIRED = 3;35+2136 export function ExportView(props: ExportProps) {2237 const [view, setView] = createSignal<ExportSubview>("menu");2338 return (@@ -83,6 +98,8 @@8398 function PeachScriptExport(props: { onBack: () => void }) {8499 const [pass, setPass] = createSignal("");85100 const [confirm, setConfirm] = createSignal("");101+ const [strengthScore, setStrengthScore] = createSignal<number>(0);102+ const [strengthHint, setStrengthHint] = createSignal<string>("");86103 const [working, setWorking] = createSignal(false);87104 const [script, setScript] = createSignal<string | null>(null);88105 const [error, setError] = createSignal<string | null>(null);@@ -96,9 +113,26 @@96113 const [copied, setCopied] = createSignal(false);97114 98115 const passReady = createMemo(99- () => pass().length >= MIN_BACKUP_PASSWORD && pass() === confirm()116+ () => strengthScore() >= STRENGTH_REQUIRED && pass() === confirm()100117 );101118 119+ function handlePassInput(value: string) {120+ setPass(value);121+ if (!value) {122+ setStrengthScore(0);123+ setStrengthHint("");124+ return;125+ }126+ // Fire-and-forget: if zxcvbn hasn't loaded yet the signal updates when it does127+ void getZxcvbn().then((zxcvbn) => {128+ if (pass() !== value) return; // stale — user typed ahead129+ const result = zxcvbn(value);130+ setStrengthScore(result.score);131+ const hint = result.feedback.warning || result.feedback.suggestions[0] || "";132+ setStrengthHint(hint);133+ });134+ }135+102136 async function handleEncode() {103137 setError(null);104138 setScript(null);@@ -170,9 +204,9 @@170204 ref={(el) => queueMicrotask(() => el.focus())}171205 class="peach-settings-input"172206 type="password"173- placeholder={`backup password (min ${MIN_BACKUP_PASSWORD} chars)`}207+ placeholder="backup password"174208 value={pass()}175- onInput={(e) => setPass((e.target as HTMLInputElement).value)}209+ onInput={(e) => handlePassInput((e.target as HTMLInputElement).value)}176210 onKeyDown={(e) => {177211 if (e.key === "Enter" && passReady() && !working()) {178212 e.preventDefault();@@ -180,6 +214,25 @@180214 }181215 }}182216 />217+ <Show when={pass().length > 0}>218+ <div class="peach-export-strength-track">219+ <div220+ class="peach-export-strength-fill"221+ style={{222+ width: `${(strengthScore() / 4) * 100}%`,223+ background: ["transparent","#e84040","#e8a040","#40a840","#40c840"][strengthScore()],224+ }}225+ />226+ </div>227+ <Show when={strengthScore() < STRENGTH_REQUIRED}>228+ <p class="peach-export-strength-warn">229+ {strengthHint() || "Choose a stronger backup password. This protects your vault if the file is ever found."}230+ </p>231+ </Show>232+ <Show when={strengthScore() >= STRENGTH_REQUIRED && strengthHint()}>233+ <p class="peach-export-strength-hint">{strengthHint()}</p>234+ </Show>235+ </Show>183236 <input184237 class="peach-settings-input"185238 type="password"@@ -196,11 +249,6 @@196249 <Show when={confirm().length > 0 && pass() !== confirm()}>197250 <p class="peach-export-note" style="color:var(--peach-t2)">198251 Passwords don't match199- </p>200- </Show>201- <Show when={pass().length > 0 && pass().length < MIN_BACKUP_PASSWORD}>202- <p class="peach-export-note" style="color:var(--peach-t2)">203- At least {MIN_BACKUP_PASSWORD} characters204252 </p>205253 </Show>206254 </div>