Use unsecure from a CDN
Load one module of unsecure into a plain HTML page from esm.sh, pinned to an exact version.
Sometimes a page is one HTML file with no build step, and that's fine: the browser can import the package straight from a CDN. Each module has its own door, so asking for unsecure/hash downloads the hash module and the few internals it needs, not the whole library.
#Before you start
- Functions that call
crypto.subtleneed a secure context, a page served over HTTPS or fromlocalhost:hash,hmac,hmacVerify,importHmacKey,hkdf,importHkdfKey,hotp,hotpVerify,totpandtotpVerify. Elsewhere they throwPLATFORM. - Everything else works in any context, including the Argon2 functions,
generateOTPSecret,otpauthURI, random values, UUIDs andsecureGenerate. - Pin an exact version. Without one, esm.sh serves the newest release, and your page changes without a deploy. A pinned page also stays on that release: update the version in the URL when you upgrade.
- A pinned URL still trusts esm.sh to serve what npm published. For a page that handles secrets, install the package and bundle it instead, as Install unsecure describes.
#Hash a file in the browser
This page shows the SHA-256 checksum of a file the user picks, so they can compare it with the one a download page lists. The file never leaves their computer.
checksum.html
<!doctype html>
<meta charset="utf-8" />
<title>File checksum</title>
<input type="file" id="file" />
<output id="checksum"></output>
<script type="module">
import { hash } from "https://esm.sh/unsecure@0.3.4/hash";
const input = document.querySelector("#file");
input.addEventListener("change", async () => {
const [file] = input.files;
if (!file) return;
const digest = await hash(await file.arrayBuffer(), { returnAs: "hex" });
document.querySelector("#checksum").value = digest;
});
</script>Three details make this page work without a build step:
- Subpath and version: the URL names a release and
/hash, so the browser fetches that version of that module and nothing else. The pin on this page follows the library'spackage.json. - Module script:
importonly works inside<script type="module">, so a plain<script>won't do. - Bytes in, hex out:
hashtakes the file'sArrayBufferas it is. Bytes in would give bytes back, soreturnAs: "hex"asks for the text a download page shows.
#When it fails
You'll see each of these in the browser console:
| Situation | What happens |
|---|---|
Page on plain HTTP, from a host other than localhost | hash throws an UnsecureError with code PLATFORM. Its cause is the browser's own error. See Handle errors. |
URL without a subpath, https://esm.sh/unsecure@<version> | Works, but downloads every module, Argon2 included, for one function. |
| Version that does not exist, or a typo in the URL | The import fails, and nothing in that module script runs. |
| Argon2 at default cost on the page | The page freezes until the hash finishes. Run it in a Worker; Store passwords gives the cost. |
#Going further
Other ways to load the same modules, and one warning:
- Import maps: keep the version in one place with
<script type="importmap">mapping"unsecure/"to"https://esm.sh/unsecure@<version>/". Thenimport { hash } from "unsecure/hash"works in every module script on the page, and an upgrade changes one line. - Deno:
import { hash } from "npm:unsecure@<version>/hash"reads the package from npm without a CDN. - Bundlers:
import { hash } from "unsecure"shrinks down to the same module once the bundler drops what you don't use. The subpath only matters for imports over the network. - Not a security boundary: the user controls everything their browser runs, so a check there protects nothing on your server. Verify again on the server whatever the page verified.