Skip to content

Commit 4b2e587

Browse files
committed
docs: add CSV export security FAQ and remove hard wrapping
1 parent 45f933a commit 4b2e587

1 file changed

Lines changed: 57 additions & 26 deletions

File tree

CSV_EXPORT.md

Lines changed: 57 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ Last updated: 2026-05-23
44

55
## Purpose
66

7-
This document is the long-lived technical reference for CSV export delivery.
8-
ADR 004 records the decision; this file explains how the export architecture
9-
works in practice and how to plan capacity as datasets evolve.
7+
This document is the long-lived technical reference for CSV export delivery. ADR 004 records the decision; this file explains how the export architecture works in practice and how to plan capacity as datasets evolve.
108

119
## Scope
1210

@@ -20,13 +18,10 @@ works in practice and how to plan capacity as datasets evolve.
2018

2119
The export bundle is built from canonical retained files:
2220

23-
- `storage.CSV_REGISTRY` files:
24-
`traffic-log.csv`, `traffic-daily.csv`, `traffic-snapshots.csv`,
25-
`traffic-referrers.csv`, `traffic-paths.csv`, `repo-metrics.csv`
21+
- `storage.CSV_REGISTRY` files: `traffic-log.csv`, `traffic-daily.csv`, `traffic-snapshots.csv`, `traffic-referrers.csv`, `traffic-paths.csv`, `repo-metrics.csv`
2622
- `manifest.json`
2723

28-
This includes repos currently excluded from dashboard rendering, because export
29-
is for retained-data portability and parity.
24+
This includes repos currently excluded from dashboard rendering, because export is for retained-data portability and parity.
3025

3126
### 2) Publish-time build pipeline
3227

@@ -36,8 +31,7 @@ On encrypted publish:
3631
2. Compute `plaintext_sha256` of ZIP bytes.
3732
3. Encrypt ZIP with AES-GCM under a PBKDF2-derived key from dashboard secret.
3833
4. Compute `ciphertext_sha256`.
39-
5. Write encrypted asset to a content-addressed path:
40-
`assets/export-data-<digest16>.enc`.
34+
5. Write encrypted asset to a content-addressed path: `assets/export-data-<digest16>.enc`.
4135
6. Embed only compact export metadata in `index.html` (`export-manifest`).
4236

4337
### 3) Runtime export pipeline (browser)
@@ -60,22 +54,20 @@ No plaintext export bytes are uploaded back to GitHub by this path.
6054
- `plaintext_sha256`: confirms decrypted bytes match publish-time canonical ZIP.
6155

6256
Important boundary:
57+
6358
- These controls protect payload integrity at export time.
6459
- They do not replace trust requirements for build/deploy/runtime supply chain.
6560

6661
## Caching and Asset Versioning
6762

6863
- Export asset filenames are content-addressed to avoid stale-cache mismatches.
69-
- Manifest asset path validation is strict:
70-
`assets/export-data-[a-f0-9]{16}.enc`.
64+
- Manifest asset path validation is strict: `assets/export-data-[a-f0-9]{16}.enc`.
7165

7266
## Offline Behavior
7367

7468
- `file://` export is best-effort, not guaranteed across browsers.
75-
- If browser fetch/CORS behavior blocks local-file export, runtime fails closed
76-
with explicit error messaging.
77-
- Recommended fallback: hosted Pages URL or serving extracted artifact files
78-
over local HTTP.
69+
- If browser fetch/CORS behavior blocks local-file export, runtime fails closed with explicit error messaging.
70+
- Recommended fallback: hosted Pages URL or serving extracted artifact files over local HTTP.
7971

8072
## Performance Posture
8173

@@ -86,8 +78,7 @@ Hard architectural budgets:
8678

8779
Operational target:
8880

89-
- Keep click-to-download within an interactive desktop range for the reference
90-
profile (`R=50`, `D=90`, `C=1`) as features evolve.
81+
- Keep click-to-download within an interactive desktop range for the reference profile (`R=50`, `D=90`, `C=1`) as features evolve.
9182

9283
## Size Estimation Framework
9384

@@ -141,17 +132,14 @@ Let `rho` be ZIP compression ratio (`S_zip / S_raw`).
141132

