Web Crypto API en JavaScript: hash, cifrado, firmas digitales y claves en el navegador

La Web Crypto API proporciona operaciones criptográficas primitivas directamente en el navegador y en Node.js (vía el objeto crypto global desde la versión 19, o require('crypto').webcrypto antes). No requiere librerías de terceros, usa aceleración hardware cuando está disponible y es la base segura para implementar autenticación, cifrado de datos del cliente y firmas digitales en aplicaciones web.

Hashes SHA-256 con crypto.subtle.digest

crypto.subtle.digest calcula el hash de un mensaje. El resultado es un ArrayBuffer que normalmente se convierte a hexadecimal para mostrarlo o compararlo:

async function sha256(mensaje) {
  const encoder = new TextEncoder();
  const datos = encoder.encode(mensaje);
  const hashBuffer = await crypto.subtle.digest('SHA-256', datos);

  // Convertir ArrayBuffer a hex string
  return Array.from(new Uint8Array(hashBuffer))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
}

const hash = await sha256('Hola, mundo');
console.log(hash);
// "872e4e50ce9990d8b041330c47c9ddd11bec6b503ae9386a99da8584e9bb12c4"

// Para checksums de ficheros
async function hashFichero(file) {
  const buffer = await file.arrayBuffer();
  const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
  return Array.from(new Uint8Array(hashBuffer))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
}

Cifrado simétrico con AES-GCM

AES-GCM (Galois/Counter Mode) es el algoritmo de cifrado simétrico recomendado: autentica e integridad del mensaje además de cifrarlo, detectando cualquier modificación del texto cifrado. Cada cifrado necesita un vector de inicialización (IV) único y aleatorio:

async function generarClave() {
  return crypto.subtle.generateKey(
    { name: 'AES-GCM', length: 256 },
    true,        // extractable: se puede exportar
    ['encrypt', 'decrypt']
  );
}

async function cifrar(clave, texto) {
  const iv = crypto.getRandomValues(new Uint8Array(12)); // IV de 12 bytes para GCM
  const datos = new TextEncoder().encode(texto);

  const cifrado = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv },
    clave,
    datos
  );

  // Guardar IV junto al texto cifrado (el IV no es secreto)
  return { iv, cifrado };
}

async function descifrar(clave, iv, cifrado) {
  const datos = await crypto.subtle.decrypt(
    { name: 'AES-GCM', iv },
    clave,
    cifrado
  );
  return new TextDecoder().decode(datos);
}

// Uso completo
const clave = await generarClave();
const { iv, cifrado } = await cifrar(clave, 'Mensaje secreto');
const texto = await descifrar(clave, iv, cifrado);
console.log(texto); // "Mensaje secreto"

Firma digital con ECDSA

ECDSA (Elliptic Curve Digital Signature Algorithm) con la curva P-256 permite firmar datos y verificar la firma, sin revelar la clave privada. Útil para tokens de autenticación del lado cliente:

async function generarParClaves() {
  return crypto.subtle.generateKeyPair(
    { name: 'ECDSA', namedCurve: 'P-256' },
    true,
    ['sign', 'verify']
  );
}

async function firmar(clavePrivada, mensaje) {
  const datos = new TextEncoder().encode(mensaje);
  const firma = await crypto.subtle.sign(
    { name: 'ECDSA', hash: 'SHA-256' },
    clavePrivada,
    datos
  );
  return firma;
}

async function verificar(clavePublica, firma, mensaje) {
  const datos = new TextEncoder().encode(mensaje);
  return crypto.subtle.verify(
    { name: 'ECDSA', hash: 'SHA-256' },
    clavePublica,
    firma,
    datos
  );
}

const { privateKey, publicKey } = await generarParClaves();
const firma = await firmar(privateKey, 'Documento importante');
const valida = await verificar(publicKey, firma, 'Documento importante');
console.log('Firma válida:', valida); // true

Importar y exportar claves

exportKey e importKey permiten serializar claves para guardarlas en localStorage o IndexedDB, o transmitirlas al servidor:

// Exportar clave pública en formato SPKI (para compartir)
const clavePublicaExportada = await crypto.subtle.exportKey('spki', publicKey);
const base64 = btoa(String.fromCharCode(...new Uint8Array(clavePublicaExportada)));

// Importar clave pública recibida del servidor
async function importarClavePublica(base64) {
  const binario = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
  return crypto.subtle.importKey(
    'spki',
    binario,
    { name: 'ECDSA', namedCurve: 'P-256' },
    true,
    ['verify']
  );
}

// Exportar clave simétrica como JSON Web Key (JWK)
const claveJwk = await crypto.subtle.exportKey('jwk', clave);
// { kty: 'oct', k: '...', alg: 'A256GCM', ext: true, key_ops: ['encrypt', 'decrypt'] }

// Importar de nuevo desde JWK
const claveImportada = await crypto.subtle.importKey(
  'jwk',
  claveJwk,
  { name: 'AES-GCM' },
  true,
  ['encrypt', 'decrypt']
);

Para derivar una clave de cifrado a partir de una contraseña del usuario, usa PBKDF2 o scrypt (también disponibles en crypto.subtle) en lugar de usar la contraseña directamente como clave. Nunca almacenes la clave privada en localStorage: usa IndexedDB con el flag extractable: false para que la clave solo sea usable desde el navegador donde se generó.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP