Import.tsx

28.3 KB · raw

1// Signal-conditioned views in this file must use <Show>, never a2// signal-conditioned early return — see Projects.md §4.1.3import { 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  onBack: () => void;26  onImport: () => void | Promise<void>;27}2829export function ImportView(props: ImportProps) {30  const [view, setView] = createSignal<ImportSubview>("menu");31  const mode = () => props.mode ?? "existing";32  const isOnboarding = () => mode() === "onboarding";3334  return (35    <div class={isOnboarding() ? "peach-lock-view" : "peach-overlay-view ov-enter-right"} style="display:flex">36      <Show when={view() === "menu"}>37        <ImportMenu38          mode={mode()}39          replacesExistingVault={props.replacesExistingVault}40          onBack={props.onBack}41          onPick={(v) => setView(v)}42        />43      </Show>44      <Show when={view() === "peachscript"}>45        <PeachScriptImport46          mode={mode()}47          replacesExistingVault={props.replacesExistingVault}48          onBack={() => setView("menu")}49          onImport={props.onImport}50        />51      </Show>52      <Show when={view() === "file"}>53        <FileImport54          mode={mode()}55          replacesExistingVault={props.replacesExistingVault}56          onBack={() => setView("menu")}57          onImport={props.onImport}58        />59      </Show>60    </div>61  );62}6364// ---------------------------------------------------------------------------65// Menu66// ---------------------------------------------------------------------------6768function ImportMenu(props: {69  mode: "existing" | "onboarding";70  replacesExistingVault?: boolean;71  onBack: () => void;72  onPick: (view: ImportSubview) => void;73}) {74  const isOnboarding = props.mode === "onboarding";75  return (76    <>77      <Show78        when={isOnboarding}79        fallback={<OverlayHeader title="import" onBack={props.onBack} />}80      >81        <div class="peach-lock-view-header">82          <PeachWordmark class="peach-wordmark-wrap" />83          <button class="peach-lock-restore-link" onClick={props.onBack}>84            ‹ back85          </button>86        </div>87      </Show>88      <div class="peach-settings-body peach-export-menu">89        <Show when={isOnboarding && props.replacesExistingVault}>90          <div class="peach-settings-group">91            <div class="peach-restore-warning">92              Restoring now replaces the locked local vault on this device with93              a fresh one from your backup or export file.94            </div>95          </div>96        </Show>9798        <div class="peach-settings-group peach-export-card peach-export-card--primary">99          <p class="peach-settings-label">peachscript</p>100          <p class="peach-export-card-copy">101            Paste a PeachScript backup from another Peach device.102            {props.mode === "existing" ? " We'll merge it into your vault." : ""}103          </p>104          <button105            class="peach-btn peach-btn--subtle peach-btn--sm"106            onClick={() => props.onPick("peachscript")}107          >108            Paste PeachScript →109          </button>110        </div>111112        <div class="peach-settings-group peach-export-card">113          <p class="peach-settings-label">backup file</p>114          <p class="peach-export-card-copy">115            A Peach JSON export, or a file from Bitwarden, Proton Pass,116            1Password, or a browser CSV (Chrome, Safari, Edge).117          </p>118          <button119            class="peach-btn peach-btn--subtle peach-btn--sm"120            onClick={() => props.onPick("file")}121          >122            Choose file →123          </button>124        </div>125      </div>126    </>127  );128}129130// ---------------------------------------------------------------------------131// PeachScript paste flow132// ---------------------------------------------------------------------------133134function PeachScriptImport(props: {135  mode: "existing" | "onboarding";136  replacesExistingVault?: boolean;137  onBack: () => void;138  onImport: () => void | Promise<void>;139}) {140  const [scriptInput, setScriptInput] = createSignal("");141  const [scriptPass, setScriptPass] = createSignal("");142  const [masterPassword, setMasterPassword] = createSignal("");143  const [masterConfirm, setMasterConfirm] = createSignal("");144  const [working, setWorking] = createSignal(false);145  const [error, setError] = createSignal<string | null>(null);146  const [result, setResult] = createSignal<{ entryCount: number } | null>(null);147148  const scriptLooksValid = createMemo(() => isPeachScript(scriptInput()));149  const isOnboarding = props.mode === "onboarding";150  const masterPasswordReady = createMemo(151    () => masterPassword().length >= 8 && masterPassword() === masterConfirm()152  );153154  async function handleImport() {155    setError(null);156    setResult(null);157    if (!scriptLooksValid()) {158      setError("Paste your PeachScript first.");159      return;160    }161    if (!scriptPass().trim()) {162      setError("Enter the backup password.");163      return;164    }165    setWorking(true);166    try {167      const bytes = decodePeachScript(normalisePeachScript(scriptInput()));168      const payload = await parsePayload(bytes, scriptPass());169170      if (isOnboarding) {171        if (!masterPasswordReady()) {172          throw new Error("MASTER_PASSWORD_REQUIRED");173        }174        const createRes = await sendMessage({175          type: "CREATE_VAULT",176          payload: { password: masterPassword() },177        });178        if (!(createRes as { ok: boolean }).ok) throw new Error("CREATE_VAULT_FAILED");179      }180181      const restoreRes = await sendMessage({182        type: "RESTORE_ENTRIES",183        payload: {184          entries: payload.entries,185          mode: "merge",186          settings: payload.settings,187        },188      });189      if (!(restoreRes as { ok: boolean }).ok) throw new Error("RESTORE_FAILED");190191      if (payload.license?.key) {192        await sendMessage({193          type: "ACTIVATE_LICENSE",194          payload: { key: payload.license.key },195        }).catch(() => {});196      }197198      setResult({ entryCount: payload.entries.length });199      await props.onImport();200    } catch (err) {201      const code = err instanceof Error ? err.message : String(err);202      setError(getImportErrorCopy(code));203    } finally {204      setWorking(false);205    }206  }207208  return (209    <>210      <SubviewHeader title="peachscript" onBack={props.onBack} mode={props.mode} />211      <div class="peach-settings-body">212        <Show when={isOnboarding && props.replacesExistingVault}>213          <div class="peach-settings-group">214            <div class="peach-restore-warning">215              Restoring now replaces the locked local vault on this device with216              a fresh one from your backup.217            </div>218          </div>219        </Show>220        <div class="peach-settings-group peach-export-card peach-export-card--primary">221          <p class="peach-settings-label">paste peachscript</p>222          <p class="peach-export-card-copy">223            {isOnboarding224              ? "Paste a PeachScript backup from another Peach device to restore your vault."225              : "We'll merge the entries into your existing vault."}226          </p>227          <textarea228            class="peach-script-input"229            placeholder="paste PeachScript here…"230            value={scriptInput()}231            onInput={(e) => setScriptInput((e.target as HTMLTextAreaElement).value)}232          />233          <input234            class="peach-restore-input"235            type="password"236            placeholder="PeachScript password"237            value={scriptPass()}238            onInput={(e) => setScriptPass((e.target as HTMLInputElement).value)}239          />240          <Show when={isOnboarding}>241            <div class="peach-auth-grid">242              <input243                class="peach-restore-input"244                type="password"245                placeholder="new master password"246                value={masterPassword()}247                onInput={(e) => setMasterPassword((e.target as HTMLInputElement).value)}248              />249              <input250                class="peach-restore-input"251                type="password"252                placeholder="confirm master password"253                value={masterConfirm()}254                onInput={(e) => setMasterConfirm((e.target as HTMLInputElement).value)}255              />256            </div>257          </Show>258          <button259            class="peach-btn peach-btn--subtle peach-btn--sm"260            onClick={() => void handleImport()}261            disabled={262              working() ||263              !scriptLooksValid() ||264              !scriptPass().trim() ||265              (isOnboarding && !masterPasswordReady())266            }267          >268            {working() ? "decoding…" : "Restore from PeachScript"}269          </button>270          <Show when={error()}>271            <div class="peach-restore-error">{error()}</div>272          </Show>273          <Show when={result()}>274            <div class="peach-export-success">275              <span class="peach-export-success-check">✓</span>276              <span>277                Restored {result()!.entryCount} entr278                {result()!.entryCount === 1 ? "y" : "ies"}.279              </span>280            </div>281          </Show>282        </div>283      </div>284    </>285  );286}287288// ---------------------------------------------------------------------------289// File flow (Bitwarden / 1Password / Peach JSON / etc.)290// ---------------------------------------------------------------------------291292function FileImport(props: {293  mode: "existing" | "onboarding";294  replacesExistingVault?: boolean;295  onBack: () => void;296  onImport: () => void | Promise<void>;297}) {298  const isOnboarding = props.mode === "onboarding";299  const needsMasterPassword = () => isOnboarding;300301  const [preview, setPreview] = createSignal<ImportParseResult | null>(null);302  const [rawImport, setRawImport] = createSignal<string | null>(null);303  const [backupPassword, setBackupPassword] = createSignal("");304  const [masterPassword, setMasterPassword] = createSignal("");305  const [masterConfirm, setMasterConfirm] = createSignal("");306  const [importing, setImporting] = createSignal(false);307  const [error, setError] = createSignal<string | null>(null);308  const [mergeMode, setMergeMode] = createSignal<ImportMergeMode>("add");309  const [awaitingReplaceConfirm, setAwaitingReplaceConfirm] = createSignal(false);310  const [importProgress, setImportProgress] = createSignal<{ current: number; total: number } | null>(null);311  const [duplicateWarning, setDuplicateWarning] = createSignal<number>(0);312  const [existingCount, setExistingCount] = createSignal<number>(0);313314  const needsBackupPassword = createMemo(() => preview()?.requiresPassword ?? false);315  const masterPasswordReady = createMemo(316    () => masterPassword().length >= 8 && masterPassword() === masterConfirm()317  );318319  async function refreshExistingCount() {320    if (isOnboarding) return;321    const res = await sendMessage({ type: "GET_ENTRIES" });322    const list = (res as { ok: boolean; data?: VaultEntry[] }).data ?? [];323    setExistingCount(list.length);324  }325  void refreshExistingCount();326327  async function parseSource(text: string, password?: string) {328    try {329      const result = await parseImportSource(text, password);330      setPreview(result);331      setError(null);332      if (result.format === "unknown") {333        setError(334          "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."335        );336      }337      return result;338    } catch (err) {339      const message = err instanceof Error ? err.message : String(err);340      setPreview(null);341      setError(getImportErrorCopy(message));342      return null;343    }344  }345346  async function handleFileSelect(event: Event) {347    const file = (event.target as HTMLInputElement).files?.[0];348    if (!file) return;349350    setPreview(null);351    setRawImport(null);352    setBackupPassword("");353    setError(null);354    setAwaitingReplaceConfirm(false);355356    const text = await file.text();357    setRawImport(text);358    await parseSource(text);359  }360361  async function handleUnlockBackup() {362    const raw = rawImport();363    if (!raw || !backupPassword().trim()) return;364    await parseSource(raw, backupPassword());365  }366367  async function countDuplicates(incoming: ImportParseResult["entries"]): Promise<number> {368    if (incoming.length === 0) return 0;369    const res = await sendMessage({ type: "GET_ENTRIES" });370    const existing = ((res as { ok: boolean; data?: VaultEntry[] }).data ?? []).filter((e) => !e.deletedAt);371    const keyOf = (title?: string, url?: string, username?: string) =>372      `${(title ?? "").toLowerCase()}|${(url ?? "").toLowerCase()}|${(username ?? "").toLowerCase()}`;373    const existingKeys = new Set(existing.map((e) => keyOf(e.title, e.url, e.username)));374    let dupes = 0;375    for (const entry of incoming) {376      const u = "username" in entry ? entry.username : undefined;377      const url = "url" in entry ? entry.url : undefined;378      if (existingKeys.has(keyOf(entry.title, url, u))) dupes += 1;379    }380    return dupes;381  }382383  async function handleImport() {384    const currentPreview = preview();385    if (!currentPreview) return;386387    if (needsBackupPassword() && !backupPassword().trim()) {388      setError("Enter the backup password used for this export.");389      return;390    }391392    if (needsMasterPassword()) {393      if (masterPassword().length < 8) {394        setError("Master password must be at least 8 characters.");395        return;396      }397      if (!masterPasswordReady()) {398        setError("Master passwords do not match.");399        return;400      }401    }402403    if (!isOnboarding && mergeMode() === "replace" && !awaitingReplaceConfirm()) {404      setAwaitingReplaceConfirm(true);405      return;406    }407408    setImporting(true);409    setError(null);410    setDuplicateWarning(0);411    setImportProgress(null);412413    try {414      if (currentPreview.entries.length === 0) {415        throw new Error("NO_ENTRIES_IMPORTED");416      }417418      if (needsMasterPassword()) {419        const createResponse = await sendMessage({420          type: "CREATE_VAULT",421          payload: { password: masterPassword() },422        });423        if (!(createResponse as { ok: boolean }).ok) {424          throw new Error("CREATE_VAULT_FAILED");425        }426      }427428      if (!isOnboarding && mergeMode() === "replace") {429        const clearRes = await sendMessage({ type: "CLEAR_VAULT_ENTRIES" });430        if (!(clearRes as { ok: boolean }).ok) {431          throw new Error("CLEAR_VAULT_FAILED");432        }433      }434435      let dupes = 0;436      if (!isOnboarding && mergeMode() === "add") {437        dupes = await countDuplicates(currentPreview.entries);438      }439440      let imported = 0;441      setImportProgress({ current: 0, total: currentPreview.entries.length });442443      const batchRes = await sendMessage({444        type: "BATCH_ADD_ENTRIES",445        payload: { entries: currentPreview.entries },446      });447448      if ((batchRes as { ok: boolean }).ok) {449        imported = ((batchRes as { ok: boolean; data?: VaultEntry[] }).data ?? []).length;450        setImportProgress({ current: imported, total: currentPreview.entries.length });451      } else {452        for (const entry of currentPreview.entries) {453          const response = await sendMessage({454            type: "ADD_ENTRY",455            payload: entry,456          });457          if ((response as { ok: boolean }).ok) imported += 1;458          setImportProgress({ current: imported, total: currentPreview.entries.length });459        }460      }461462      if (imported === 0) {463        throw new Error("NO_ENTRIES_IMPORTED");464      }465466      if (currentPreview.licenseKey) {467        await sendMessage({ type: "ACTIVATE_LICENSE", payload: { key: currentPreview.licenseKey } }).catch(() => {});468      }469470      setDuplicateWarning(dupes);471      await props.onImport();472    } catch (err) {473      const message = err instanceof Error ? err.message : String(err);474      setError(getImportErrorCopy(message));475      setImporting(false);476      setImportProgress(null);477      return;478    }479480    setImporting(false);481    setImportProgress(null);482  }483484  return (485    <>486      <SubviewHeader title="backup file" onBack={props.onBack} mode={props.mode} />487      <div class="peach-settings-body">488        <Show when={isOnboarding && props.replacesExistingVault}>489          <div class="peach-settings-group">490            <div class="peach-restore-warning">491              Restoring now replaces the locked local vault on this device with492              a fresh one from your backup.493            </div>494          </div>495        </Show>496        <div class="peach-settings-group peach-export-card peach-export-card--primary">497          <p class="peach-settings-label">choose a file</p>498          <p class="peach-export-card-copy">499            Bitwarden, 1Password, Proton Pass, Chrome / Edge / Safari CSV, FIDO500            credential-exchange passkeys, or a Peach JSON backup.501          </p>502503          <div class="peach-provider-chips">504            <span class="peach-provider-chip">Peach JSON</span>505            <span class="peach-provider-chip">FIDO passkeys</span>506            <span class="peach-provider-chip">Proton Pass</span>507            <span class="peach-provider-chip">Bitwarden</span>508            <span class="peach-provider-chip">1Password</span>509            <span class="peach-provider-chip">Browser CSV</span>510          </div>511512          <input513            class="peach-restore-file"514            type="file"515            accept=".csv,.json"516            onChange={(event) => void handleFileSelect(event)}517          />518        </div>519520        <Show when={needsBackupPassword()}>521          <div class="peach-settings-group">522            <p class="peach-settings-label">unlock backup</p>523            <p class="peach-export-card-copy">524              Enter the password used when you created this encrypted Peach JSON525              backup.526            </p>527            <input528              class="peach-restore-input"529              type="password"530              placeholder="backup password"531              value={backupPassword()}532              onInput={(event) =>533                setBackupPassword((event.target as HTMLInputElement).value)534              }535              onBlur={() => { if (backupPassword().trim()) void handleUnlockBackup(); }}536              onKeyDown={(event) => {537                if (event.key === "Enter") {538                  event.preventDefault();539                  void handleUnlockBackup();540                }541              }}542            />543            <div class="peach-settings-actions">544              <button545                class="peach-btn peach-btn--subtle peach-btn--sm"546                onClick={() => void handleUnlockBackup()}547                disabled={!backupPassword().trim()}548              >549                Decrypt →550              </button>551            </div>552          </div>553        </Show>554555        <Show when={preview() && !needsBackupPassword() && preview()!.format !== "unknown"}>556          <div class="peach-settings-group">557            <p class="peach-settings-label">{preview()!.label}</p>558            <div class="peach-import-summary">559              Found <strong>{preview()!.entries.length}</strong>{" "}560              entr{preview()!.entries.length === 1 ? "y" : "ies"}561              {preview()!.skippedItems > 0562                ? `. ${preview()!.skippedItems} skipped because no password was present.`563                : "."}564            </div>565566            <div class="peach-import-preview">567              {preview()!.entries.slice(0, 8).map((entry) => (568                <div class="peach-import-preview-row">569                  <span class="peach-import-preview-title">{entry.title}</span>570                  <span class="peach-import-preview-meta">571                    {entry.type === "passkey"572                      ? `${entry.userName || "passkey"} • ${entry.rpId || "site"}`573                      : entry.type === "note"574                        ? "secure note"575                        : entry.username || entry.url || "password item"}576                  </span>577                </div>578              ))}579              <Show when={preview()!.entries.length > 8}>580                <div class="peach-import-preview-more">581                  ...and {preview()!.entries.length - 8} more582                </div>583              </Show>584            </div>585          </div>586        </Show>587588        <Show when={!isOnboarding && preview() && !needsBackupPassword() && preview()!.format !== "unknown" && preview()!.entries.length > 0}>589          <div class="peach-settings-group">590            <p class="peach-settings-label">merge or replace</p>591            <label class="peach-import-choice">592              <input593                type="radio"594                name="merge-mode"595                value="add"596                checked={mergeMode() === "add"}597                onChange={() => { setMergeMode("add"); setAwaitingReplaceConfirm(false); }}598              />599              <span class="peach-import-choice-main">600                <span class="peach-import-choice-label">Add to existing vault</span>601                <span class="peach-import-choice-sub">602                  This will add {preview()!.entries.length} entries to your existing {existingCount()}.603                </span>604              </span>605            </label>606            <label class="peach-import-choice">607              <input608                type="radio"609                name="merge-mode"610                value="replace"611                checked={mergeMode() === "replace"}612                onChange={() => setMergeMode("replace")}613              />614              <span class="peach-import-choice-main">615                <span class="peach-import-choice-label">Replace vault</span>616                <span class="peach-import-choice-sub">617                  This will replace your {existingCount()} entries with {preview()!.entries.length} imported ones.618                </span>619              </span>620            </label>621            <Show when={awaitingReplaceConfirm()}>622              <div class="peach-restore-warning" style="margin-top:6px">623                Replace forever? This cannot be undone.624              </div>625            </Show>626          </div>627        </Show>628629        <Show when={needsMasterPassword() && preview() && !needsBackupPassword() && preview()!.format !== "unknown"}>630          <div class="peach-settings-group">631            <p class="peach-settings-label">new master password</p>632            <p class="peach-export-card-copy">633              This password unlocks the Peach vault created on this device after634              import.635            </p>636            <div class="peach-auth-grid">637              <input638                class="peach-restore-input"639                type="password"640                placeholder="new master password"641                value={masterPassword()}642                onInput={(event) =>643                  setMasterPassword((event.target as HTMLInputElement).value)644                }645              />646              <input647                class="peach-restore-input"648                type="password"649                placeholder="confirm master password"650                value={masterConfirm()}651                onInput={(event) =>652                  setMasterConfirm((event.target as HTMLInputElement).value)653                }654              />655            </div>656          </div>657        </Show>658659        <Show when={error()}>660          <div class="peach-settings-group">661            <div class="peach-restore-error">{error()}</div>662          </div>663        </Show>664665        <Show when={importing() && importProgress()}>666          <div class="peach-settings-group">667            <div class="peach-import-summary">668              Importing... [{importProgress()!.current} / {importProgress()!.total}]669            </div>670          </div>671        </Show>672673        <Show when={duplicateWarning() > 0 && !importing()}>674          <div class="peach-settings-group">675            <div class="peach-export-card-copy">676              {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.677            </div>678          </div>679        </Show>680681        <Show when={preview() && !needsBackupPassword() && preview()!.format !== "unknown"}>682          <div class="peach-settings-group">683            <div class="peach-settings-actions">684              <button685                class="peach-btn peach-btn--subtle peach-btn--sm"686                onClick={() => void handleImport()}687                disabled={688                  importing() ||689                  preview()!.entries.length === 0 ||690                  (needsMasterPassword() && !masterPasswordReady())691                }692              >693                {importing()694                  ? "importing..."695                  : isOnboarding696                    ? `Create vault + import ${preview()!.entries.length}`697                    : mergeMode() === "replace"698                      ? awaitingReplaceConfirm()699                        ? "Yes, replace forever"700                        : `Replace vault with ${preview()!.entries.length}`701                      : `Import ${preview()!.entries.length}`}702              </button>703            </div>704          </div>705        </Show>706      </div>707    </>708  );709}710711// ---------------------------------------------------------------------------712// Shared headers713// ---------------------------------------------------------------------------714715function OverlayHeader(props: { title: string; onBack: () => void }) {716  return (717    <div class="peach-overlay-header">718      <button719        class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"720        onClick={props.onBack}721        aria-label="Back"722      >723        <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">724          <path d="M6 1L1 6L6 11" />725        </svg>726      </button>727      <span class="peach-overlay-title">{props.title}</span>728      <div class="peach-overlay-spacer"></div>729    </div>730  );731}732733function SubviewHeader(props: {734  title: string;735  onBack: () => void;736  mode: "existing" | "onboarding";737}) {738  // Onboarding sub-flows reuse the lock-screen header; existing-vault flows739  // use the standard overlay back-arrow header.740  return (741    <Show742      when={props.mode === "onboarding"}743      fallback={<OverlayHeader title={props.title} onBack={props.onBack} />}744    >745      <div class="peach-lock-view-header">746        <PeachWordmark class="peach-wordmark-wrap" />747        <button class="peach-lock-restore-link" onClick={props.onBack}>748          ‹ back749        </button>750      </div>751    </Show>752  );753}754755function getImportErrorCopy(error: string): string {756  switch (error) {757    case "UNSUPPORTED_IMPORT_FORMAT":758      return "Unrecognised file format. Use CSV, Bitwarden JSON, Proton Pass JSON/CSV, or a Peach encrypted backup.";759    case "INVALID_EXPORT_PASSWORD":760    case "INVALID_PASSPHRASE":761      return "Wrong backup password. This is the password you chose when you created the backup.";762    case "INVALID_PEACH_EXPORT":763      return "This Peach backup file looks corrupted or incomplete.";764    case "INVALID_PEACHSCRIPT_CHARACTER":765      return "This doesn't look like PeachScript. Make sure you copied the entire backup string.";766    case "PAYLOAD_TOO_SHORT":767      return "PeachScript looks truncated. Copy the full backup string and try again.";768    case "BAD_MAGIC":769    case "UNSUPPORTED_VERSION":770      return "This PeachScript was created by a different version of Peach.";771    case "CHECKSUM_FAILED":772      return "PeachScript appears corrupted. Re-copy from the source device.";773    case "CREATE_VAULT_FAILED":774      return "Peach couldn't create a new local vault before importing.";775    case "CLEAR_VAULT_FAILED":776      return "Peach couldn't clear the existing vault before replacing it.";777    case "MASTER_PASSWORD_REQUIRED":778      return "Choose a master password for your new vault first.";779    case "RESTORE_FAILED":780      return "Restore failed. Your existing vault is unchanged.";781    case "NO_ENTRIES_IMPORTED":782      return "No importable password entries were found in that file.";783    default:784      return error;785  }786}