Back to ShipLog
🚀 Feature DropFebruary 22, 20265 min read

Zero-Knowledge Document Editor: Edit XLSX & DOCX In-Browser with Full E2EE

Edit spreadsheets with Fortune-Sheet and Word documents with Syncfusion — all client-side. Documents are decrypted, edited, re-encrypted, and saved back to R2 without plaintext ever touching the server.

By TradeStance Engineering
securitye2eedocument-editorfortune-sheet

The Problem

TradeDrive had read-only viewers for DOCX (docx-preview), XLSX (SheetJS), and PDF (pdf.js) inside SecureFilePreview. Users could preview documents but had to download, edit locally, and re-upload to make changes. For encrypted vault files, this broke the zero-knowledge chain — plaintext would exist on disk during the edit cycle.

In-Browser Spreadsheet Editor

Fortune-Sheet, a maintained React fork of Luckysheet, provides a full-featured spreadsheet UI with cell editing, formatting, and multi-sheet support. SheetJS parses the uploaded .xlsx into JSON cell data, which Fortune-Sheet renders as an interactive grid. On save, ExcelJS converts the Fortune-Sheet state back into a valid .xlsx ArrayBuffer. The entire pipeline runs in the browser — no server round-trips for the document content.

In-Browser Word Editor

Syncfusion DocumentEditorContainer opens .docx files directly in a rich Word-like editor with a Ribbon toolbar. No serviceUrl is configured, making it a pure client-side editor. Documents are exported as .docx blobs via saveAsBlob('Docx'). Syncfusion styles are loaded from CDN only when the editor is opened, keeping the main bundle lean.

E2EE Round-Trip

The EditorContainer orchestrates the full zero-knowledge flow: fetch the encrypted blob from R2, prompt for the vault passphrase, decrypt with OpenPGP.js (Argon2id-hardened key), pass the plaintext ArrayBuffer to the editor, then on save re-encrypt with the user's public key before uploading back. The server only ever sees ciphertext. Decrypted buffers are zeroed on unmount via clearBuffer().

R2 Multipart Upload

For files larger than 5 MB, the client splits the encrypted blob into 5 MB chunks and uploads them via three new worker endpoints that wrap R2's native multipart API: upload-init (createMultipartUpload), upload-part (uploadPart), and upload-complete (complete). Files under 5 MB use a single PUT to /drive/files/:id/save. Both paths overwrite the existing R2 object at the same key, so shared links remain valid.

Code-Splitting & Memory

Performance and security measures:

  • All editor code is dynamically imported via React.lazy and next/dynamic with ssr: false — Fortune-Sheet, ExcelJS, and Syncfusion are never included in the initial bundle.
  • useBeforeUnloadWarning prevents accidental tab closes when there are unsaved changes.
  • clearBuffer() zeroes out ArrayBuffers on unmount to minimize plaintext exposure in memory.
  • Object URLs are revoked on cleanup to prevent memory leaks.

File Explorer Integration

An Edit button (pencil icon) now appears on .xlsx and .docx files in both list and grid view. Clicking it navigates to /email/drive/edit?fileId=X&type=xlsx|docx, a dedicated full-screen editor page. Non-editable files (PDF, images, CSV) show no Edit button. The getEditorType() utility maps content types and file extensions to the correct editor.

Related Help GuideDocument Editor Guide
Open Guide

Was this update useful?

#security#e2ee#document-editor#fortune-sheet#syncfusion#r2-multipart