← blog
JavaScript Web Crypto API Zero dipendenze Sicurezza

HMAC-SHA256 con Web Crypto API in JavaScript

Firma e verifica un payload con HMAC-SHA256 usando solo l'API crittografica nativa del browser β€” crypto.subtle. Nessuna libreria esterna, nessuna dipendenza. Funziona su tutti i browser moderni e in Node.js 18+.

Il problema

Hai bisogno di autenticare un token o verificare che un dato non sia stato manomesso, ma non vuoi aggiungere dipendenze come crypto-js o jsonwebtoken. La Web Crypto API Γ¨ giΓ  nel browser β€” basta usarla.

Firma un payload (sign)

hmac.js β€” firma
/**
 * Firma un payload con HMAC-SHA256.
 * @param {string} secret  – chiave segreta condivisa
 * @param {string} payload – stringa da firmare (es. JSON.stringify di un oggetto)
 * @returns {Promise<string>} firma in hex
 */
async function hmacSign(secret, payload) {
  const enc = new TextEncoder();

  const key = await crypto.subtle.importKey(
    'raw',
    enc.encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,          // non estraibile
    ['sign']
  );

  const sig = await crypto.subtle.sign('HMAC', key, enc.encode(payload));

  // Converte ArrayBuffer β†’ stringa hex
  return [...new Uint8Array(sig)]
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
}

Verifica la firma (verify)

hmac.js β€” verifica
/**
 * Verifica che tokenHex corrisponda a HMAC-SHA256(secret, payload).
 * Usa una comparazione a tempo costante per prevenire timing attacks.
 * @returns {Promise<boolean>}
 */
async function hmacVerify(secret, payload, tokenHex) {
  const expected = await hmacSign(secret, payload);

  // Confronto a lunghezza fissa β€” evita timing attacks
  if (expected.length !== tokenHex.length) return false;
  let diff = 0;
  for (let i = 0; i < expected.length; i++) {
    diff |= expected.charCodeAt(i) ^ tokenHex.charCodeAt(i);
  }
  return diff === 0;
}

Esempio completo: URL firmato con scadenza

example.js β€” usage
const SECRET = 'chiave-segreta-condivisa';

// ── MITTENTE: crea token ──────────────────────────────
async function buildAuthUrl(userId, targetUrl) {
  const payload = JSON.stringify({
    uid: userId,
    exp: Date.now() + 5 * 60 * 1000 // scade in 5 minuti
  });

  const b64    = btoa(payload);
  const sig    = await hmacSign(SECRET, b64);
  const token  = `${b64}.${sig}`;

  return `${targetUrl}?auth=${encodeURIComponent(token)}`;
}

// ── DESTINATARIO: legge e verifica token ──────────────
async function parseAuthToken(token) {
  const [b64, sig] = token.split('.');
  if (!b64 || !sig) throw new Error('Token malformato');

  const valid = await hmacVerify(SECRET, b64, sig);
  if (!valid) throw new Error('Firma non valida');

  const data = JSON.parse(atob(b64));
  if (Date.now() > data.exp) throw new Error('Token scaduto');

  return data; // { uid: '...', exp: ... }
}

Quando usarlo: autenticazione cross-app, validazione webhook, URL firmati a scadenza, verifica integritΓ  dati tra client e server.

Attenzione: il segreto nel codice client Γ¨ visibile a chi ispeziona il bundle. Per applicazioni pubbliche usa un backend che firma server-side (es. Firebase Cloud Functions o un endpoint Node.js).

PerchΓ© usare crypto.subtle e non btoa()?

btoa() Γ¨ solo una codifica Base64 β€” non firma nulla, non garantisce integritΓ . crypto.subtle.sign() usa primitivi crittografici reali: la chiave viene importata in forma non estraibile, le operazioni avvengono in un contesto protetto del motore JS.

  • HMAC = Hash-based Message Authentication Code
  • SHA-256 = funzione di hash a 256 bit
  • Il risultato Γ¨ deterministico: stesso secret + stesso payload = stessa firma
  • Senza il secret Γ¨ computazionalmente impossibile forgiare una firma valida
Articolo correlato
Token HMAC tra due PWA su Firebase diversi β€” caso reale completo
β†’