CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-noble--hashes

Audited & minimal 0-dependency JS implementation of SHA, RIPEMD, BLAKE, HMAC, HKDF, PBKDF & Scrypt

Overview
Eval results
Files

hmac.mddocs/

HMAC: Hash-based Message Authentication Code

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.

Imports

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';

Capabilities

HMAC Function

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>;

HMAC Class Interface

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>;
}

Usage Examples

Basic HMAC

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-256

Incremental HMAC

import { 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();

Message Authentication

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); // false

HMAC with Different Hash Functions

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';

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 bytes

API Authentication

import { 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"}');

JWT-like Token Signing

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);
}

HMAC-based Key Derivation

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);

Technical Details

HMAC Construction

HMAC uses the following construction (RFC 2104):

HMAC(K, m) = H((K' ⊕ opad) || H((K' ⊕ ipad) || m))

Where:

  • H is the hash function
  • K' is the key padded to block size
  • opad is the outer padding (0x5c repeated)
  • ipad is the inner padding (0x36 repeated)
  • || denotes concatenation
  • denotes XOR

This construction:

  1. Prevents length extension attacks
  2. Provides security even with weak hash functions (e.g., HMAC-SHA1 is still secure)
  3. Ensures proper key mixing

Key Length Recommendations

  • Minimum: Hash output length (e.g., 32 bytes for SHA-256)
  • Recommended: 32-64 bytes for most applications
  • Maximum: Hash block length (longer keys are hashed first)
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 internally

Output Length

HMAC output length always matches the hash function output:

  • hmac(sha256, ...) → 32 bytes
  • hmac(sha512, ...) → 64 bytes
  • hmac(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 bytes

RFC 2104 allows truncation but recommends at least 16 bytes (128 bits) for security.

Security Properties

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;
}

Performance

HMAC overhead is approximately 2x the hash function performance (two hash operations). Benchmarks on Apple M4:

  • hmac(sha256): ~1μs per 32-byte input
  • hmac(sha512): ~5μs per 32-byte input
  • hmac(blake2b): ~2μs per 32-byte input

For incremental HMAC, the overhead is minimal compared to the hash operations.

HMAC vs Other MACs

HMAC advantages:

  • Works with any hash function
  • Simple construction
  • Well-studied security
  • No patent issues

Alternatives:

  • KMAC: Keccak-based MAC, native to SHA-3 family
  • BLAKE2/BLAKE3 keyed mode: Direct keyed hashing, simpler and faster
  • Poly1305: Faster for stream ciphers but requires careful key management
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 });

When to Use HMAC

Use HMAC for:

  • API request signing
  • JWT token signing
  • Cookie signing
  • Message authentication in protocols
  • Simple key derivation
  • Integrity protection with authentication

Consider alternatives for:

  • High-performance scenarios (use BLAKE2/BLAKE3 keyed mode)
  • SHA-3 based systems (use KMAC)
  • Encrypt-then-MAC (use dedicated AEAD)

References

  • RFC 2104: HMAC: Keyed-Hashing for Message Authentication
  • RFC 6151: Updated Security Considerations for HMAC
  • FIPS 198-1: The Keyed-Hash Message Authentication Code

Install with Tessl CLI

npx tessl i tessl/npm-noble--hashes

docs

argon2.md

blake.md

eskdf.md

hkdf.md

hmac.md

index.md

legacy.md

pbkdf2.md

scrypt.md

sha2.md

sha3-addons.md

sha3.md

utils.md

webcrypto.md

tile.json