Skip to content

@absoluteui/services/crypto

Web Crypto wrappers: digests, HMAC, AES-GCM, PBKDF2, and timing-safe comparison.

import { ... } from '@absoluteui/services/crypto';

Thin, typed wrappers over the browser’s Web Crypto API: digests, HMAC, AES-GCM encryption, password-based keys (PBKDF2), ECDSA signatures, and timing-safe comparison. Every function is async and accepts text (UTF-8) or bytes. Web Crypto needs a secure context (HTTPS or localhost); elsewhere every call rejects with SVC_CRYPTO_UNAVAILABLE, so check isWebCryptoAvailable() before offering features that depend on it.

sha256Hex and digestHex return lowercase hex, ready to compare with a checksum published by a server. For files, fileChecksum in @absoluteui/services/file streams large inputs.

import { digestHex, sha256Hex } from '@absoluteui/services/crypto';
await sha256Hex('payroll-2026-09.csv'); // e.g. a 64-character hex string
(await sha256Hex('abc')).length; // => 64
await digestHex('SHA-1', 'abc'); // => 'a9993e364706816aba3e25717850c26c9cd0d89d'

How do I sign or verify a webhook payload with HMAC?

Section titled “How do I sign or verify a webhook payload with HMAC?”

Sign with hmac or hmacHex and verify with hmacVerify, which compares in constant time. Import the secret once with importHmacKey when you check many messages.

import { hexDecode } from '@absoluteui/helpers/encoding';
import { hmacHex, hmacVerify, importHmacKey } from '@absoluteui/services/crypto';
const body = JSON.stringify({ event: 'employee.hired', id: 42 });
const key = await importHmacKey('webhook-secret', 'SHA-256');
const header = await hmacHex(key, body); // what the sender puts in its signature header
await hmacVerify(key, body, hexDecode(header)); // => true
await hmacVerify(key, body.replace('42', '43'), hexDecode(header)); // => false

Never compare signatures or tokens with ===; use hmacVerify or timingSafeEqual.

Derive an AES-GCM key from the password and a random salt with deriveKeyPbkdf2 (600,000 iterations by default), then encrypt to a compact token. Store the salt with the token; it is not secret.

import { aesGcmDecryptString, aesGcmEncryptString, deriveKeyPbkdf2, randomSalt } from '@absoluteui/services/crypto';
import { base64UrlDecode, base64UrlEncode } from '@absoluteui/helpers/encoding';
const salt = randomSalt();
const key = await deriveKeyPbkdf2('correct horse battery staple', salt);
const token = await aesGcmEncryptString(key, 'Salary review: 8% raise', { aad: 'employee:42' });
const saved = { salt: base64UrlEncode(salt), token };
const again = await deriveKeyPbkdf2('correct horse battery staple', base64UrlDecode(saved.salt));
await aesGcmDecryptString(again, saved.token, { aad: 'employee:42' }); // => 'Salary review: 8% raise'

aad binds the ciphertext to a context (here the employee), so a token copied to another record fails to decrypt. For encrypted local storage, use createEncryptedStore in @absoluteui/services/storage.

How do I sign a document so others can verify it?

Section titled “How do I sign a document so others can verify it?”

ECDSA keeps the private key in the browser (non-extractable by default) and hands out the public key.

import { ecdsaSign, ecdsaVerify, exportKey, generateEcdsaKeyPair } from '@absoluteui/services/crypto';
const { privateKey, publicKey } = await generateEcdsaKeyPair('P-256');
const signature = await ecdsaSign(privateKey, 'payslip-2026-09-42.pdf');
await ecdsaVerify(publicKey, signature, 'payslip-2026-09-42.pdf'); // => true
const jwk = await exportKey(publicKey, 'jwk'); // send this to the verifier
jwk.kty; // => 'EC'

jwtDecodeUnsafe decodes the header and payload without verifying anything. Use it only for display hints, such as warning before a session expires; the server must make every authorization decision.

import { jwtDecodeUnsafe } from '@absoluteui/services/crypto';
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsImV4cCI6MTc5MDAwMDAwMH0.sig';
jwtDecodeUnsafe(token)?.payload['exp']; // => 1790000000
jwtDecodeUnsafe('not-a-jwt'); // => undefined
  • A wrong key, wrong aad, or a tampered token rejects with SVC_CRYPTO_DECRYPT_FAILED; the error never says which, by design.
  • Keys are non-extractable unless you pass { extractable: true }; exportKey on such a key throws SVC_CRYPTO_NOT_EXTRACTABLE.
  • SHA-1 is available for interoperability with old systems only; do not use it for new signatures.
