crypto.subtle
The Web Cryptography API in windows and workers, in secure contexts: CryptoKey and all the operations of SubtleCrypto. The
computations are on core:crypto, in the browser; the JavaScript engine never holds a key's material except while a key is being
exported or cloned. It costs nothing until a script calls an operation (native/js/odin/web_crypto.js is evaluated then).
Checked: amazon.com (its scripts call importKey; without it the page stayed empty), and web-platform-tests WebCryptoAPI
(wpt/expectations).
| Layer | File |
|---|---|
| the description | api/crypto.widl (generates wj.wr_CryptoKey_*, wj.wr_SubtleCrypto_* and the host table) |
| the browser side | src/page/bindings_crypto_subtle.odin (keys, DER, computations), src/page/bindings_crypto.odin (random values, digest) |
| the realm side | native/js/odin/web_crypto.odin (CryptoKey, the natives) and web_crypto.js (conversions, normalization, usages, error names, JWK) |
| between realms | native/js/odin/clone.odin (Crypto_Key) |
| the tests | examples/jsweb/webcrypto.js (jsweb_webcrypto), web-platform-tests WebCryptoAPI (wpt/expectations) |
web_crypto.js, like web_wasm.js, is an exception to the rule of native/js/odin/prelude.odin (no JavaScript in the realm): it
is not in the image, and its source is stripped.
What there is
| Algorithm | Operations | Formats |
|---|---|---|
| AES-GCM, AES-CBC, AES-CTR | encrypt, decrypt, generateKey, wrapKey, unwrapKey | raw, jwk |
| AES-KW | wrapKey, unwrapKey, generateKey | raw, jwk |
| HMAC | sign, verify, generateKey | raw, jwk |
| PBKDF2, HKDF | deriveBits, deriveKey | raw (import only) |
| ECDSA (P-256, P-384, P-521) | sign, verify, generateKey | raw (public), spki, pkcs8, jwk |
| ECDH (P-256, P-384, P-521) | deriveBits, deriveKey, generateKey | raw (public), spki, pkcs8, jwk |
| RSASSA-PKCS1-v1_5, RSA-PSS | sign, verify, generateKey | spki, pkcs8, jwk |
| RSA-OAEP | encrypt, decrypt, wrapKey, unwrapKey, generateKey | spki, pkcs8, jwk |
| Ed25519 | sign, verify, generateKey | raw (public), spki, pkcs8, jwk |
| X25519 | deriveBits, deriveKey, generateKey | raw (public), spki, pkcs8, jwk |
Hashes: SHA-1, SHA-256, SHA-384, SHA-512.
The design
- The material stays in the browser. A
Crypto_Keyis an object of the page (CryptoKeyData); the realm holds a handle. It does not change once made and is counted, so a computation on another thread keeps it alive if the realm lets go of it. extractableis checked twice.web_crypto.jsthrows the specification's error;CryptoKey_exportrefuses what is secret in a key that is not extractable, whatever the realm asks. Usages are checked in the realm only.- Off the page's thread. RSA, the curves, PBKDF2 and symmetric work on inputs of
crypto.digest_offload_bytesor more run on the blocking pool; a worker has no pool and works on its own thread. The promise settles from a task either way. - The library's
ensures end the process, sobindings_crypto_subtle.odinchecks sizes before it callscore:crypto, even whatweb_crypto.jshas checked. - P-521 is RustCrypto's (
p521, innative/crypto, behindsrc/page/crypto_p521.odin):core:cryptohas only P-256 and P-384, and this curve's arithmetic is not one to write by hand. The material stays in theCrypto_Keyand goes to the library for the call; hashes and random bytes are the browser's. Its nonces are RustCrypto's deterministic ones, not the bytes of RFC 6979's examples for this curve: signatures verify, in both directions, against that RFC's. - What
core:cryptodoes not do is here: CBC and AES-KW over its ECB; CTR's counter oflengthbits, in two runs of its 128-bit one; GCM tags shorter than 128 bits, by sealing twice (the keystream is the same both ways); DER, read and written by hand; compressed points, throughcore:crypto/_weierstrass. CryptoKeyis native (generated fromwebcrypto.idl,wjr_CryptoKey_get_*): it holds a state made byweb_crypto.jsthat a page never sees. Whatkey.algorithmandkey.usagesreturn are copies: changing them changes nothing.- Structured serialization (
structuredClone, messages, IndexedDB, history) writes the key's description andCryptoKey_serialize's bytes, for a key that is not extractable too: a script does not see them, and a key must survive in IndexedDB. These bytes are the one way a non-extractable key's material enters the engine's memory, and what IndexedDB writes is not encrypted.
What is not there
- RSA moduli over 4096 bits, public exponents over 32 bits (the library's limits):
DataError.generateKeymakes only the exponent 65537 (OperationErrorotherwise), of 1024 to 4096 bits. RSA keys of more than two primes:NotSupportedError. A private JWK needs all ofp,q,dp,dq,qi. - The algorithms of
modern-algos(Ed448, X448, SHA-3, ML-KEM, ML-DSA, AES-OCB, ChaCha20-Poly1305, Argon2, KMAC). - Stopping a computation: PBKDF2 with a huge iteration count holds a thread of the pool until it ends, and the browser's exit
waits for it (
host.blocking_destroy).