Audited & minimal 0-dependency JS implementation of SHA, RIPEMD, BLAKE, HMAC, HKDF, PBKDF & Scrypt
HMAC provides message authentication using cryptographic hash functions. It ensures both data integrity and authenticity by combining a secret key with the message through a specific construction. HMAC is a standard building block for secure protocols.
import { hmac } from '@noble/hashes/hmac.js';
import { sha256, sha512 } from '@noble/hashes/sha2.js';
import { sha3_256 } from '@noble/hashes/sha3.js';
import { blake2b } from '@noble/hashes/blake2.js';HMAC can be used with any hash function from the library. The security of HMAC depends on the underlying hash function.
/**
* Computes HMAC of a message with a key using specified hash function
* @param hash - Hash function to use (e.g., sha256, sha512, blake2b)
* @param key - Secret key as Uint8Array
* @param message - Message to authenticate as Uint8Array
* @returns HMAC tag (length matches hash function output)
*/
function hmac(
hash: CHash,
key: Uint8Array,
message: Uint8Array
): Uint8Array;
/**
* Creates an incremental HMAC instance
* @param hash - Hash function to use
* @param key - Secret key as Uint8Array
* @returns HMAC instance for incremental updates
*/
function create(hash: CHash, key: Uint8Array): _HMAC<any>;The HMAC instance returned by hmac.create() implements the following interface:
class _HMAC<T> {
blockLen: number;
outputLen: number;
oHash: T; // Outer hash instance
iHash: T; // Inner hash instance
/**
* Updates HMAC with new data
* @param buf - Data to include in MAC
* @returns this for chaining
*/
update(buf: Uint8Array): this;
/**
* Finalizes HMAC and returns tag
* @returns HMAC tag as Uint8Array
*/
digest(): Uint8Array;
/**
* Finalizes HMAC and writes tag to provided buffer
* @param out - Output buffer (must be >= outputLen)
*/
digestInto(out: Uint8Array): void;
/**
* Destroys internal state (zeros memory)
*/
destroy(): void;
/**
* Creates independent copy of current HMAC state
* @returns Cloned HMAC instance
*/
clone(): _HMAC<T>;
}import { hmac } from '@noble/hashes/hmac.js';
import { sha256 } from '@noble/hashes/sha2.js';
import { bytesToHex } from '@noble/hashes/utils.js';
const key = new Uint8Array(32).fill(0x42);
const message = new Uint8Array(32).fill(0xff);
// Compute HMAC-SHA256
const tag = hmac(sha256, key, message);
console.log(bytesToHex(tag)); // 32-byte tag
// Output length matches the hash function
console.log(tag.length); // 32 for SHA-256import { hmac } from '@noble/hashes/hmac.js';
import { sha256 } from '@noble/hashes/sha2.js';
const key = new Uint8Array(32).fill(0x42);
// Create HMAC instance
const mac = hmac.create(sha256, key);
// Update with multiple chunks
mac.update(chunk1);
mac.update(chunk2);
mac.update(chunk3);
// Finalize and get tag
const tag = mac.digest();import { hmac } from '@noble/hashes/hmac.js';
import { sha256 } from '@noble/hashes/sha2.js';
function authenticate(
key: Uint8Array,
message: Uint8Array
): { message: Uint8Array; tag: Uint8Array } {
const tag = hmac(sha256, key, message);
return { message, tag };
}
function verify(
key: Uint8Array,
message: Uint8Array,
tag: Uint8Array
): boolean {
const computed = hmac(sha256, key, message);
// Constant-time comparison (algorithmic, not guaranteed in JS)
if (computed.length !== tag.length) return false;
let diff = 0;
for (let i = 0; i < computed.length; i++) {
diff |= computed[i] ^ tag[i];
}
return diff === 0;
}
// Usage
const key = new Uint8Array(32).fill(0x42);
const message = new Uint8Array([1, 2, 3, 4, 5]);
const { message: msg, tag } = authenticate(key, message);
const isValid = verify(key, msg, tag);
console.log('Valid:', isValid); // true
// Tampering detection
const tamperedMsg = new Uint8Array([1, 2, 3, 4, 6]);
const isInvalid = verify(key, tamperedMsg, tag);
console.log('Valid:', isInvalid); // falseimport { hmac } from '@noble/hashes/hmac.js';
import { sha256, sha512 } from '@noble/hashes/sha2.js';
import { sha3_256 } from '@noble/hashes/sha3.js';
import { blake2b } from '@noble/hashes/blake2.js';
const key = new Uint8Array(32).fill(0x42);
const message = new Uint8Array([1, 2, 3]);
// HMAC-SHA256 (most common)
const hmacSha256 = hmac(sha256, key, message); // 32 bytes
// HMAC-SHA512 (stronger security)
const hmacSha512 = hmac(sha512, key, message); // 64 bytes
// HMAC-SHA3-256 (alternative design)
const hmacSha3 = hmac(sha3_256, key, message); // 32 bytes
// HMAC-BLAKE2b (fastest)
const hmacBlake2 = hmac(blake2b, key, message); // 64 bytesimport { hmac } from '@noble/hashes/hmac.js';
import { sha256 } from '@noble/hashes/sha2.js';
import { utf8ToBytes, bytesToHex } from '@noble/hashes/utils.js';
function signRequest(
apiKey: Uint8Array,
method: string,
path: string,
timestamp: number,
body: string
): string {
// Concatenate request components
const message = utf8ToBytes(
`${method}\n${path}\n${timestamp}\n${body}`
);
// Generate signature
const signature = hmac(sha256, apiKey, message);
return bytesToHex(signature);
}
function verifyRequest(
apiKey: Uint8Array,
method: string,
path: string,
timestamp: number,
body: string,
providedSignature: string
): boolean {
const expectedSignature = signRequest(apiKey, method, path, timestamp, body);
// Constant-time comparison
if (expectedSignature.length !== providedSignature.length) {
return false;
}
let diff = 0;
for (let i = 0; i < expectedSignature.length; i++) {
diff |= expectedSignature.charCodeAt(i) ^ providedSignature.charCodeAt(i);
}
return diff === 0;
}
// Usage
const apiKey = new Uint8Array(32).fill(0x42);
const signature = signRequest(apiKey, 'POST', '/api/data', Date.now(), '{"key":"value"}');import { hmac } from '@noble/hashes/hmac.js';
import { sha256 } from '@noble/hashes/sha2.js';
import { utf8ToBytes, bytesToHex, hexToBytes } from '@noble/hashes/utils.js';
interface TokenPayload {
userId: string;
expires: number;
}
function createToken(secret: Uint8Array, payload: TokenPayload): string {
const payloadJson = JSON.stringify(payload);
const payloadBase64 = btoa(payloadJson);
// Sign payload
const signature = hmac(sha256, secret, utf8ToBytes(payloadBase64));
const signatureHex = bytesToHex(signature);
return `${payloadBase64}.${signatureHex}`;
}
function verifyToken(secret: Uint8Array, token: string): TokenPayload | null {
const parts = token.split('.');
if (parts.length !== 2) return null;
const [payloadBase64, signatureHex] = parts;
// Verify signature
const expectedSig = hmac(sha256, secret, utf8ToBytes(payloadBase64));
const expectedHex = bytesToHex(expectedSig);
if (expectedHex !== signatureHex) return null;
// Parse payload
try {
const payloadJson = atob(payloadBase64);
const payload = JSON.parse(payloadJson) as TokenPayload;
// Check expiration
if (payload.expires < Date.now()) return null;
return payload;
} catch {
return null;
}
}
// Usage
const secret = new Uint8Array(32).fill(0x42);
const token = createToken(secret, {
userId: 'user123',
expires: Date.now() + 3600000 // 1 hour
});
const payload = verifyToken(secret, token);
if (payload) {
console.log('Valid token for user:', payload.userId);
}import { hmac } from '@noble/hashes/hmac.js';
import { sha256 } from '@noble/hashes/sha2.js';
import { utf8ToBytes } from '@noble/hashes/utils.js';
function deriveKey(
masterKey: Uint8Array,
context: string,
keyId: number
): Uint8Array {
const message = new Uint8Array(context.length + 4);
message.set(utf8ToBytes(context));
// Append keyId as 4-byte integer
const view = new DataView(message.buffer, context.length);
view.setUint32(0, keyId, false);
return hmac(sha256, masterKey, message);
}
// Derive multiple keys from a master key
const masterKey = new Uint8Array(32).fill(0x42);
const encKey = deriveKey(masterKey, 'encryption', 1);
const authKey = deriveKey(masterKey, 'authentication', 2);
const sigKey = deriveKey(masterKey, 'signing', 3);HMAC uses the following construction (RFC 2104):
HMAC(K, m) = H((K' ⊕ opad) || H((K' ⊕ ipad) || m))Where:
H is the hash functionK' is the key padded to block sizeopad is the outer padding (0x5c repeated)ipad is the inner padding (0x36 repeated)|| denotes concatenation⊕ denotes XORThis construction:
import { hmac } from '@noble/hashes/hmac.js';
import { sha256 } from '@noble/hashes/sha2.js';
import { randomBytes } from '@noble/hashes/utils.js';
// Good key length
const goodKey = randomBytes(32); // 32 bytes = 256 bits
// Too short (but still works)
const shortKey = randomBytes(16); // Less secure
// Very long (automatically hashed to 32 bytes)
const longKey = randomBytes(1024); // Hashed to 32 bytes internallyHMAC output length always matches the hash function output:
hmac(sha256, ...) → 32 byteshmac(sha512, ...) → 64 byteshmac(blake2b, ...) → 64 bytes (default)To truncate HMAC output:
import { hmac } from '@noble/hashes/hmac.js';
import { sha256 } from '@noble/hashes/sha2.js';
const key = new Uint8Array(32);
const message = new Uint8Array([1, 2, 3]);
// Full HMAC
const fullTag = hmac(sha256, key, message); // 32 bytes
// Truncated HMAC (still secure if >= 16 bytes)
const truncatedTag = fullTag.slice(0, 16); // 16 bytesRFC 2104 allows truncation but recommends at least 16 bytes (128 bits) for security.
Unforgeability: Without the key, an attacker cannot create a valid HMAC tag for any message, even with access to tags for other messages.
Collision Resistance: Even if the underlying hash function has collision weaknesses, HMAC remains secure for authentication.
Key Recovery: Recovering the key from HMAC tags requires brute force.
Timing Resistance: Always use constant-time comparison for tag verification:
function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {
if (a.length !== b.length) return false;
let diff = 0;
for (let i = 0; i < a.length; i++) {
diff |= a[i] ^ b[i];
}
return diff === 0;
}HMAC overhead is approximately 2x the hash function performance (two hash operations). Benchmarks on Apple M4:
hmac(sha256): ~1μs per 32-byte inputhmac(sha512): ~5μs per 32-byte inputhmac(blake2b): ~2μs per 32-byte inputFor incremental HMAC, the overhead is minimal compared to the hash operations.
HMAC advantages:
Alternatives:
import { hmac } from '@noble/hashes/hmac.js';
import { sha256 } from '@noble/hashes/sha2.js';
import { kmac256 } from '@noble/hashes/sha3-addons.js';
import { blake2b } from '@noble/hashes/blake2.js';
const key = new Uint8Array(32);
const message = new Uint8Array(100);
// HMAC-SHA256 (standard, well-supported)
const hmacTag = hmac(sha256, key, message);
// KMAC256 (native Keccak MAC)
const kmacTag = kmac256(key, message);
// BLAKE2b keyed mode (fastest)
const blake2Tag = blake2b(message, { key });Use HMAC for:
Consider alternatives for: