crypto.webcrypto

Exposes the standard Web Crypto API, giving access to SubtleCrypto and cryptographically strong randomness.

Signature
crypto.webcrypto
  • Added in Node.js v15.0.0

Return value

CryptoA `Crypto` instance implementing the W3C Web Crypto API, exposing `subtle`, `getRandomValues()` and `randomUUID()`. It is the same object as `globalThis.crypto`.

Errors

Reading the property never throws; individual SubtleCrypto calls reject with NotSupportedError for an unknown algorithm, InvalidAccessError when a key's usages do not permit the operation, and OperationError on malformed input or a failed decryption.

Changelog

  • PHP v15.7.0crypto.subtle was added as a direct alias for crypto.webcrypto.subtle.
  • PHP v16.7.0crypto.randomUUID() was added to the Crypto instance.
  • PHP v18.0.0globalThis.crypto exposes the same object, so require('node:crypto') is no longer needed to reach Web Crypto.
  • PHP v19.0.0The Web Crypto implementation left experimental status and became Stable.
  • PHP v18.4.0Ed25519, Ed448, X25519 and X448 algorithms were added to SubtleCrypto.

crypto.webcrypto is Node's implementation of the same Crypto object browsers expose as window.crypto. Since v18 it is also available as the global globalThis.crypto with no import at all — require('node:crypto').webcrypto === globalThis.crypto is true. Reach for it when the same code has to run in a browser, a service worker, Deno or an edge runtime, where the node:crypto classes do not exist.

What is on it

  • subtle — the SubtleCrypto object holding digest, encrypt, decrypt, sign, verify, generateKey, importKey, exportKey, deriveKey, deriveBits, wrapKey and unwrapKey.
  • getRandomValues(typedArray) — fills a typed array in place, capped at 65536 bytes.
  • randomUUID() — a v4 UUID string.

The friction

Every subtle method is asynchronous and returns a promise, even digest(). Everything speaks ArrayBuffer and TypedArray, never strings: encode with TextEncoder going in and wrap results in Buffer.from() or new Uint8Array() coming out. Keys are opaque CryptoKey objects whose keyUsages are enforced — a key generated for ['sign'] rejects a verify() call — and a key created with extractable: false can never be exported.

For Node-only code, createHash() and friends are shorter and synchronous.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Share one signing or encryption module between a browser bundle and a Node server.
  • Encrypt a field with AES-GCM using a key derived from a password via PBKDF2.
  • Import a JWK from a JWKS endpoint and verify an RS256 or ES256 token with it.
  • Generate a request identifier with crypto.randomUUID() without importing anything.

Frequently asked questions

What is the difference between crypto.webcrypto and node:crypto?

crypto.webcrypto is the standards-based Web Crypto API that also exists in browsers, with promise-returning methods and opaque CryptoKey objects, while the rest of node:crypto is Node's own synchronous, stream-oriented API. Use webcrypto for code that must run in both environments.

Do I need to require node:crypto to use crypto.subtle in Node?

No. Since Node 18, globalThis.crypto is the same Crypto object, so const { subtle } = globalThis.crypto works with no import. require('node:crypto').webcrypto remains available for older versions and for explicit imports.

Why does subtle.digest return a Promise instead of a hash?

Because every SubtleCrypto method is asynchronous by specification, including digest. Await the call and wrap the resulting ArrayBuffer, for example Buffer.from(await subtle.digest('SHA-256', data)).toString('hex').

More examples

Encrypt a field with AES-GCM using a password-derived key

The ciphertext is 22 bytes for a 6-byte plaintext because AES-GCM appends a 16-byte authentication tag; there is no separate tag parameter as there is in node:crypto. The salt and IV are random per message and must be stored alongside the ciphertext, and extractable is false so the derived key can never be exported back out of the process.

Example · javascript
const { subtle, getRandomValues } = globalThis.crypto;

async function deriveKey(password, salt) {
  const baseKey = await subtle.importKey(
    'raw', new TextEncoder().encode(password), 'PBKDF2', false, ['deriveKey'],
  );
  return subtle.deriveKey(
    { name: 'PBKDF2', salt, iterations: 210000, hash: 'SHA-256' },
    baseKey,
    { name: 'AES-GCM', length: 256 },
    false,                      // never extractable
    ['encrypt', 'decrypt'],
  );
}

const salt = getRandomValues(new Uint8Array(16));
const iv = getRandomValues(new Uint8Array(12)); // fresh per message
const key = await deriveKey('correct horse battery staple', salt);

const ciphertext = await subtle.encrypt(
  { name: 'AES-GCM', iv }, key, new TextEncoder().encode('secret'),
);
console.log('ciphertext bytes:', new Uint8Array(ciphertext).length); // 22

const plain = await subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext);
console.log('decrypted:', new TextDecoder().decode(plain));

Verify an ES256 token with a public key imported from a JWK

importKey with format 'jwk' turns a JWKS entry straight into a CryptoKey, which is why Web Crypto is the natural fit for OIDC work. Web Crypto expects the raw 64-byte r||s signature that JOSE already uses, so no DER conversion is needed here — the opposite of crypto.createVerify(). Listing only 'verify' in keyUsages means an accidental sign() call rejects with InvalidAccessError.

Example · javascript
const { subtle } = globalThis.crypto;

// One entry as it would arrive from a /.well-known/jwks.json endpoint.
const jwk = {
  kty: 'EC', crv: 'P-256',
  x: 'FR7QG5rZ0k2CqCyH3sWv0h0oQ0i0YbC5rWJ0oT2f9nA',
  y: 'wq3n2sJb4Kf1t0oQ8dP7cE2hV9mB3xR6nL0aS4uY7iM',
  ext: true,
};

const key = await subtle.importKey(
  'jwk', jwk,
  { name: 'ECDSA', namedCurve: 'P-256' },
  false,
  ['verify'],           // verify only: sign() would reject
);

const [header, payload, sig] = token.split('.');
const signed = new TextEncoder().encode(`${header}.${payload}`);
const signature = Buffer.from(sig, 'base64url');

const ok = await subtle.verify(
  { name: 'ECDSA', hash: 'SHA-256' }, key, signature, signed,
);
console.log('token signature valid:', ok);

getRandomValues and randomUUID

getRandomValues mutates the array you hand it and returns the same reference, so assigning the result is optional. The final block shows the 65536-byte quota the Web Crypto spec imposes per call, which node:crypto's randomBytes() does not have — for a larger buffer, loop or use crypto.randomBytes() instead.

Example · javascript
const { webcrypto } = require('node:crypto');

// Filled in place; the same array is also returned.
const bytes = new Uint8Array(16);
const returned = webcrypto.getRandomValues(bytes);
console.log('filled in place:', returned === bytes);
console.log('length:', bytes.length);
console.log('all zero:', bytes.every((b) => b === 0)); // false

const id = webcrypto.randomUUID();
console.log('uuid length:', id.length);        // 36
console.log('version nibble:', id[14]);        // 4

// One call cannot exceed 65536 bytes.
try {
  webcrypto.getRandomValues(new Uint8Array(65537));
} catch (err) {
  console.log('too large:', err.name);          // QuotaExceededError
}

Discussion

  • Be the first to comment on this lesson.