Published Feb 23, 2026
Portable Secret: End-to-End Encrypted Files in One HTML
There is a recurring security UX problem that sounds simple and turns into chaos the moment real users touch it:
I need to send something sensitive to someone who is not in our system.
You can send a password-protected zip, but then you are teaching non-technical users how to install archive tools and pray they picked a safe cipher.
You can upload to a secure portal, but now you need account creation, link expiration, storage policy, and support docs.
You can use messaging apps, but compliance usually says no.
We wanted something almost boring in its simplicity:
- one file
- no account
- no server-side decrypt path
- works offline in a modern browser
Portable Secret is our answer: a self-contained .html file that contains encrypted payload bytes plus the code to decrypt them locally.
The core inspiration came from mprimi.github.io/portable-secret. We kept the spirit, then pushed on product-grade details: richer payloads, stronger KDF options, better progress UX, and a build pipeline that emits a genuinely portable artifact.
Part 1: The Product Constraint
We imposed one hard rule early:
The recipient must be able to decrypt with only a password and a browser.
No extensions. No native app. No backend key exchange. No online dependency.
That single decision drives almost everything else:
- The file must include both encrypted data and decrypt runtime.
- Key derivation must happen fully in the browser.
- UX has to survive slow devices without feeling frozen.
- The format has to be forward-compatible because files may be opened months later.
This is not just cryptography. It is cryptography plus “will this still work when forwarded between five people and opened on a random laptop?”
Part 2: What Is Inside the File
A generated Portable Secret file is still just HTML, but it embeds two key blocks:
<!--PS:METADATA:...--><!--PS:PAYLOAD:...-->
The metadata block tells the opener how to derive the key and what kind of payload to expect.
The payload block is AES-GCM ciphertext in base64.
On open, the script does this:
- Parse metadata and payload from HTML comments.
- Ask user for password.
- Derive key with the KDF specified in metadata.
- Decrypt ciphertext with AES-256-GCM.
- Optionally decompress gzip.
- Render text and downloadable files.
No network calls are needed for decrypt. Everything runs locally.
Part 3: Cryptography Choices (and Why We Offer Two KDFs)
For encryption we use AES-256-GCM with a random 96-bit IV per file.
The interesting choice is key derivation.
We support:
- PBKDF2-SHA256 as the compatibility baseline.
- Argon2id as the preferred memory-hard option on capable devices.
Why both? Because “strongest possible” and “works everywhere” are different axes.
Argon2id is usually the better security/performance tradeoff, but it can be rough on low-memory devices. PBKDF2 is universally available through Web Crypto and gives us a reliable fallback path.
The selected KDF and parameters are embedded in metadata, so each file is self-describing.
Part 4: The UX Problem Nobody Mentions
Browser crypto has a known anti-feature: if KDF takes 10-120 seconds, users assume the app crashed.
We solved this with calibration.
Before decrypt starts, we run a small baseline derivation and estimate “iterations per millisecond” on the current device. That estimate powers:
- a pre-decrypt time hint (“~2m”)
- a simulated progress bar during key derivation
- elapsed and remaining time labels
The same idea is used during file creation to tune iteration counts to a target duration (for example 1 second, 5 seconds, or 2 minutes).
So instead of freezing and praying, users see predictable progress.
Part 5: Payloads as a Structured Bundle
We chose not to encrypt “just one blob.”
Instead, plaintext is a versioned JSON bundle:
- optional text content
- list of files (
name,mime,size,dataB64) - bundle version metadata
That bundle can be gzip-compressed before encryption.
Why this format works well:
- Multi-file support is first-class.
- Payload previews (name, size, file count) are available from metadata before decrypt.
- Versioning lets us evolve the format without breaking older files.
In practice, this turns Portable Secret from “encrypted note” into “encrypted package.”
Part 6: Keeping the UI Responsive Under Argon2
Argon2id can be computationally expensive. Running it on the main thread is an easy way to create a bad experience.
So for Argon2 secrets, we inline the Argon2 implementation and execute derivation in a Web Worker when possible. The main thread stays responsive and the progress UI keeps updating.
If worker execution is unavailable, we still have a main-thread fallback. It is slower UX-wise, but it keeps the format resilient.
Small implementation detail, big product impact.
Part 7: Security and Human Factors
A few details are deliberately opinionated:
- Password hints are optional and stored unencrypted for usability.
- Green theme after successful decrypt gives a clear visual state change.
- No server-side decrypt path reduces accidental plaintext exposure in backend logs or storage.
None of this replaces good password hygiene, but these touches reduce user mistakes and support tickets.
Part 8: Build System Split (Create vs Open)
The architecture is intentionally split:
- Create side (Svelte app): collects inputs, builds the bundle, encrypts bytes, and emits one HTML download.
- Open side (template runtime): plain inlined JS/CSS that works in a bare document with no framework runtime.
Template JS is bundled once (esbuild), template CSS is generated once (Tailwind), and both are injected into placeholder slots in template.html together with metadata and ciphertext.
The result is exactly what we wanted from the beginning: a standalone artifact.
Tradeoffs We Accept
Portable Secret is not “free security”:
- Large attachments produce large HTML files.
- Browser memory limits are still browser memory limits.
- Client-side crypto strength is bounded by user password quality.
But compared to “send plaintext over email and hope,” this is a significant practical improvement with almost zero recipient friction.
Closing Thought
The best security tools are often the ones that remove decisions, not add them.
Portable Secret gives users a familiar primitive (a file) and upgrades its behavior: encrypted by default, decrypted locally, portable by design.
It is not a replacement for full secure collaboration platforms. It is a tactical tool for the messy real-world moment when someone just needs to send a secret safely, now, without onboarding a new system.