import { aesGcmDecryptString, aesGcmEncryptString, exportKey, generateAesKey } from '@absoluteui/services/crypto';
const key = await generateAesKey();
const token = await aesGcmEncryptString(key, 'Annual leave', { aad: 'employee:42' });
await aesGcmDecryptString(key, token, { aad: 'employee:43' }); // => throws SVC_CRYPTO_DECRYPT_FAILED
await exportKey(key); // => throws SVC_CRYPTO_NOT_EXTRACTABLE

aesGcmDecrypt

Decrypts an AES-GCM payload (or packed iv || ciphertext bytes). Any failure, including tampering or a wrong key or AAD, rejects with SVC_CRYPTO_DECRYPT_FAILED and no further detail.

function aesGcmDecrypt(key: CryptoKey, payload: AesGcmPayload | BufferSource, options?: AesGcmOptions): Promise<Uint8Array<ArrayBuffer>>;

Example:

const key = await generateAesKey();
const payload = await aesGcmEncrypt(key, 'secret', { aad: 'user:42' });
const plain = await aesGcmDecrypt(key, payload, { aad: 'user:42' });
new TextDecoder().decode(plain); // => 'secret'

aesGcmDecryptString

Decrypts a token from {@link aesGcmEncryptString} back to text.

function aesGcmDecryptString(key: CryptoKey, token: string, options?: AesGcmOptions): Promise<string>;

Example:

const key = await generateAesKey();
const token = await aesGcmEncryptString(key, 'Annual leave');
await aesGcmDecryptString(key, token); // => 'Annual leave'

aesGcmEncrypt

Encrypts with AES-GCM using a fresh random 96-bit IV per call (never reuse an IV with the same key).

function aesGcmEncrypt(key: CryptoKey, plaintext: CryptoInput, options?: AesGcmOptions): Promise<AesGcmPayload>;

Example:

const key = await generateAesKey();
const { iv, ciphertext } = await aesGcmEncrypt(key, 'secret', { aad: 'user:42' });
iv.length; // => 12

aesGcmEncryptString

Encrypts text to a compact base64url token (iv || ciphertext).

function aesGcmEncryptString(key: CryptoKey, plaintext: string, options?: AesGcmOptions): Promise<string>;

Example:

const key = await generateAesKey();
const token = await aesGcmEncryptString(key, JSON.stringify({ reason: 'Annual leave' }));
typeof token; // => 'string'

deriveBitsPbkdf2

Derives raw bits from a password (for example to compare against a stored hash).

function deriveBitsPbkdf2(password: CryptoInput, salt: CryptoInput, options?: Pbkdf2Options & {
readonly length?: number;
}): Promise<Uint8Array<ArrayBuffer>>;

Example:

import { hexEncode } from '@absoluteui/helpers/encoding';
const bits = await deriveBitsPbkdf2('password', 'salt', { iterations: 1, hash: 'SHA-1', length: 160 });
hexEncode(bits); // => '0c60c80f961f0e71f3a9b524af6012062fe037a6' // RFC 6070

deriveKeyPbkdf2

Derives a non-extractable AES-GCM key from a password.

function deriveKeyPbkdf2(password: CryptoInput, salt: CryptoInput, options?: Pbkdf2Options & KeyOptions & {
readonly length?: AesKeyLength;
}): Promise<CryptoKey>;

Example:

const salt = randomSalt(); // store it with the encrypted data
const key = await deriveKeyPbkdf2('correct horse battery staple', salt);
key.algorithm.name; // => 'AES-GCM'

digest

Hashes data with a SHA algorithm.

function digest(algorithm: DigestAlgorithm, data: CryptoInput): Promise<Uint8Array<ArrayBuffer>>;

Example:

const d = await digest('SHA-256', 'abc');
d.length; // => 32

digestHex

Hashes data and returns lowercase hex.

function digestHex(algorithm: DigestAlgorithm, data: CryptoInput): Promise<string>;

Example:

