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.subtle need a secure context, a page served over HTTPS or from localhost: hash, hmac, hmacVerify, importHmacKey, hkdf, importHkdfKey, hotp, hotpVerify, totp and totpVerify. Elsewhere they throw PLATFORM.
  • Everything else works in any context, including the Argon2 functions, generateOTPSecret, otpauthURI, random values, UUIDs and secureGenerate.
  • 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's package.json.
  • Module script: import only works inside <script type="module">, so a plain <script> won't do.
  • Bytes in, hex out: hash takes the file's ArrayBuffer as it is. Bytes in would give bytes back, so returnAs: "hex" asks for the text a download page shows.

#When it fails

You'll see each of these in the browser console:

SituationWhat happens
Page on plain HTTP, from a host other than localhosthash 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 URLThe import fails, and nothing in that module script runs.
Argon2 at default cost on the pageThe 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>/". Then import { 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.