123import { createMemo, createSignal, Show } from "solid-js";45import { sendMessage } from "../App";6import { PeachWordmark } from "../components/PeachMark";7import {8 parseImportSource,9 type ImportParseResult,10} from "../../shared/import";11import {12 decodePeachScript,13 isPeachScript,14 normalisePeachScript,15} from "../../crypto/peachScript";16import { parsePayload } from "../../crypto/peachPayload";17import type { VaultEntry } from "../../shared/types";1819type ImportSubview = "menu" | "peachscript" | "file";20type ImportMergeMode = "add" | "replace";2122interface ImportProps {23 mode?: "existing" | "onboarding";24 replacesExistingVault?: boolean;25 26 initialView?: ImportSubview;27 onBack: () => void;28 onImport: () => void | Promise<void>;29}3031export function ImportView(props: ImportProps) {32 const [view, setView] = createSignal<ImportSubview>(props.initialView ?? "menu");33 const mode = () => props.mode ?? "existing";34 const isOnboarding = () => mode() === "onboarding";35 36 37 const subviewBack = () => (props.initialView ? props.onBack() : setView("menu"));3839 return (40 <div class={isOnboarding() ? "peach-lock-view" : "peach-overlay-view ov-enter-right"} style="display:flex">41 <Show when={view() === "menu"}>42 <ImportMenu43 mode={mode()}44 replacesExistingVault={props.replacesExistingVault}45 onBack={props.onBack}46 onPick={(v) => setView(v)}47 />48 </Show>49 <Show when={view() === "peachscript"}>50 <PeachScriptImport51 mode={mode()}52 replacesExistingVault={props.replacesExistingVault}53 onBack={subviewBack}54 onImport={props.onImport}55 />56 </Show>57 <Show when={view() === "file"}>58 <FileImport59 mode={mode()}60 replacesExistingVault={props.replacesExistingVault}61 onBack={subviewBack}62 onImport={props.onImport}63 />64 </Show>65 </div>66 );67}686970717273function ImportMenu(props: {74 mode: "existing" | "onboarding";75 replacesExistingVault?: boolean;76 onBack: () => void;77 onPick: (view: ImportSubview) => void;78}) {79 const isOnboarding = props.mode === "onboarding";80 return (81 <>82 <Show83 when={isOnboarding}84 fallback={<OverlayHeader title="import" onBack={props.onBack} />}85 >86 <div class="peach-lock-view-header">87 <PeachWordmark class="peach-wordmark-wrap" />88 <button class="peach-lock-restore-link" onClick={props.onBack}>89 ‹ back90 </button>91 </div>92 </Show>93 <div class="peach-settings-body peach-export-menu">94 <Show when={isOnboarding && props.replacesExistingVault}>95 <div class="peach-settings-group">96 <div class="peach-restore-warning">97 Restoring now replaces the locked local vault on this device with98 a fresh one from your backup or export file.99 </div>100 </div>101 </Show>102103 <div class="peach-settings-group peach-export-card peach-export-card--primary">104 <p class="peach-settings-label">peachscript</p>105 <p class="peach-export-card-copy">106 Paste a PeachScript backup from another Peach device.107 {props.mode === "existing" ? " We'll merge it into your vault." : ""}108 </p>109 <button110 class="peach-btn peach-btn--subtle peach-btn--sm"111 onClick={() => props.onPick("peachscript")}112 >113 Paste PeachScript →114 </button>115 </div>116117 <div class="peach-settings-group peach-export-card">118 <p class="peach-settings-label">backup file</p>119 <p class="peach-export-card-copy">120 A Peach JSON export, or a file from Bitwarden, Proton Pass,121 1Password, or a browser CSV (Chrome, Safari, Edge).122 </p>123 <button124 class="peach-btn peach-btn--subtle peach-btn--sm"125 onClick={() => props.onPick("file")}126 >127 Choose file →128 </button>129 </div>130 </div>131 </>132 );133}134135136137138139function PeachScriptImport(props: {140 mode: "existing" | "onboarding";141 replacesExistingVault?: boolean;142 onBack: () => void;143 onImport: () => void | Promise<void>;144}) {145 const [scriptInput, setScriptInput] = createSignal("");146 const [scriptPass, setScriptPass] = createSignal("");147 const [masterPassword, setMasterPassword] = createSignal("");148 const [masterConfirm, setMasterConfirm] = createSignal("");149 const [working, setWorking] = createSignal(false);150 const [error, setError] = createSignal<string | null>(null);151 const [result, setResult] = createSignal<{ entryCount: number } | null>(null);152153 const scriptLooksValid = createMemo(() => isPeachScript(scriptInput()));154 const isOnboarding = props.mode === "onboarding";155 const masterPasswordReady = createMemo(156 () => masterPassword().length >= 8 && masterPassword() === masterConfirm()157 );158159 async function handleImport() {160 setError(null);161 setResult(null);162 if (!scriptLooksValid()) {163 setError("Paste your PeachScript first.");164 return;165 }166 if (!scriptPass().trim()) {167 setError("Enter the backup password.");168 return;169 }170 setWorking(true);171 try {172 const bytes = decodePeachScript(normalisePeachScript(scriptInput()));173 const payload = await parsePayload(bytes, scriptPass());174175 if (isOnboarding) {176 if (!masterPasswordReady()) {177 throw new Error("MASTER_PASSWORD_REQUIRED");178 }179 const createRes = await sendMessage({180 type: "CREATE_VAULT",181 payload: { password: masterPassword() },182 });183 if (!(createRes as { ok: boolean }).ok) throw new Error("CREATE_VAULT_FAILED");184 }185186 const restoreRes = await sendMessage({187 type: "RESTORE_ENTRIES",188 payload: {189 entries: payload.entries,190 mode: "merge",191 settings: payload.settings,192 },193 });194 const restoreEnvelope = restoreRes as { ok: boolean; error?: string };195 if (!restoreEnvelope.ok) {196 throw new Error(restoreEnvelope.error ?? "RESTORE_FAILED");197 }198199 if (payload.license?.key) {200 await sendMessage({201 type: "ACTIVATE_LICENSE",202 payload: { key: payload.license.key },203 }).catch(() => {});204 }205206 setResult({ entryCount: payload.entries.length });207 await props.onImport();208 } catch (err) {209 const code = err instanceof Error ? err.message : String(err);210 setError(getImportErrorCopy(code));211 } finally {212 setWorking(false);213 }214 }215216 return (217 <>218 <SubviewHeader title="peachscript" onBack={props.onBack} mode={props.mode} />219 <div class="peach-settings-body">220 <Show when={isOnboarding && props.replacesExistingVault}>221 <div class="peach-settings-group">222 <div class="peach-restore-warning">223 Restoring now replaces the locked local vault on this device with224 a fresh one from your backup.225 </div>226 </div>227 </Show>228 <div class="peach-settings-group peach-export-card peach-export-card--primary peach-export-pass-group">229 <p class="peach-settings-label">paste peachscript</p>230 <p class="peach-export-card-copy">231 {isOnboarding232 ? "Paste a PeachScript backup from another Peach device to restore your vault — or load a .peach file from disk."233 : "We'll merge the entries into your existing vault. You can paste PeachScript or load a .peach file."}234 </p>235 <textarea236 class="peach-script-input"237 placeholder="paste PeachScript here…"238 value={scriptInput()}239 onInput={(e) => setScriptInput((e.target as HTMLTextAreaElement).value)}240 />241 <label242 class="peach-btn peach-btn--ghost peach-btn--sm"243 style="display:inline-flex;align-items:center;justify-content:center;cursor:pointer;"244 >245 Load .peach file246 <input247 type="file"248 accept=".peach,application/x-peach,text/plain"249 style="display:none;"250 onChange={async (event) => {251 const input = event.currentTarget;252 const file = input.files?.[0];253 if (!file) return;254 try {255 const text = await file.text();256 setScriptInput(text);257 } catch (err) {258 setError(err instanceof Error ? err.message : "Couldn't read file.");259 } finally {260 input.value = "";261 }262 }}263 />264 </label>265 <div class="peach-settings-stack peach-export-password-stack">266 <input267 class="peach-settings-input"268 type="password"269 placeholder="PeachScript password"270 value={scriptPass()}271 onInput={(e) => setScriptPass((e.target as HTMLInputElement).value)}272 />273 </div>274 <Show when={isOnboarding}>275 <p class="peach-settings-label" style="margin-top:4px">new master password</p>276 <p class="peach-export-card-copy">277 This password unlocks Peach on this device after the restore.278 </p>279 <div class="peach-settings-stack peach-export-password-stack">280 <input281 class="peach-settings-input"282 type="password"283 placeholder={`master password (min 8 chars)`}284 value={masterPassword()}285 onInput={(e) => setMasterPassword((e.target as HTMLInputElement).value)}286 />287 <input288 class="peach-settings-input"289 type="password"290 placeholder="confirm master password"291 value={masterConfirm()}292 onInput={(e) => setMasterConfirm((e.target as HTMLInputElement).value)}293 />294 <Show when={masterConfirm().length > 0 && masterPassword() !== masterConfirm()}>295 <p class="peach-export-note" style="color:var(--peach-t2)">296 Passwords don't match297 </p>298 </Show>299 </div>300 </Show>301 <button302 class="peach-btn peach-btn--subtle peach-btn--sm"303 onClick={() => void handleImport()}304 disabled={305 working() ||306 !scriptLooksValid() ||307 !scriptPass().trim() ||308 (isOnboarding && !masterPasswordReady())309 }310 >311 {working() ? "decoding…" : "Restore from PeachScript"}312 </button>313 <Show when={error()}>314 <div class="peach-restore-error">{error()}</div>315 </Show>316 <Show when={result()}>317 <div class="peach-export-success">318 <span class="peach-export-success-check">✓</span>319 <span>320 Restored {result()!.entryCount} entr321 {result()!.entryCount === 1 ? "y" : "ies"}.322 </span>323 </div>324 </Show>325 </div>326 </div>327 </>328 );329}330331332333334335function FileImport(props: {336 mode: "existing" | "onboarding";337 replacesExistingVault?: boolean;338 onBack: () => void;339 onImport: () => void | Promise<void>;340}) {341 const isOnboarding = props.mode === "onboarding";342 const needsMasterPassword = () => isOnboarding;343344 const [preview, setPreview] = createSignal<ImportParseResult | null>(null);345 const [rawImport, setRawImport] = createSignal<string | null>(null);346 const [backupPassword, setBackupPassword] = createSignal("");347 const [masterPassword, setMasterPassword] = createSignal("");348 const [masterConfirm, setMasterConfirm] = createSignal("");349 const [importing, setImporting] = createSignal(false);350 const [error, setError] = createSignal<string | null>(null);351 const [mergeMode, setMergeMode] = createSignal<ImportMergeMode>("add");352 const [awaitingReplaceConfirm, setAwaitingReplaceConfirm] = createSignal(false);353 const [importProgress, setImportProgress] = createSignal<{ current: number; total: number } | null>(null);354 const [duplicateWarning, setDuplicateWarning] = createSignal<number>(0);355 const [existingCount, setExistingCount] = createSignal<number>(0);356357 const needsBackupPassword = createMemo(() => preview()?.requiresPassword ?? false);358 const masterPasswordReady = createMemo(359 () => masterPassword().length >= 8 && masterPassword() === masterConfirm()360 );361362 async function refreshExistingCount() {363 if (isOnboarding) return;364 const res = await sendMessage({ type: "GET_ENTRIES" });365 const list = (res as { ok: boolean; data?: VaultEntry[] }).data ?? [];366 setExistingCount(list.length);367 }368 void refreshExistingCount();369370 async function parseSource(text: string, password?: string) {371 try {372 const result = await parseImportSource(text, password);373 setPreview(result);374 setError(null);375 if (result.format === "unknown") {376 setError(377 "We couldn't recognise this file format. Peach supports: Peach JSON backups, Bitwarden JSON/CSV, Proton Pass JSON/CSV, 1Password JSON, and generic browser CSV exports."378 );379 }380 return result;381 } catch (err) {382 const message = err instanceof Error ? err.message : String(err);383 setPreview(null);384 setError(getImportErrorCopy(message));385 return null;386 }387 }388389 async function handleFileSelect(event: Event) {390 const file = (event.target as HTMLInputElement).files?.[0];391 if (!file) return;392393 setPreview(null);394 setRawImport(null);395 setBackupPassword("");396 setError(null);397 setAwaitingReplaceConfirm(false);398399 const text = await file.text();400 setRawImport(text);401 await parseSource(text);402 }403404 async function handleUnlockBackup() {405 const raw = rawImport();406 if (!raw || !backupPassword().trim()) return;407 await parseSource(raw, backupPassword());408 }409410 async function countDuplicates(incoming: ImportParseResult["entries"]): Promise<number> {411 if (incoming.length === 0) return 0;412 const res = await sendMessage({ type: "GET_ENTRIES" });413 const existing = ((res as { ok: boolean; data?: VaultEntry[] }).data ?? []).filter((e) => !e.deletedAt);414 const keyOf = (title?: string, url?: string, username?: string) =>415 `${(title ?? "").toLowerCase()}|${(url ?? "").toLowerCase()}|${(username ?? "").toLowerCase()}`;416 const existingKeys = new Set(existing.map((e) => keyOf(e.title, e.url, e.username)));417 let dupes = 0;418 for (const entry of incoming) {419 const u = "username" in entry ? entry.username : undefined;420 const url = "url" in entry ? entry.url : undefined;421 if (existingKeys.has(keyOf(entry.title, url, u))) dupes += 1;422 }423 return dupes;424 }425426 async function handleImport() {427 const currentPreview = preview();428 if (!currentPreview) return;429430 if (needsBackupPassword() && !backupPassword().trim()) {431 setError("Enter the backup password used for this export.");432 return;433 }434435 if (needsMasterPassword()) {436 if (masterPassword().length < 8) {437 setError("Master password must be at least 8 characters.");438 return;439 }440 if (!masterPasswordReady()) {441 setError("Master passwords do not match.");442 return;443 }444 }445446 if (!isOnboarding && mergeMode() === "replace" && !awaitingReplaceConfirm()) {447 setAwaitingReplaceConfirm(true);448 return;449 }450451 setImporting(true);452 setError(null);453 setDuplicateWarning(0);454 setImportProgress(null);455456 try {457 if (currentPreview.entries.length === 0) {458 throw new Error("NO_ENTRIES_IMPORTED");459 }460461 if (needsMasterPassword()) {462 const createResponse = await sendMessage({463 type: "CREATE_VAULT",464 payload: { password: masterPassword() },465 });466 if (!(createResponse as { ok: boolean }).ok) {467 throw new Error("CREATE_VAULT_FAILED");468 }469 }470471 if (!isOnboarding && mergeMode() === "replace") {472 const clearRes = await sendMessage({ type: "CLEAR_VAULT_ENTRIES" });473 if (!(clearRes as { ok: boolean }).ok) {474 throw new Error("CLEAR_VAULT_FAILED");475 }476 }477478 let dupes = 0;479 if (!isOnboarding && mergeMode() === "add") {480 dupes = await countDuplicates(currentPreview.entries);481 }482483 let imported = 0;484 setImportProgress({ current: 0, total: currentPreview.entries.length });485486 const batchRes = await sendMessage({487 type: "BATCH_ADD_ENTRIES",488 payload: { entries: currentPreview.entries },489 });490491 if ((batchRes as { ok: boolean }).ok) {492 imported = ((batchRes as { ok: boolean; data?: VaultEntry[] }).data ?? []).length;493 setImportProgress({ current: imported, total: currentPreview.entries.length });494 } else {495 for (const entry of currentPreview.entries) {496 const response = await sendMessage({497 type: "ADD_ENTRY",498 payload: entry,499 });500 if ((response as { ok: boolean }).ok) imported += 1;501 setImportProgress({ current: imported, total: currentPreview.entries.length });502 }503 }504505 if (imported === 0) {506 throw new Error("NO_ENTRIES_IMPORTED");507 }508509 if (currentPreview.licenseKey) {510 await sendMessage({ type: "ACTIVATE_LICENSE", payload: { key: currentPreview.licenseKey } }).catch(() => {});511 }512513 setDuplicateWarning(dupes);514 await props.onImport();515 } catch (err) {516 const message = err instanceof Error ? err.message : String(err);517 setError(getImportErrorCopy(message));518 setImporting(false);519 setImportProgress(null);520 return;521 }522523 setImporting(false);524 setImportProgress(null);525 }526527 return (528 <>529 <SubviewHeader title="backup file" onBack={props.onBack} mode={props.mode} />530 <div class="peach-settings-body">531 <Show when={isOnboarding && props.replacesExistingVault}>532 <div class="peach-settings-group">533 <div class="peach-restore-warning">534 Restoring now replaces the locked local vault on this device with535 a fresh one from your backup.536 </div>537 </div>538 </Show>539 <div class="peach-settings-group peach-export-card peach-export-card--primary">540 <p class="peach-settings-label">choose a file</p>541 <p class="peach-export-card-copy">542 Bitwarden, 1Password, Proton Pass, Chrome / Edge / Safari CSV, FIDO543 credential-exchange passkeys, or a Peach JSON backup.544 </p>545546 <div class="peach-provider-chips">547 <span class="peach-provider-chip">Peach JSON</span>548 <span class="peach-provider-chip">FIDO passkeys</span>549 <span class="peach-provider-chip">Proton Pass</span>550 <span class="peach-provider-chip">Bitwarden</span>551 <span class="peach-provider-chip">1Password</span>552 <span class="peach-provider-chip">Browser CSV</span>553 </div>554555 <input556 class="peach-restore-file"557 type="file"558 accept=".csv,.json"559 onChange={(event) => void handleFileSelect(event)}560 />561 </div>562563 <Show when={needsBackupPassword()}>564 <div class="peach-settings-group">565 <p class="peach-settings-label">unlock backup</p>566 <p class="peach-export-card-copy">567 Enter the password used when you created this encrypted Peach JSON568 backup.569 </p>570 <input571 class="peach-restore-input"572 type="password"573 placeholder="backup password"574 value={backupPassword()}575 onInput={(event) =>576 setBackupPassword((event.target as HTMLInputElement).value)577 }578 onBlur={() => { if (backupPassword().trim()) void handleUnlockBackup(); }}579 onKeyDown={(event) => {580 if (event.key === "Enter") {581 event.preventDefault();582 void handleUnlockBackup();583 }584 }}585 />586 <div class="peach-settings-actions">587 <button588 class="peach-btn peach-btn--subtle peach-btn--sm"589 onClick={() => void handleUnlockBackup()}590 disabled={!backupPassword().trim()}591 >592 Decrypt →593 </button>594 </div>595 </div>596 </Show>597598 <Show when={preview() && !needsBackupPassword() && preview()!.format !== "unknown"}>599 <div class="peach-settings-group">600 <p class="peach-settings-label">{preview()!.label}</p>601 <div class="peach-import-summary">602 Found <strong>{preview()!.entries.length}</strong>{" "}603 entr{preview()!.entries.length === 1 ? "y" : "ies"}604 {preview()!.skippedItems > 0605 ? `. ${preview()!.skippedItems} skipped because no password was present.`606 : "."}607 </div>608609 <div class="peach-import-preview">610 {preview()!.entries.slice(0, 8).map((entry) => (611 <div class="peach-import-preview-row">612 <span class="peach-import-preview-title">{entry.title}</span>613 <span class="peach-import-preview-meta">614 {entry.passkey615 ? `${entry.passkey.userName} • ${entry.passkey.rpId} (passkey)`616 : entry.type === "note"617 ? "secure note"618 : entry.username || entry.url || "password item"}619 </span>620 </div>621 ))}622 <Show when={preview()!.entries.length > 8}>623 <div class="peach-import-preview-more">624 ...and {preview()!.entries.length - 8} more625 </div>626 </Show>627 </div>628 </div>629 </Show>630631 <Show when={!isOnboarding && preview() && !needsBackupPassword() && preview()!.format !== "unknown" && preview()!.entries.length > 0}>632 <div class="peach-settings-group">633 <p class="peach-settings-label">merge or replace</p>634 <label class="peach-import-choice">635 <input636 type="radio"637 name="merge-mode"638 value="add"639 checked={mergeMode() === "add"}640 onChange={() => { setMergeMode("add"); setAwaitingReplaceConfirm(false); }}641 />642 <span class="peach-import-choice-main">643 <span class="peach-import-choice-label">Add to existing vault</span>644 <span class="peach-import-choice-sub">645 This will add {preview()!.entries.length} entries to your existing {existingCount()}.646 </span>647 </span>648 </label>649 <label class="peach-import-choice">650 <input651 type="radio"652 name="merge-mode"653 value="replace"654 checked={mergeMode() === "replace"}655 onChange={() => setMergeMode("replace")}656 />657 <span class="peach-import-choice-main">658 <span class="peach-import-choice-label">Replace vault</span>659 <span class="peach-import-choice-sub">660 This will replace your {existingCount()} entries with {preview()!.entries.length} imported ones.661 </span>662 </span>663 </label>664 <Show when={awaitingReplaceConfirm()}>665 <div class="peach-restore-warning" style="margin-top:6px">666 Replace forever? This cannot be undone.667 </div>668 </Show>669 </div>670 </Show>671672 <Show when={needsMasterPassword() && preview() && !needsBackupPassword() && preview()!.format !== "unknown"}>673 <div class="peach-settings-group">674 <p class="peach-settings-label">new master password</p>675 <p class="peach-export-card-copy">676 This password unlocks the Peach vault created on this device after677 import.678 </p>679 <div class="peach-auth-grid">680 <input681 class="peach-restore-input"682 type="password"683 placeholder="new master password"684 value={masterPassword()}685 onInput={(event) =>686 setMasterPassword((event.target as HTMLInputElement).value)687 }688 />689 <input690 class="peach-restore-input"691 type="password"692 placeholder="confirm master password"693 value={masterConfirm()}694 onInput={(event) =>695 setMasterConfirm((event.target as HTMLInputElement).value)696 }697 />698 </div>699 </div>700 </Show>701702 <Show when={error()}>703 <div class="peach-settings-group">704 <div class="peach-restore-error">{error()}</div>705 </div>706 </Show>707708 <Show when={importing() && importProgress()}>709 <div class="peach-settings-group">710 <div class="peach-import-summary">711 Importing... [{importProgress()!.current} / {importProgress()!.total}]712 </div>713 </div>714 </Show>715716 <Show when={duplicateWarning() > 0 && !importing()}>717 <div class="peach-settings-group">718 <div class="peach-export-card-copy">719 {duplicateWarning()} {duplicateWarning() === 1 ? "entry appears" : "entries appear"} to be duplicates of existing ones. They were still imported — you can review and delete them in your vault.720 </div>721 </div>722 </Show>723724 <Show when={preview() && !needsBackupPassword() && preview()!.format !== "unknown"}>725 <div class="peach-settings-group">726 <div class="peach-settings-actions">727 <button728 class="peach-btn peach-btn--subtle peach-btn--sm"729 onClick={() => void handleImport()}730 disabled={731 importing() ||732 preview()!.entries.length === 0 ||733 (needsMasterPassword() && !masterPasswordReady())734 }735 >736 {importing()737 ? "importing..."738 : isOnboarding739 ? `Create vault + import ${preview()!.entries.length}`740 : mergeMode() === "replace"741 ? awaitingReplaceConfirm()742 ? "Yes, replace forever"743 : `Replace vault with ${preview()!.entries.length}`744 : `Import ${preview()!.entries.length}`}745 </button>746 </div>747 </div>748 </Show>749 </div>750 </>751 );752}753754755756757758function OverlayHeader(props: { title: string; onBack: () => void }) {759 return (760 <div class="peach-overlay-header">761 <button762 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"763 onClick={props.onBack}764 aria-label="Back"765 >766 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">767 <path d="M6 1L1 6L6 11" />768 </svg>769 </button>770 <span class="peach-overlay-title">{props.title}</span>771 <div class="peach-overlay-spacer"></div>772 </div>773 );774}775776function SubviewHeader(props: {777 title: string;778 onBack: () => void;779 mode: "existing" | "onboarding";780}) {781 782 783 return (784 <Show785 when={props.mode === "onboarding"}786 fallback={<OverlayHeader title={props.title} onBack={props.onBack} />}787 >788 <div class="peach-lock-view-header">789 <PeachWordmark class="peach-wordmark-wrap" />790 <button class="peach-lock-restore-link" onClick={props.onBack}>791 ‹ back792 </button>793 </div>794 </Show>795 );796}797798function getImportErrorCopy(error: string): string {799 switch (error) {800 case "UNSUPPORTED_IMPORT_FORMAT":801 return "Unrecognised file format. Use CSV, Bitwarden JSON, Proton Pass JSON/CSV, or a Peach encrypted backup.";802 case "INVALID_EXPORT_PASSWORD":803 case "INVALID_PASSPHRASE":804 return "Wrong backup password. This is the password you chose when you created the backup.";805 case "INVALID_PEACH_EXPORT":806 return "This Peach backup file looks corrupted or incomplete.";807 case "INVALID_PEACHSCRIPT_CHARACTER":808 return "This doesn't look like PeachScript. Make sure you copied the entire backup string.";809 case "PAYLOAD_TOO_SHORT":810 return "PeachScript looks truncated. Copy the full backup string and try again.";811 case "BAD_MAGIC":812 case "UNSUPPORTED_VERSION":813 return "This PeachScript was created by a different version of Peach.";814 case "CHECKSUM_FAILED":815 return "PeachScript appears corrupted. Re-copy from the source device.";816 case "CREATE_VAULT_FAILED":817 return "Peach couldn't create a new local vault before importing.";818 case "CLEAR_VAULT_FAILED":819 return "Peach couldn't clear the existing vault before replacing it.";820 case "MASTER_PASSWORD_REQUIRED":821 return "Choose a master password for your new vault first.";822 case "RESTORE_FAILED":823 return "Restore failed. Your existing vault is unchanged.";824 case "vault locked":825 return "Peach is locked. Open Peach with your master password and try again.";826 case "refuse to replace with empty entry set":827 return "This PeachScript decoded to zero entries — refusing to wipe the vault.";828 case "NO_ENTRIES_IMPORTED":829 return "No importable password entries were found in that file.";830 default:831 return error;832 }833}