await digestHex('SHA-1', 'abc'); // => 'a9993e364706816aba3e25717850c26c9cd0d89d'

ecdsaSign

Signs with ECDSA (IEEE P1363 r || s signature format).

function ecdsaSign(privateKey: CryptoKey, data: CryptoInput, hash?: HashAlgorithm): Promise<Uint8Array<ArrayBuffer>>;

Example:

const { privateKey } = await generateEcdsaKeyPair();
const sig = await ecdsaSign(privateKey, 'payroll-2026-09.csv');
sig.length; // => 64 // P-256 r||s

ecdsaVerify

Verifies an ECDSA signature.

function ecdsaVerify(publicKey: CryptoKey, signature: BufferSource, data: CryptoInput, hash?: HashAlgorithm): Promise<boolean>;

Example:

const { privateKey, publicKey } = await generateEcdsaKeyPair();
const sig = await ecdsaSign(privateKey, 'payroll-2026-09.csv');
await ecdsaVerify(publicKey, sig, 'payroll-2026-09.csv'); // => true

exportKey

Exports an extractable key as raw bytes, or as a JWK with format: 'jwk'.

function exportKey(key: CryptoKey, format?: 'raw'): Promise<Uint8Array<ArrayBuffer>>;
function exportKey(key: CryptoKey, format: 'jwk'): Promise<JsonWebKey>;

Example:

const key = await generateAesKey({ extractable: true }); // exportKey needs an extractable key
const raw = await exportKey(key);
raw.length; // => 32 // 256-bit key

generateAesKey

Generates a random AES-GCM key (default 256-bit, non-extractable).

function generateAesKey(options?: KeyOptions & {
readonly length?: AesKeyLength;
}): Promise<CryptoKey>;

Example:

const key = await generateAesKey();
key.algorithm.name; // => 'AES-GCM'

generateEcdsaKeyPair

Generates an ECDSA key pair (private key non-extractable by default; the public key is always extractable).

function generateEcdsaKeyPair(curve?: EcdsaCurve, options?: KeyOptions): Promise<CryptoKeyPair>;

Example:

const { privateKey, publicKey } = await generateEcdsaKeyPair();
publicKey.type; // => 'public'

hmac

HMAC signature of data. key is a CryptoKey from {@link importHmacKey} or raw secret material.

function hmac(key: CryptoKey | CryptoInput, data: CryptoInput, hash?: HashAlgorithm): Promise<Uint8Array<ArrayBuffer>>;

Example:

const mac = await hmac('Jefe', 'what do ya want for nothing?', 'SHA-256');
mac.length; // => 32

hmacHex

HMAC as lowercase hex.

function hmacHex(key: CryptoKey | CryptoInput, data: CryptoInput, hash?: HashAlgorithm): Promise<string>;

Example:

await hmacHex('Jefe', 'what do ya want for nothing?'); // => '5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843' // RFC 4231

hmacVerify

Verifies an HMAC with crypto.subtle.verify (constant-time in the platform).

function hmacVerify(key: CryptoKey | CryptoInput, data: CryptoInput, signature: BufferSource, hash?: HashAlgorithm): Promise<boolean>;

Example:

const body = '{"event":"leave.approved"}';
const signature = await hmac('webhook-secret', body);
await hmacVerify('webhook-secret', body, signature); // => true
await hmacVerify('webhook-secret', body + ' ', signature); // => false

importAesKey

Imports raw AES key bytes (16, 24 or 32) as a non-extractable AES-GCM key.

function importAesKey(raw: BufferSource, options?: KeyOptions): Promise<CryptoKey>;

Example:

import { base64UrlDecode } from '@absoluteui/helpers/encoding';
declare const stored: string; // base64url key saved earlier
const key = await importAesKey(base64UrlDecode(stored));

importHmacKey

Imports a raw HMAC secret as a non-extractable CryptoKey (sign and verify).

function importHmacKey(secret: CryptoInput, hash?: HashAlgorithm, options?: KeyOptions): Promise<CryptoKey>;

Example:

const key = await importHmacKey('webhook-secret', 'SHA-256');
await hmacHex(key, 'payload') === await hmacHex('webhook-secret', 'payload'); // => true

isWebCryptoAvailable

True when crypto.subtle is usable (secure context in browsers).

function isWebCryptoAvailable(): boolean;

Example:

