Import.tsx

28.6 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  /** Skip the menu and open straight into a specific subview. */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  // When the caller deep-links into a subview, the back arrow exits the36  // ImportView entirely. Otherwise it returns to the import menu.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}6869// ---------------------------------------------------------------------------70// Menu71// ---------------------------------------------------------------------------7273function 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}134135// ---------------------------------------------------------------------------136// PeachScript paste flow137// ---------------------------------------------------------------------------138139function 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      if (!(restoreRes as { ok: boolean }).ok) throw new Error("RESTORE_FAILED");195196      if (payload.license?.key) {197        await sendMessage({198          type: "ACTIVATE_LICENSE",199          payload: { key: payload.license.key },200        }).catch(() => {});201      }202203      setResult({ entryCount: payload.entries.length });204      await props.onImport();205    } catch (err) {206      const code = err instanceof Error ? err.message : String(err);207      setError(getImportErrorCopy(code));208    } finally {209      setWorking(false);210    }211  }212213  return (214    <>215      <SubviewHeader title="peachscript" onBack={props.onBack} mode={props.mode} />216      <div class="peach-settings-body">217        <Show when={isOnboarding && props.replacesExistingVault}>218          <div class="peach-settings-group">219            <div class="peach-restore-warning">220              Restoring now replaces the locked local vault on this device with221              a fresh one from your backup.222            </div>223          </div>224        </Show>225        <div class="peach-settings-group peach-export-card peach-export-card--primary">226          <p class="peach-settings-label">paste peachscript</p>227          <p class="peach-export-card-copy">228            {isOnboarding229              ? "Paste a PeachScript backup from another Peach device to restore your vault."230              : "We'll merge the entries into your existing vault."}231          </p>232          <textarea233            class="peach-script-input"234            placeholder="paste PeachScript here…"235            value={scriptInput()}236            onInput={(e) => setScriptInput((e.target as HTMLTextAreaElement).value)}237          />238          <input239            class="peach-restore-input"240            type="password"241            placeholder="PeachScript password"242            value={scriptPass()}243            onInput={(e) => setScriptPass((e.target as HTMLInputElement).value)}244          />245          <Show when={isOnboarding}>246            <div class="peach-auth-grid">247              <input248                class="peach-restore-input"249                type="password"250                placeholder="new master password"251                value={masterPassword()}252                onInput={(e) => setMasterPassword((e.target as HTMLInputElement).value)}253              />254              <input255                class="peach-restore-input"256                type="password"257                placeholder="confirm master password"258                value={masterConfirm()}259                onInput={(e) => setMasterConfirm((e.target as HTMLInputElement).value)}260              />261            </div>262          </Show>263          <button264            class="peach-btn peach-btn--subtle peach-btn--sm"265            onClick={() => void handleImport()}266            disabled={267              working() ||268              !scriptLooksValid() ||269              !scriptPass().trim() ||270              (isOnboarding && !masterPasswordReady())271            }272          >273            {working() ? "decoding…" : "Restore from PeachScript"}274          </button>275          <Show when={error()}>276            <div class="peach-restore-error">{error()}</div>277          </Show>278          <Show when={result()}>279            <div class="peach-export-success">280              <span class="peach-export-success-check">✓</span>281              <span>282                Restored {result()!.entryCount} entr283                {result()!.entryCount === 1 ? "y" : "ies"}.284              </span>285            </div>286          </Show>287        </div>288      </div>289    </>290  );291}292293// ---------------------------------------------------------------------------294// File flow (Bitwarden / 1Password / Peach JSON / etc.)295// ---------------------------------------------------------------------------296297function FileImport(props: {298  mode: "existing" | "onboarding";299  replacesExistingVault?: boolean;300  onBack: () => void;301  onImport: () => void | Promise<void>;302}) {303  const isOnboarding = props.mode === "onboarding";304  const needsMasterPassword = () => isOnboarding;305306  const [preview, setPreview] = createSignal<ImportParseResult | null>(null);307  const [rawImport, setRawImport] = createSignal<string | null>(null);308  const [backupPassword, setBackupPassword] = createSignal("");309  const [masterPassword, setMasterPassword] = createSignal("");310  const [masterConfirm, setMasterConfirm] = createSignal("");311  const [importing, setImporting] = createSignal(false);312  const [error, setError] = createSignal<string | null>(null);313  const [mergeMode, setMergeMode] = createSignal<ImportMergeMode>("add");314  const [awaitingReplaceConfirm, setAwaitingReplaceConfirm] = createSignal(false);315  const [importProgress, setImportProgress] = createSignal<{ current: number; total: number } | null>(null);316  const [duplicateWarning, setDuplicateWarning] = createSignal<number>(0);317  const [existingCount, setExistingCount] = createSignal<number>(0);318319  const needsBackupPassword = createMemo(() => preview()?.requiresPassword ?? false);320  const masterPasswordReady = createMemo(321    () => masterPassword().length >= 8 && masterPassword() === masterConfirm()322  );323324  async function refreshExistingCount() {325    if (isOnboarding) return;326    const res = await sendMessage({ type: "GET_ENTRIES" });327    const list = (res as { ok: boolean; data?: VaultEntry[] }).data ?? [];328    setExistingCount(list.length);329  }330  void refreshExistingCount();331332  async function parseSource(text: string, password?: string) {333    try {334      const result = await parseImportSource(text, password);335      setPreview(result);336      setError(null);337      if (result.format === "unknown") {338        setError(339          "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."340        );341      }342      return result;343    } catch (err) {344      const message = err instanceof Error ? err.message : String(err);345      setPreview(null);346      setError(getImportErrorCopy(message));347      return null;348    }349  }350351  async function handleFileSelect(event: Event) {352    const file = (event.target as HTMLInputElement).files?.[0];353    if (!file) return;354355    setPreview(null);356    setRawImport(null);357    setBackupPassword("");358    setError(null);359    setAwaitingReplaceConfirm(false);360361    const text = await file.text();362    setRawImport(text);363    await parseSource(text);364  }365366  async function handleUnlockBackup() {367    const raw = rawImport();368    if (!raw || !backupPassword().trim()) return;369    await parseSource(raw, backupPassword());370  }371372  async function countDuplicates(incoming: ImportParseResult["entries"]): Promise<number> {373    if (incoming.length === 0) return 0;374    const res = await sendMessage({ type: "GET_ENTRIES" });375    const existing = ((res as { ok: boolean; data?: VaultEntry[] }).data ?? []).filter((e) => !e.deletedAt);376    const keyOf = (title?: string, url?: string, username?: string) =>377      `${(title ?? "").toLowerCase()}|${(url ?? "").toLowerCase()}|${(username ?? "").toLowerCase()}`;378    const existingKeys = new Set(existing.map((e) => keyOf(e.title, e.url, e.username)));379    let dupes = 0;380    for (const entry of incoming) {381      const u = "username" in entry ? entry.username : undefined;382      const url = "url" in entry ? entry.url : undefined;383      if (existingKeys.has(keyOf(entry.title, url, u))) dupes += 1;384    }385    return dupes;386  }387388  async function handleImport() {389    const currentPreview = preview();390    if (!currentPreview) return;391392    if (needsBackupPassword() && !backupPassword().trim()) {393      setError("Enter the backup password used for this export.");394      return;395    }396397    if (needsMasterPassword()) {398      if (masterPassword().length < 8) {399        setError("Master password must be at least 8 characters.");400        return;401      }402      if (!masterPasswordReady()) {403        setError("Master passwords do not match.");404        return;405      }406    }407408    if (!isOnboarding && mergeMode() === "replace" && !awaitingReplaceConfirm()) {409      setAwaitingReplaceConfirm(true);410      return;411    }412413    setImporting(true);414    setError(null);415    setDuplicateWarning(0);416    setImportProgress(null);417418    try {419      if (currentPreview.entries.length === 0) {420        throw new Error("NO_ENTRIES_IMPORTED");421      }422423      if (needsMasterPassword()) {424        const createResponse = await sendMessage({425          type: "CREATE_VAULT",426          payload: { password: masterPassword() },427        });428        if (!(createResponse as { ok: boolean }).ok) {429          throw new Error("CREATE_VAULT_FAILED");430        }431      }432433      if (!isOnboarding && mergeMode() === "replace") {434        const clearRes = await sendMessage({ type: "CLEAR_VAULT_ENTRIES" });435        if (!(clearRes as { ok: boolean }).ok) {436          throw new Error("CLEAR_VAULT_FAILED");437        }438      }439440      let dupes = 0;441      if (!isOnboarding && mergeMode() === "add") {442        dupes = await countDuplicates(currentPreview.entries);443      }444445      let imported = 0;446      setImportProgress({ current: 0, total: currentPreview.entries.length });447448      const batchRes = await sendMessage({449        type: "BATCH_ADD_ENTRIES",450        payload: { entries: currentPreview.entries },451      });452453      if ((batchRes as { ok: boolean }).ok) {454        imported = ((batchRes as { ok: boolean; data?: VaultEntry[] }).data ?? []).length;455        setImportProgress({ current: imported, total: currentPreview.entries.length });456      } else {457        for (const entry of currentPreview.entries) {458          const response = await sendMessage({459            type: "ADD_ENTRY",460            payload: entry,461          });462          if ((response as { ok: boolean }).ok) imported += 1;463          setImportProgress({ current: imported, total: currentPreview.entries.length });464        }465      }466467      if (imported === 0) {468        throw new Error("NO_ENTRIES_IMPORTED");469      }470471      if (currentPreview.licenseKey) {472        await sendMessage({ type: "ACTIVATE_LICENSE", payload: { key: currentPreview.licenseKey } }).catch(() => {});473      }474475      setDuplicateWarning(dupes);476      await props.onImport();477    } catch (err) {478      const message = err instanceof Error ? err.message : String(err);479      setError(getImportErrorCopy(message));480      setImporting(false);481      setImportProgress(null);482      return;483    }484485    setImporting(false);486    setImportProgress(null);487  }488489  return (490    <>491      <SubviewHeader title="backup file" onBack={props.onBack} mode={props.mode} />492      <div class="peach-settings-body">493        <Show when={isOnboarding && props.replacesExistingVault}>494          <div class="peach-settings-group">495            <div class="peach-restore-warning">496              Restoring now replaces the locked local vault on this device with497              a fresh one from your backup.498            </div>499          </div>500        </Show>501        <div class="peach-settings-group peach-export-card peach-export-card--primary">502          <p class="peach-settings-label">choose a file</p>503          <p class="peach-export-card-copy">504            Bitwarden, 1Password, Proton Pass, Chrome / Edge / Safari CSV, FIDO505            credential-exchange passkeys, or a Peach JSON backup.506          </p>507508          <div class="peach-provider-chips">509            <span class="peach-provider-chip">Peach JSON</span>510            <span class="peach-provider-chip">FIDO passkeys</span>511            <span class="peach-provider-chip">Proton Pass</span>512            <span class="peach-provider-chip">Bitwarden</span>513            <span class="peach-provider-chip">1Password</span>514            <span class="peach-provider-chip">Browser CSV</span>515          </div>516517          <input518            class="peach-restore-file"519            type="file"520            accept=".csv,.json"521            onChange={(event) => void handleFileSelect(event)}522          />523        </div>524525        <Show when={needsBackupPassword()}>526          <div class="peach-settings-group">527            <p class="peach-settings-label">unlock backup</p>528            <p class="peach-export-card-copy">529              Enter the password used when you created this encrypted Peach JSON530              backup.531            </p>532            <input533              class="peach-restore-input"534              type="password"535              placeholder="backup password"536              value={backupPassword()}537              onInput={(event) =>538                setBackupPassword((event.target as HTMLInputElement).value)539              }540              onBlur={() => { if (backupPassword().trim()) void handleUnlockBackup(); }}541              onKeyDown={(event) => {542                if (event.key === "Enter") {543                  event.preventDefault();544                  void handleUnlockBackup();545                }546              }}547            />548            <div class="peach-settings-actions">549              <button550                class="peach-btn peach-btn--subtle peach-btn--sm"551                onClick={() => void handleUnlockBackup()}552                disabled={!backupPassword().trim()}553              >554                Decrypt →555              </button>556            </div>557          </div>558        </Show>559560        <Show when={preview() && !needsBackupPassword() && preview()!.format !== "unknown"}>561          <div class="peach-settings-group">562            <p class="peach-settings-label">{preview()!.label}</p>563            <div class="peach-import-summary">564              Found <strong>{preview()!.entries.length}</strong>{" "}565              entr{preview()!.entries.length === 1 ? "y" : "ies"}566              {preview()!.skippedItems > 0567                ? `. ${preview()!.skippedItems} skipped because no password was present.`568                : "."}569            </div>570571            <div class="peach-import-preview">572              {preview()!.entries.slice(0, 8).map((entry) => (573                <div class="peach-import-preview-row">574                  <span class="peach-import-preview-title">{entry.title}</span>575                  <span class="peach-import-preview-meta">576                    {entry.type === "passkey"577                      ? `${entry.userName || "passkey"} • ${entry.rpId || "site"}`578                      : entry.type === "note"579                        ? "secure note"580                        : entry.username || entry.url || "password item"}581                  </span>582                </div>583              ))}584              <Show when={preview()!.entries.length > 8}>585                <div class="peach-import-preview-more">586                  ...and {preview()!.entries.length - 8} more587                </div>588              </Show>589            </div>590          </div>591        </Show>592593        <Show when={!isOnboarding && preview() && !needsBackupPassword() && preview()!.format !== "unknown" && preview()!.entries.length > 0}>594          <div class="peach-settings-group">595            <p class="peach-settings-label">merge or replace</p>596            <label class="peach-import-choice">597              <input598                type="radio"599                name="merge-mode"600                value="add"601                checked={mergeMode() === "add"}602                onChange={() => { setMergeMode("add"); setAwaitingReplaceConfirm(false); }}603              />604              <span class="peach-import-choice-main">605                <span class="peach-import-choice-label">Add to existing vault</span>606                <span class="peach-import-choice-sub">607                  This will add {preview()!.entries.length} entries to your existing {existingCount()}.608                </span>609              </span>610            </label>611            <label class="peach-import-choice">612              <input613                type="radio"614                name="merge-mode"615                value="replace"616                checked={mergeMode() === "replace"}617                onChange={() => setMergeMode("replace")}618              />619              <span class="peach-import-choice-main">620                <span class="peach-import-choice-label">Replace vault</span>621                <span class="peach-import-choice-sub">622                  This will replace your {existingCount()} entries with {preview()!.entries.length} imported ones.623                </span>624              </span>625            </label>626            <Show when={awaitingReplaceConfirm()}>627              <div class="peach-restore-warning" style="margin-top:6px">628                Replace forever? This cannot be undone.629              </div>630            </Show>631          </div>632        </Show>633634        <Show when={needsMasterPassword() && preview() && !needsBackupPassword() && preview()!.format !== "unknown"}>635          <div class="peach-settings-group">636            <p class="peach-settings-label">new master password</p>637            <p class="peach-export-card-copy">638              This password unlocks the Peach vault created on this device after639              import.640            </p>641            <div class="peach-auth-grid">642              <input643                class="peach-restore-input"644                type="password"645                placeholder="new master password"646                value={masterPassword()}647                onInput={(event) =>648                  setMasterPassword((event.target as HTMLInputElement).value)649                }650              />651              <input652                class="peach-restore-input"653                type="password"654                placeholder="confirm master password"655                value={masterConfirm()}656                onInput={(event) =>657                  setMasterConfirm((event.target as HTMLInputElement).value)658                }659              />660            </div>661          </div>662        </Show>663664        <Show when={error()}>665          <div class="peach-settings-group">666            <div class="peach-restore-error">{error()}</div>667          </div>668        </Show>669670        <Show when={importing() && importProgress()}>671          <div class="peach-settings-group">672            <div class="peach-import-summary">673              Importing... [{importProgress()!.current} / {importProgress()!.total}]674            </div>675          </div>676        </Show>677678        <Show when={duplicateWarning() > 0 && !importing()}>679          <div class="peach-settings-group">680            <div class="peach-export-card-copy">681              {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.682            </div>683          </div>684        </Show>685686        <Show when={preview() && !needsBackupPassword() && preview()!.format !== "unknown"}>687          <div class="peach-settings-group">688            <div class="peach-settings-actions">689              <button690                class="peach-btn peach-btn--subtle peach-btn--sm"691                onClick={() => void handleImport()}692                disabled={693                  importing() ||694                  preview()!.entries.length === 0 ||695                  (needsMasterPassword() && !masterPasswordReady())696                }697              >698                {importing()699                  ? "importing..."700                  : isOnboarding701                    ? `Create vault + import ${preview()!.entries.length}`702                    : mergeMode() === "replace"703                      ? awaitingReplaceConfirm()704                        ? "Yes, replace forever"705                        : `Replace vault with ${preview()!.entries.length}`706                      : `Import ${preview()!.entries.length}`}707              </button>708            </div>709          </div>710        </Show>711      </div>712    </>713  );714}715716// ---------------------------------------------------------------------------717// Shared headers718// ---------------------------------------------------------------------------719720function OverlayHeader(props: { title: string; onBack: () => void }) {721  return (722    <div class="peach-overlay-header">723      <button724        class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"725        onClick={props.onBack}726        aria-label="Back"727      >728        <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">729          <path d="M6 1L1 6L6 11" />730        </svg>731      </button>732      <span class="peach-overlay-title">{props.title}</span>733      <div class="peach-overlay-spacer"></div>734    </div>735  );736}737738function SubviewHeader(props: {739  title: string;740  onBack: () => void;741  mode: "existing" | "onboarding";742}) {743  // Onboarding sub-flows reuse the lock-screen header; existing-vault flows744  // use the standard overlay back-arrow header.745  return (746    <Show747      when={props.mode === "onboarding"}748      fallback={<OverlayHeader title={props.title} onBack={props.onBack} />}749    >750      <div class="peach-lock-view-header">751        <PeachWordmark class="peach-wordmark-wrap" />752        <button class="peach-lock-restore-link" onClick={props.onBack}>753          ‹ back754        </button>755      </div>756    </Show>757  );758}759760function getImportErrorCopy(error: string): string {761  switch (error) {762    case "UNSUPPORTED_IMPORT_FORMAT":763      return "Unrecognised file format. Use CSV, Bitwarden JSON, Proton Pass JSON/CSV, or a Peach encrypted backup.";764    case "INVALID_EXPORT_PASSWORD":765    case "INVALID_PASSPHRASE":766      return "Wrong backup password. This is the password you chose when you created the backup.";767    case "INVALID_PEACH_EXPORT":768      return "This Peach backup file looks corrupted or incomplete.";769    case "INVALID_PEACHSCRIPT_CHARACTER":770      return "This doesn't look like PeachScript. Make sure you copied the entire backup string.";771    case "PAYLOAD_TOO_SHORT":772      return "PeachScript looks truncated. Copy the full backup string and try again.";773    case "BAD_MAGIC":774    case "UNSUPPORTED_VERSION":775      return "This PeachScript was created by a different version of Peach.";776    case "CHECKSUM_FAILED":777      return "PeachScript appears corrupted. Re-copy from the source device.";778    case "CREATE_VAULT_FAILED":779      return "Peach couldn't create a new local vault before importing.";780    case "CLEAR_VAULT_FAILED":781      return "Peach couldn't clear the existing vault before replacing it.";782    case "MASTER_PASSWORD_REQUIRED":783      return "Choose a master password for your new vault first.";784    case "RESTORE_FAILED":785      return "Restore failed. Your existing vault is unchanged.";786    case "NO_ENTRIES_IMPORTED":787      return "No importable password entries were found in that file.";788    default:789      return error;790  }791}