142133
- Typical CSV-heavy range: `rho ~= 0.25..0.45`
143134
- `S_zip ~= rho * S_raw`
144-
- `S_enc ~= S_zip + 16 + O(1KB)`
145-
(`16` is AES-GCM tag bytes; metadata is small and near-constant)
135+
- `S_enc ~= S_zip + 16 + O(1KB)` (`16` is AES-GCM tag bytes; metadata is small and near-constant)
146136

147137
### Worked baseline (current schema profile)
148138

149139
Representative row-byte profiles used internally:
150140

151-
- Typical: `b_log=73`, `b_snap=69`, `b_daily=73`, `b_metric=221`, `b_ref=61`,
152-
`b_path=121`
153-
- Conservative: `b_log=120`, `b_snap=110`, `b_daily=120`, `b_metric=300`,
154-
`b_ref=100`, `b_path=260`
141+
- Typical: `b_log=73`, `b_snap=69`, `b_daily=73`, `b_metric=221`, `b_ref=61`, `b_path=121`
142+
- Conservative: `b_log=120`, `b_snap=110`, `b_daily=120`, `b_metric=300`, `b_ref=100`, `b_path=260`
155143

156144
For `R=50`, `D=90`, `C=1`, `W=14`, `q_r=q_p=10`:
157145

@@ -180,6 +168,49 @@ When a new retained table/metric is added:
180168
- Export manifest exists in encrypted dashboard HTML.
181169
- Export asset is generated and content-addressed.
182170
- Decrypted export ZIP reproduces canonical retained files.
183-
- Integrity failures (ciphertext size/digest, plaintext digest, wrong key) fail
184-
closed with no partial plaintext output.
171+
- Integrity failures (ciphertext size/digest, plaintext digest, wrong key) fail closed with no partial plaintext output.
185172
- Pre-unlock export remains unavailable.
173+
174+
## FAQ
175+
176+
### Can someone use devtools to trigger CSV export before unlocking the dashboard?
177+
178+
Not in a way that yields plaintext data.
179+
180+
The export flow is wired after successful unlock, and the runtime keeps an explicit key gate (`unlockedDashboardKey`) before any export work proceeds. Even if a user manually invokes JavaScript in devtools, they still cannot produce plaintext ZIP bytes without the correct dashboard secret.
181+
182+
### If someone forces the click path early, will plaintext ZIP download anyway?
183+
184+
No.
185+
186+
The downloadable asset is encrypted (`.enc`). Plaintext ZIP is only produced after successful decryption with the correct key, followed by digest checks. Without the key, decryption fails and no valid plaintext ZIP download is produced by the supported runtime path.
187+
188+
### What checks does the browser run before download?
189+
190+
For encrypted exports, the browser verifies:
191+
192+
1. Ciphertext size matches manifest.
193+
2. Ciphertext SHA-256 matches manifest (`ciphertext_sha256`).
194+
3. AES-GCM decrypt succeeds with the provided secret-derived key.
195+
4. Decrypted ZIP SHA-256 matches manifest (`plaintext_sha256`).
196+
197+
Only then does the browser trigger the ZIP download.
198+
199+
### If the browser already verifies everything, why offer manual checksum copy?
200+
201+
Operational trust and auditability.
202+
203+
Some users want an extra independent verification step or need to share verification artifacts in support/debug workflows. The UI can copy a checksum line (`<sha256> <filename>`) so users can run local checks (for example `shasum -a 256 <file>.zip`, or checksum-file workflows with `-c`).
204+
205+
### Does this protect against all attacks?
206+
207+
No. It protects export payload integrity within the client-side model, but it does not replace broader supply-chain or endpoint trust.
208+
209+
Examples outside scope include malicious runtime JS, compromised CI/deploy pipeline, compromised browser/device, or weak secrets in `casual` mode.
210+
211+
### What is the practical privacy difference between `strong` and `casual` here?
212+
213+
Both modes keep export payload encrypted at rest/in transit in published assets. The difference is key strength policy.
214+
215+
- `strong`: requires high-entropy secret and is intended to resist offline guessing.
216+
- `casual`: allows weak/non-empty secrets and is not intended to resist determined offline brute-force attempts.

0 commit comments

Comments
 (0)