Skip to content

Commit 9d54ec9

Browse files
committed
Bundle encrypted exports as .zip with decryption tool and README for future-proof access
1 parent 2fe4a2f commit 9d54ec9

4 files changed

Lines changed: 177 additions & 14 deletions

File tree

web/package-lock.json

Lines changed: 100 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"bs58check": "^3.0.1",
2525
"class-variance-authority": "^0.7.1",
2626
"clsx": "^2.1.1",
27+
"jszip": "^3.10.1",
2728
"lucide-react": "^0.553.0",
2829
"next": "^16.0.7",
2930
"react": "19.2.0",

web/src/app/dossiers/page.tsx

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type { UtxoDossier, ProofArchive } from "@/core/dossier/types";
1616
import { getHeaderByHeight } from "@/core/headers/store";
1717
import { Beef } from "@bsv/sdk";
1818
import { Label } from "@/components/ui/label";
19+
import JSZip from "jszip";
1920

2021
type SortOption = "newest" | "oldest" | "value-high" | "value-low" | "label-asc" | "label-desc";
2122

@@ -313,8 +314,38 @@ export default function DossiersPage() {
313314
result.set(iv, salt.length);
314315
result.set(new Uint8Array(encrypted_data), salt.length + iv.length);
315316

316-
blob = new Blob([result], { type: "application/octet-stream" });
317-
filename = `chronicle-utxos-${selected.size}-${new Date().toISOString().slice(0, 10)}.enc`;
317+
// Create zip with encrypted file + decryption tool
318+
const zip = new JSZip();
319+
const baseName = `chronicle-utxos-${selected.size}-${new Date().toISOString().slice(0, 10)}`;
320+
zip.file(`${baseName}.enc`, result);
321+
322+
// Fetch and include the decryption tool
323+
const decryptToolResponse = await fetch("/decrypt-tool.html");
324+
const decryptToolHtml = await decryptToolResponse.text();
325+
zip.file("decrypt-tool.html", decryptToolHtml);
326+
327+
// Add a README
328+
zip.file("README.txt", `Chronicle Cold Vault - Encrypted Export
329+
========================================
330+
331+
This archive contains:
332+
- ${baseName}.enc - Your encrypted UTXO data
333+
- decrypt-tool.html - Standalone decryption tool
334+
335+
To decrypt:
336+
1. Open decrypt-tool.html in any modern web browser
337+
2. Select the .enc file
338+
3. Enter your passphrase
339+
4. Download the decrypted JSON
340+
341+
Encryption: AES-256-GCM with PBKDF2 (100,000 iterations, SHA-256)
342+
343+
Exported: ${new Date().toISOString()}
344+
UTXOs: ${selected.size}
345+
`);
346+
347+
blob = await zip.generateAsync({ type: "blob" });
348+
filename = `${baseName}-encrypted.zip`;
318349
} else {
319350
blob = new Blob([json], { type: "application/json" });
320351
filename = `chronicle-utxos-${selected.size}-${new Date().toISOString().slice(0, 10)}.json`;
@@ -385,15 +416,7 @@ export default function DossiersPage() {
385416
autoComplete="new-password"
386417
/>
387418
<p className="text-xs text-muted-foreground">
388-
💡 Save the{" "}
389-
<a
390-
href="/decrypt-tool.html"
391-
target="_blank"
392-
className="text-primary underline"
393-
>
394-
standalone decryption tool
395-
</a>{" "}
396-
alongside your encrypted backup for future access without Chronicle.
419+
� Encrypted exports include a standalone decryption tool in the .zip for future-proof access.
397420
</p>
398421
</div>
399422
)}

web/src/app/export/page.tsx

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { useHeaderStore } from "@/contexts/header-store-context";
1515
import { exportHeaderStore, importHeaderStore } from "@/core/headers/store";
1616
import { markExportPerformed, saveDossiersBatch } from "@/core/dossier/store";
1717
import type { UtxoDossier, ProofArchive, Bucket } from "@/core/dossier/types";
18+
import JSZip from "jszip";
1819

1920
type ArchiveData = {
2021
version: number;
@@ -147,17 +148,48 @@ export default function ExportPage() {
147148
result.set(iv, salt.length);
148149
result.set(new Uint8Array(encrypted), salt.length + iv.length);
149150

150-
const blob = new Blob([result], { type: "application/octet-stream" });
151+
// Create zip with encrypted file + decryption tool
152+
const zip = new JSZip();
153+
const baseName = `chronicle-archive-${new Date().toISOString().slice(0, 10)}`;
154+
zip.file(`${baseName}.enc`, result);
155+
156+
// Fetch and include the decryption tool
157+
const decryptToolResponse = await fetch("/decrypt-tool.html");
158+
const decryptToolHtml = await decryptToolResponse.text();
159+
zip.file("decrypt-tool.html", decryptToolHtml);
160+
161+
// Add a README
162+
zip.file("README.txt", `Chronicle Cold Vault - Encrypted Full Backup
163+
=============================================
164+
165+
This archive contains:
166+
- ${baseName}.enc - Your encrypted Chronicle data (dossiers, BEEF proofs, headers)
167+
- decrypt-tool.html - Standalone decryption tool
168+
169+
To decrypt:
170+
1. Open decrypt-tool.html in any modern web browser
171+
2. Select the .enc file
172+
3. Enter your passphrase
173+
4. Download the decrypted JSON
174+
175+
Encryption: AES-256-GCM with PBKDF2 (100,000 iterations, SHA-256)
176+
177+
Exported: ${new Date().toISOString()}
178+
Dossiers: ${dossiers.length}
179+
BEEF Archives: ${archives.length}
180+
`);
181+
182+
const blob = await zip.generateAsync({ type: "blob" });
151183
const url = URL.createObjectURL(blob);
152184
const a = document.createElement("a");
153185
a.href = url;
154-
a.download = `chronicle-archive-${new Date().toISOString().slice(0, 10)}.enc`;
186+
a.download = `${baseName}-encrypted.zip`;
155187
a.click();
156188
URL.revokeObjectURL(url);
157189

158190
// Mark export performed for reminder tracking
159191
markExportPerformed(dossiers.length);
160-
setStatus("Encrypted archive exported successfully.");
192+
setStatus("Encrypted archive exported successfully (zip with decryption tool).");
161193
setPassphrase("");
162194
} catch (e) {
163195
setStatus(`Encryption error: ${e instanceof Error ? e.message : "Unknown"}`);
@@ -434,6 +466,13 @@ export default function ExportPage() {
434466
)}
435467
</div>
436468
<Button onClick={handleExportEncrypted}>Export Encrypted</Button>
469+
<p className="text-xs text-muted-foreground">
470+
📦 Encrypted exports are bundled as a .zip with a standalone{" "}
471+
<a href="/decrypt-tool.html" target="_blank" className="text-primary underline">
472+
decryption tool
473+
</a>{" "}
474+
for future-proof access without Chronicle.
475+
</p>
437476
</CardContent>
438477
</Card>
439478

0 commit comments

Comments
 (0)