Audited & minimal 0-dependency JS implementation of SHA, RIPEMD, BLAKE, HMAC, HKDF, PBKDF & Scrypt
A friendly wrapper over the native WebCrypto API (crypto.subtle) that provides a noble-hashes compatible interface. Uses browser/Node.js native implementations for better performance while maintaining API consistency with the pure JavaScript implementations.
import { sha256, sha384, sha512, hmac, hkdf, pbkdf2 } from '@noble/hashes/webcrypto.js';WebCrypto wrapper provides SHA-256, SHA-384, and SHA-512 with noble-hashes compatible interface.
/**
* WebCrypto SHA-256 hash function
* @param msg - Input data as Uint8Array
* @returns Promise resolving to 32-byte hash digest
*/
async function sha256(msg: Uint8Array): Promise<Uint8Array>;
// Properties
sha256.webCryptoName: 'SHA-256'; // Native algorithm name
sha256.outputLen: 32; // Output length in bytes
sha256.blockLen: 64; // Block length in bytes
/**
* WebCrypto SHA-384 hash function
* @param msg - Input data as Uint8Array
* @returns Promise resolving to 48-byte hash digest
*/
async function sha384(msg: Uint8Array): Promise<Uint8Array>;
// Properties
sha384.webCryptoName: 'SHA-384';
sha384.outputLen: 48;
sha384.blockLen: 128;
/**
* WebCrypto SHA-512 hash function
* @param msg - Input data as Uint8Array
* @returns Promise resolving to 64-byte hash digest
*/
async function sha512(msg: Uint8Array): Promise<Uint8Array>;
// Properties
sha512.webCryptoName: 'SHA-512';
sha512.outputLen: 64;
sha512.blockLen: 128;
/**
* WebHash type for hash functions
*/
interface WebHash {
(msg: Uint8Array): Promise<Uint8Array>;
webCryptoName: string;
outputLen: number;
blockLen: number;
}HMAC implementation using native WebCrypto for better performance.
/**
* HMAC using WebCrypto
* @param hash - WebCrypto hash function (sha256, sha384, or sha512)
* @param key - Secret key as Uint8Array
* @param message - Message to authenticate as Uint8Array
* @returns Promise resolving to HMAC tag
*/
async function hmac(
hash: WebHash,
key: Uint8Array,
message: Uint8Array
): Promise<Uint8Array>;HKDF key derivation using native WebCrypto.
/**
* HKDF key derivation using WebCrypto
* @param hash - WebCrypto hash function (sha256, sha384, or sha512)
* @param ikm - Input keying material as Uint8Array
* @param salt - Optional salt (defaults to hash.outputLen zeros)
* @param info - Optional context information
* @param length - Desired output length in bytes
* @returns Promise resolving to derived key material
*/
async function hkdf(
hash: WebHash,
ikm: Uint8Array,
salt: Uint8Array | undefined,
info: Uint8Array | undefined,
length: number
): Promise<Uint8Array>;PBKDF2 password-based key derivation using native WebCrypto.
/**
* PBKDF2 using WebCrypto
* @param hash - WebCrypto hash function (sha256, sha384, or sha512)
* @param password - Password as string or Uint8Array
* @param salt - Salt as string or Uint8Array
* @param opts - Options
* @param opts.c - Iteration count (>= 1)
* @param opts.dkLen - Desired key length in bytes (default: 32)
* @returns Promise resolving to derived key
*/
async function pbkdf2(
hash: WebHash,
password: string | Uint8Array,
salt: string | Uint8Array,
opts: { c: number; dkLen?: number }
): Promise<Uint8Array>;import { sha256, sha384, sha512 } from '@noble/hashes/webcrypto.js';
import { bytesToHex } from '@noble/hashes/utils.js';
// WebCrypto hashing (async)
const data = Uint8Array.from([0xca, 0xfe, 0x01, 0x23]);
const hash256 = await sha256(data);
console.log(bytesToHex(hash256)); // 32-byte hash
const hash384 = await sha384(data);
console.log(bytesToHex(hash384)); // 48-byte hash
const hash512 = await sha512(data);
console.log(bytesToHex(hash512)); // 64-byte hashimport { sha256, hmac } from '@noble/hashes/webcrypto.js';
import { randomBytes, bytesToHex } from '@noble/hashes/utils.js';
const key = randomBytes(32);
const message = new Uint8Array([1, 2, 3, 4, 5]);
// Compute HMAC-SHA256 using native crypto
const tag = await hmac(sha256, key, message);
console.log(bytesToHex(tag)); // 32-byte tag
// Verify HMAC
async function verifyHMAC(
key: Uint8Array,
message: Uint8Array,
expectedTag: Uint8Array
): Promise<boolean> {
const computedTag = await hmac(sha256, key, message);
if (computedTag.length !== expectedTag.length) return false;
let diff = 0;
for (let i = 0; i < computedTag.length; i++) {
diff |= computedTag[i] ^ expectedTag[i];
}
return diff === 0;
}import { sha256, hkdf } from '@noble/hashes/webcrypto.js';
import { randomBytes, utf8ToBytes } from '@noble/hashes/utils.js';
// Derive keys from shared secret
const sharedSecret = randomBytes(32);
const salt = randomBytes(32);
const info = utf8ToBytes('application-key-v1');
// Derive 32-byte key
const derivedKey = await hkdf(sha256, sharedSecret, salt, info, 32);
// Derive multiple keys with different contexts
async function deriveMultipleKeys(ikm: Uint8Array, salt: Uint8Array) {
const encKey = await hkdf(
sha256,
ikm,
salt,
utf8ToBytes('encryption'),
32
);
const authKey = await hkdf(
sha256,
ikm,
salt,
utf8ToBytes('authentication'),
32
);
return { encKey, authKey };
}import { sha256, pbkdf2 } from '@noble/hashes/webcrypto.js';
import { randomBytes } from '@noble/hashes/utils.js';
// Hash password with PBKDF2
const password = 'user-password';
const salt = randomBytes(16);
const hash = await pbkdf2(sha256, password, salt, {
c: 100000, // 100k iterations
dkLen: 32 // 32-byte output
});
// Verify password
async function verifyPassword(
password: string,
salt: Uint8Array,
expectedHash: Uint8Array
): Promise<boolean> {
const computedHash = await pbkdf2(sha256, password, salt, {
c: 100000,
dkLen: expectedHash.length
});
if (computedHash.length !== expectedHash.length) return false;
let diff = 0;
for (let i = 0; i < computedHash.length; i++) {
diff |= computedHash[i] ^ expectedHash[i];
}
return diff === 0;
}// Pure JS version
import { sha256 as sha256Pure } from '@noble/hashes/sha2.js';
// WebCrypto version
import { sha256 as sha256Web } from '@noble/hashes/webcrypto.js';
const data = new Uint8Array(1024 * 1024); // 1MB
// Pure JS (synchronous)
console.time('Pure JS');
const hashPure = sha256Pure(data);
console.timeEnd('Pure JS');
// Pure JS: ~3ms
// WebCrypto (asynchronous, but often faster)
console.time('WebCrypto');
const hashWeb = await sha256Web(data);
console.timeEnd('WebCrypto');
// WebCrypto: ~1ms (native implementation)import { sha256 as sha256Pure } from '@noble/hashes/sha2.js';
import { sha256 as sha256Web } from '@noble/hashes/webcrypto.js';
// Use WebCrypto if available, fallback to pure JS
async function hashWithFallback(data: Uint8Array): Promise<Uint8Array> {
try {
// Try WebCrypto first (faster)
return await sha256Web(data);
} catch (error) {
// Fallback to pure JS if WebCrypto unavailable
return sha256Pure(data);
}
}import { sha256 } from '@noble/hashes/webcrypto.js';
// Hash multiple items in parallel
async function hashBatch(items: Uint8Array[]): Promise<Uint8Array[]> {
return Promise.all(items.map(item => sha256(item)));
}
// Usage
const items = [
new Uint8Array([1, 2, 3]),
new Uint8Array([4, 5, 6]),
new Uint8Array([7, 8, 9])
];
const hashes = await hashBatch(items);
console.log('Hashed', hashes.length, 'items');WebCrypto is the browser standard for cryptographic operations:
crypto.subtleglobalThis.cryptoWebCrypto is typically 1.5-5x faster than pure JavaScript:
Approximate speedups (browser dependent):
Hash Functions:
.create() method)No Scrypt or Argon2:
Always Async:
No Streaming:
.create().update() for streamingBrowser Compatibility:
Performance: Native implementation is faster for large inputs
Security: Browser-maintained code, less attack surface
Standards Compliance: Official W3C standard
Resource Efficient: Offloads work to native code
Async Only: Requires async/await or Promise handling
Limited Algorithms: Only SHA-2 family available
No Incremental Hashing: Cannot stream data
Browser Dependent: Performance and availability vary
Less Control: Cannot inspect internal state
Use WebCrypto when:
Use Pure JS when:
// Check if WebCrypto is available
function isWebCryptoAvailable(): boolean {
return typeof globalThis.crypto !== 'undefined' &&
typeof globalThis.crypto.subtle !== 'undefined';
}
if (isWebCryptoAvailable()) {
console.log('WebCrypto available');
// Use webcrypto.js
} else {
console.log('WebCrypto not available');
// Use pure JS implementations
}WebCrypto is available in:
Not available in:
Timing Attacks: Native implementations may have better constant-time guarantees than JavaScript, but this is implementation-dependent.
Side Channels: Native code may be more vulnerable to some side-channel attacks (Spectre, Meltdown) but less vulnerable to timing attacks.
Auditing: Pure JS code is auditable; native code is maintained by browser/runtime vendors.
Supply Chain: Relying on browser crypto reduces your attack surface but trusts browser vendors.
| Feature | WebCrypto | Pure JS |
|---|---|---|
| Performance | Faster (1.5-5x) | Slower |
| Algorithms | SHA-2 only | All algorithms |
| Incremental | No | Yes |
| Async/Sync | Async only | Both |
| Availability | Modern only | Universal |
| Auditability | Browser code | Audited source |
| Control | Limited | Full |
// Before (Pure JS)
import { sha256 } from '@noble/hashes/sha2.js';
import { hmac } from '@noble/hashes/hmac.js';
const hash = sha256(data);
const tag = hmac(sha256, key, message);
// After (WebCrypto, async)
import { sha256, hmac } from '@noble/hashes/webcrypto.js';
const hash = await sha256(data);
const tag = await hmac(sha256, key, message);