971d0d
fix: require confirm-password in encrypted export before enabling download (v1.5.3)
- time
- 2026-04-23 22:12 UTC · 6 months ago
- machine
- koh2
- parent
- aa90db
↓ peach-browser-971d0d.tar.gzatlas at this save
files changed5 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/popup/views/Export.tsx+20 −4src/popup/views/contracts.test.tsx+21 −0manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "1.5.2",4+ "version": "1.5.3",55 "description": "Zero-knowledge password manager",66 "browser_specific_settings": {77 "gecko": {manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "1.5.2",4+ "version": "1.5.3",55 "description": "Zero-knowledge password manager",66 "action": {77 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "1.5.2",3+ "version": "1.5.3",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {src/popup/views/Export.tsx+20 −4
@@ -25,21 +25,23 @@2525 2626 function MainExportView(props: { onBack: () => void; onOpenCjk: () => void }) {2727 const [exportPassword, setExportPassword] = createSignal("");28+ const [exportPasswordConfirm, setExportPasswordConfirm] = createSignal("");2829 const [exporting, setExporting] = createSignal(false);2930 const [result, setResult] = createSignal<string | null>(null);3031 const [error, setError] = createSignal<string | null>(null);3132 const [showCsvWarning, setShowCsvWarning] = createSignal(false);3233 33- const passwordIsValid = createMemo(34+ const passwordsMatch = createMemo(3435 () => exportPassword().length >= MIN_EXPORT_PASSWORD36+ && exportPassword() === exportPasswordConfirm()3537 );3638 3739 async function handleEncryptedExport() {3840 setError(null);3941 setResult(null);4042 41- if (exportPassword().length < MIN_EXPORT_PASSWORD) {42- setError(`Export password must be at least ${MIN_EXPORT_PASSWORD} characters.`);43+ if (!passwordsMatch()) {44+ setError("Passwords do not match or are too short.");4345 return;4446 }4547 @@ -72,6 +74,7 @@7274 `Exported ${countStr}. Filename: ${filename}. Store this file and your export password somewhere safe — neither can be recovered.`7375 );7476 setExportPassword("");77+ setExportPasswordConfirm("");7578 } catch (err) {7679 setError(err instanceof Error ? err.message : "Export failed.");7780 } finally {@@ -181,6 +184,19 @@181184 value={exportPassword()}182185 onInput={(e) => setExportPassword((e.target as HTMLInputElement).value)}183186 />187+ <input188+ id="export-confirm-password"189+ class="peach-settings-input"190+ type="password"191+ placeholder="confirm backup password"192+ value={exportPasswordConfirm()}193+ onInput={(e) => setExportPasswordConfirm((e.target as HTMLInputElement).value)}194+ />195+ <Show when={exportPasswordConfirm().length > 0 && exportPassword() !== exportPasswordConfirm()}>196+ <p class="peach-export-note" style="color:var(--peach-t2)">197+ Passwords don't match198+ </p>199+ </Show>184200 <Show when={exportPassword().length > 0 && exportPassword().length < MIN_EXPORT_PASSWORD}>185201 <p class="peach-export-note" style="color:var(--peach-t2)">186202 At least {MIN_EXPORT_PASSWORD} characters@@ -190,7 +206,7 @@190206 <button191207 class="peach-btn peach-btn--subtle peach-btn--sm"192208 onClick={() => void handleEncryptedExport()}193- disabled={exporting() || !passwordIsValid()}209+ disabled={exporting() || !passwordsMatch()}194210 >195211 {exporting() ? "saving…" : "Download backup file"}196212 </button>src/popup/views/contracts.test.tsx+21 −0
@@ -239,17 +239,36 @@239239 const container = mount(<ExportView onBack={vi.fn()} />);240240 241241 const password = container.querySelector('input[placeholder*="backup password"]') as HTMLInputElement;242+ const confirm = container.querySelector("#export-confirm-password") as HTMLInputElement;242243 const button = Array.from(container.querySelectorAll("button")).find(243244 (candidate) => candidate.textContent?.trim() === "Download backup file"244245 ) as HTMLButtonElement;245246 246247 inputText(password, "too-short");248+ inputText(confirm, "too-short");247249 await flushView();248250 249251 expect(button.disabled).toBe(true);250252 expect(exportEntriesToPeachJsonMock).not.toHaveBeenCalled();251253 });252254 255+ it("Export keeps encrypted export disabled when passwords do not match", async () => {256+ const container = mount(<ExportView onBack={vi.fn()} />);257+258+ const password = container.querySelector('input[placeholder*="backup password"]') as HTMLInputElement;259+ const confirm = container.querySelector("#export-confirm-password") as HTMLInputElement;260+ const button = Array.from(container.querySelectorAll("button")).find(261+ (candidate) => candidate.textContent?.trim() === "Download backup file"262+ ) as HTMLButtonElement;263+264+ inputText(password, "correct horse battery staple");265+ inputText(confirm, "different horse battery staple");266+ await flushView();267+268+ expect(button.disabled).toBe(true);269+ expect(exportEntriesToPeachJsonMock).not.toHaveBeenCalled();270+ });271+253272 it("Export requests entries and sends the correct payload to the encrypted backup exporter", async () => {254273 sendMessageMock.mockResolvedValue({255274 ok: true,@@ -262,11 +281,13 @@262281 const container = mount(<ExportView onBack={vi.fn()} />);263282 264283 const password = container.querySelector('input[placeholder*="backup password"]') as HTMLInputElement;284+ const confirm = container.querySelector("#export-confirm-password") as HTMLInputElement;265285 const button = Array.from(container.querySelectorAll("button")).find(266286 (candidate) => candidate.textContent?.trim() === "Download backup file"267287 ) as HTMLButtonElement;268288 269289 inputText(password, "correct horse battery staple");290+ inputText(confirm, "correct horse battery staple");270291 await flushView();271292 button.click();272293 await flushView();