Password Protecting Static Site Content with WASM
Heads Up
Security and encrypted are super complicated. I'm comfortable with this process given my understanding of the topics and the fact that I'm using a library for the encryption. But, I'm not an expert. You should consult with one if you're hosting critical content.
Introduction
I use static site generators for my sites. I call out to external APIs to do anything dynamic. The exception is password protection. I'd always thought that needed to be done dynamically on a site's host server. I realized with WASM that's not the case. Files can be encrypted and served statically with a WASM process providing decryption directly on the front end.
An Example
I've encrypted a file and put copy on my server. It's encrypt-decrypt-example.txt.bin. The file is static. You can download it directly. It won't do you much good, though. Being encrypted and all.
Enter the password "wasm" here and hit the "Decrypt File" button to view the content.
If you got the password right (it's all lowercase), you'll see the text "This is the content of the encrypted file." If you got it wrong, you'll get an incorrect password message.
How It Works
HTML
The HTML for the page boils down to a form with an input for the password, a submit button, and a hidden field to hold the URL of the file to decrypt.
Password:
waiting for output
JavaScript
The JavaScript is a module that does four things:
- Imports the WASM module responsible for the decryption.
- Creates a function to handle form submissions that trigger the decryption.
-
Creates a
main()function that initializes the WASM module and adds form event listeners. -
Calls the
main()function to kick things off.
When a form submission fires, the script
parses the incoming FormData
(lines 7-14), does
a fetch to pull in the encrypted file (starting
on line 15), sends it
to the wasm module (lines 21-24),
then outputs the response (lines 25 and 26).
The JavaScript/WASM Connection
Communication between the JavaScript's
handleDecryptTextFileForm() function
and the WASM module is done by passing
an arrays of bytes back and forth.
This is done by loading the fetch response
as a .blob() (line 21), grabbing
it's .arrayBuffer() (line 22), and
turning it into a Uint8Array
(line 23)
to send to the WASM decrypt_file
function (line 24).
The bytes that come back from the WASM module
are turned back in to text with TextDecoder()
(line 25)
and output to the page via an update to the result
element's
.innerHTML (line 26).
The WASM Code
I wrote the WASM module in Rust. It's basically just a wrapper around a library called orion that handles all the encryption work. The code looks like this:
src/lib.rs
use ;
use *;
The only other file in the Rust project
is the Cargo.toml file:
Cargo.toml
[package]
name = "static-site-file-decryption"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
getrandom = { version = "0.4.2", features = ["wasm_js"] }
orion = "0.17.13"
wasm-bindgen = "0.2.117"
The project is built with wasm-pack
(which I installed with cargo install wasm-pack).
I use this script to clean the pkg
directory it outputs to before each run:
Build Script
#!/bin/bash
[ &&
[ && Usage
This version of the tool is a prototype I built to figure out how everything works. The longer term goal is to build a version that works with multiple files instead of one at a time.
That said, this version works if you only need to deal with one file at a time. Because it's WASM, you don't need to compile the Rust code yourself. You can download these two files and line up the paths with the JavaScript above to set it up yourself.
You'll also need a way to encrypt files. I've set up a tool to do that at here: Static Site File Encryption Tool.
Wrap-up
As evident by the fact that I've been running a static site for like decade without a password feature, it's not something I have a lot of need for. There's been a few things over the years that I haven't done because of it. It's not a lot, but now I've got a solution.
-a
Endnotes
- To reiterate: I'm not a security professional. I'm comfortable with this code primarily because it uses the orion library which has over 9 million downloads. I'm also not protecting anything critical.
-
It's possible to protect pages this way without using WASM using the Crypto Web API. I looked at it before, but got overwhelmed by it. Not least because of this big ol' warning on the page:
Warning: The Web Crypto API provides a number of low-level cryptographic primitives. It's very easy to misuse them, and the pitfalls involved can be very subtle.
Even assuming you use the basic cryptographic functions correctly, secure key management and overall security system design are extremely hard to get right, and are generally the domain of specialist security experts.
Errors in security system design and implementation can make the security of the system completely ineffective.
- PageCrypt uses the Web Crypto API. It's a bit of a different thing though. It's designed to serve up a full single page app.
- This JavaScript is all vanilla. The code you see in the samples is the actual code powering the page. No extra JavaScript tooling is required.
- This example works with form input where the password is entered each time. I'm working on a more full featured version where the password only needs to be entered once and let you encrypt all the content for a single or multi-page app.
- The file for this example was encrypted with the tool on the static-site-file-encryption page. It's also designed to work with one file at a time. Part of the new project is building a tool to encrypt an entire directory of content in one run.
- It should be possible to encrypt images as well. The biggest difference I can think of is the error handling (i.e. you need some way to deliver the image if it decrypts and a message if it doesn't). I expect that should be strait forward to do by returning an object with a status that lets you switch on the output. That's something I'll address in the new project.
References
- https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
- https://developer.mozilla.org/en-US/docs/Web/API/Blob
- https://developer.mozilla.org/en-US/docs/Web/API/Response
- https://developer.mozilla.org/en-US/docs/Web/API/Response/blob
- https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder
- https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API
- https://doc.libsodium.org/password_hashing/default_phf#guidelines-for-choosing-the-parameters
- https://docs.rs/getrandom/latest/getrandom/#webassembly-support
- https://docs.rs/orion/latest/orion/
- https://docs.rs/orion/latest/orion/aead/fn.open.html
- https://docs.rs/orion/latest/orion/aead/fn.seal.html
- https://docs.rs/orion/latest/orion/aead/index.html
- https://docs.rs/orion/latest/orion/aead/struct.SecretKey.html
- https://docs.rs/orion/latest/orion/aead/struct.SecretKey.html#method.from_slice
- https://docs.rs/orion/latest/orion/kdf/index.html
- https://docs.rs/orion/latest/orion/kdf/struct.Salt.html
- https://docs.rs/orion/latest/orion/kdf/struct.SecretKey.html
- https://docs.rs/wasm-bindgen/latest/wasm_bindgen/
- https://github.com/Greenheart/pagecrypt
- https://github.com/orion-rs/orion