hash()
One async function over the four SHA digests Web Crypto exposes, with the output shape you ask for.
import { hash } from "unsecure/hash";
await hash("hello world");
// "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"#Signature
function hash(
data: string | BytesSource,
options?: {
algorithm?: "SHA-1" | "SHA-256" | "SHA-384" | "SHA-512";
returnAs?: "hex" | "base64" | "b64" | "base64url" | "b64url" | "uint8array" | "bytes";
},
): Promise<string | Uint8Array>;#Options
| Option | Type | Default | What it does |
|---|---|---|---|
algorithm | DigestAlgorithm | "SHA-256" | Which digest to compute. Matched case-insensitively, so "sha-256" works. |
returnAs | DigestReturnAs | mirrors the input type | The shape of the result. |
SHA-1 is there for interoperability with systems that still require it, such as the default OTP algorithm. Do not pick it for anything new.
#Return shape
| You passed | You get back |
|---|---|
a string | a hex string |
a BytesSource | a Uint8Array |
anything, plus returnAs | exactly what returnAs names |
await hash("hello world", { returnAs: "base64" });
// "uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
await hash("hello world", { returnAs: "base64url" });
// "uU0nuZNNPgilLlLX2n2r-sSE7-N6U4DukIj3rOLvzek"
await hash("hello world", { algorithm: "SHA-512" });
// "309ecc489c12d6eb4cc40f50c902f2b4d0ed77ee511a7c7a9bcd3ca86d4cd86f989dd35bc5ff…"
await hash(new TextEncoder().encode("hello world"));
// Uint8Array(32) [185, 77, 39, 185, …]
await hash(new TextEncoder().encode("hello world"), { returnAs: "hex" });
// "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"Strings are encoded as UTF-8 before hashing.
#Errors
| Cause | code |
|---|---|
data is neither text nor bytes | INVALID_TYPE |
algorithm is not one of the four | UNSUPPORTED |
returnAs is not a name the library knows | UNSUPPORTED |
| Web Crypto refused the digest after the checks passed | PLATFORM |
Both UNSUPPORTED checks run before Web Crypto is reached, so a typo costs nothing. See Errors.
#Recipe: store an API token without storing the token
You have to be able to check a token a client sends. You do not have to be able to read it back. Store the digest.
import { hash } from "unsecure/hash";
import { secureGenerate } from "unsecure/generate";
import { secureCompare } from "unsecure/compare";
// Issue: show `token` to the user once, keep only the digest
const token = secureGenerate({ length: 48, specials: false });
const tokenHash = await hash(token);
await db.tokens.insert({ userId, tokenHash });
// Check, on each request
async function findToken(received: string) {
const receivedHash = await hash(received);
const row = await db.tokens.findByHash(receivedHash);
if (!row) return null;
return secureCompare(row.tokenHash, receivedHash) ? row : null;
}A plain SHA-256 is the right tool here and the wrong tool for a password, because a token has full random entropy and a password does not. See Password hashing for the other case.
#Recipe: notice that content changed
import { hash } from "unsecure/hash";
const etag = await hash(JSON.stringify(document), { returnAs: "base64url" });
if (etag === request.headers.get("if-none-match")) return new Response(null, { status: 304 });#Pitfall: large files and streams
hash() calls crypto.subtle.digest, which needs the whole input in memory. Web Crypto has no incremental digest, so there is nothing the library can do about it.
// Loads the entire file into memory
const fileHash = await hash(hugeFileBuffer);For streams, reach for the runtime's own API: crypto.createHash() in Node, crypto.subtle.digestStream() where it exists.
#Pitfall: comparing digests with ===
A digest is not secret, but the value you compare it against often is. Use secureCompare whenever the comparison decides whether a request is authorized.