const canEncrypt = isWebCryptoAvailable(); // false on plain HTTP pages

jwtDecodeUnsafe

Decodes a JWT’s header and payload WITHOUT verifying the signature, expiry or issuer. Use only to read display hints (for example exp for a session-timeout warning) from a token the server already trusts; never make authorization decisions on the result. Returns undefined for malformed or oversized (> 16 KiB) tokens.

function jwtDecodeUnsafe(token: string): UnverifiedJwt | undefined;

Example:

const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsImV4cCI6MTc5MDAwMDAwMH0.sig';
jwtDecodeUnsafe(token)?.payload['exp']; // => 1790000000
jwtDecodeUnsafe('not-a-jwt'); // => undefined

packAesGcm

Packs a payload as iv || ciphertext.

function packAesGcm(payload: AesGcmPayload): Uint8Array<ArrayBuffer>;

Example:

const key = await generateAesKey();
const packed = packAesGcm(await aesGcmEncrypt(key, 'secret'));
packed.length; // => 34 // 12-byte IV + 6 bytes + 16-byte tag

randomSalt

Cryptographically random salt (default 16 bytes).

function randomSalt(length?: number): Uint8Array<ArrayBuffer>;

Example:

const salt = randomSalt();
salt.length; // => 16

sha256Hex

SHA-256 as lowercase hex.

function sha256Hex(data: CryptoInput): Promise<string>;

Example:

await sha256Hex('abc') // => 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'

timingSafeEqual

Compares two values in time that depends only on the longer length, not on where they differ. Strings are compared as UTF-8 bytes. Prefer {@link hmacVerify} for MACs.

function timingSafeEqual(a: CryptoInput, b: CryptoInput): boolean;

Example:

timingSafeEqual('csrf-token-1', 'csrf-token-1'); // => true
timingSafeEqual('csrf-token-1', 'csrf-token-2'); // => false

unpackAesGcm

Splits iv || ciphertext (throws SVC_CRYPTO_DECRYPT_FAILED when too short to hold an IV and tag).

function unpackAesGcm(packed: BufferSource): AesGcmPayload;

Example:

const key = await generateAesKey();
const packed = packAesGcm(await aesGcmEncrypt(key, 'secret'));
const { iv, ciphertext } = unpackAesGcm(packed);
ciphertext.byteLength; // => 22

AES_GCM_IV_BYTES

AES-GCM IV length in bytes (96 bits, the recommended size).

const AES_GCM_IV_BYTES = 12;

PBKDF2_DEFAULT_ITERATIONS

OWASP (2023) minimum PBKDF2 iterations for SHA-256.

const PBKDF2_DEFAULT_ITERATIONS = 600000;

AesGcmOptions

Options for AES-GCM operations.

interface AesGcmOptions {
readonly aad?: CryptoInput;
}

AesGcmPayload

AES-GCM output: the random IV and the ciphertext with its 128-bit tag appended.

interface AesGcmPayload {
readonly iv: Uint8Array<ArrayBuffer>;
readonly ciphertext: Uint8Array<ArrayBuffer>;
}

KeyOptions

Options for importing or generating keys.

interface KeyOptions {
readonly extractable?: boolean;
}

Pbkdf2Options

PBKDF2 options.

interface Pbkdf2Options {
readonly iterations?: number;
readonly hash?: HashAlgorithm;
}

UnverifiedJwt

Decoded, UNVERIFIED JWT parts.

interface UnverifiedJwt {
readonly header: Readonly<Record<string, unknown>>;
readonly payload: Readonly<Record<string, unknown>>;
}

AesKeyLength

AES key sizes in bits.

type AesKeyLength = 128 | 192 | 256;

CryptoInput

Text (encoded as UTF-8) or bytes.

type CryptoInput = string | BufferSource;

DigestAlgorithm

SHA family digests supported by Web Crypto.

type DigestAlgorithm = 'SHA-1' | 'SHA-256' | 'SHA-384' | 'SHA-512';

EcdsaCurve

NIST curves supported by Web Crypto ECDSA.

type EcdsaCurve = 'P-256' | 'P-384' | 'P-521';

HashAlgorithm

Hashes allowed for HMAC, PBKDF2 and ECDSA. SHA-1 is kept only for interoperability.

type HashAlgorithm = DigestAlgorithm;