En este post de programación con JavaScript en el navegador veremos cómo encriptar y desencriptar datos usando una API nativa, segura y confiable.
Vamos a usar la interfaz Crypto a través de window.crypto
. Al final podremos encriptar y desencriptar archivos usando una contraseña, derivando una clave de la misma y usando AES para el cifrado de datos.
Nota: voy a usar cifrado y encriptado como sinónimos para referirme a la encriptación de información.
Por cierto, usaremos la encriptación simétrica aunque también es posible usar la asimétrica con claves públicas y privadas.
Demostración
Aquí te dejo una demostración que puedes usar para cifrar y descifrar información ya lista para producción: https://parzibyte.github.io/ejemplos-javascript/encriptacion-js/
Siempre puedes ver el código fuente para que veas que es totalmente del lado del cliente y seguro.
Una vez que hayas cifrado la información ya puedes guardarla en cualquier lugar, y podrás confiar en que nadie la podrá desencriptar aunque la tengan.
Explicación
Esto no es un post donde te explico los métodos de encriptación, lo que es un vector de inicialización, sal o una clave derivada de una contraseña. Si quieres aprender más de eso puedes ir a investigarlo y después volver al post.
Lo que vamos a hacer es generar una clave a partir de una contraseña del usuario (derivación de clave basada en contraseña o PBKDF2), y cada vez que encriptemos la información vamos a usar esa clave y una sal aleatoria.
Al momento de generar la información encriptada vamos a generar un vector de inicialización aleatorio (iv) y más tarde vamos a almacenar el vector y la sal junto con la información cifrada para que se puedan recuperar al momento de desencriptar.
Generando clave criptográfica a partir de contraseña
Al momento de encriptar y desencriptar con JavaScript vamos a necesitar la clave, y esa clave se generará desde la contraseña.
const derivacionDeClaveBasadaEnContraseña = async (contraseña, sal, iteraciones, longitud, hash, algoritmo = 'AES-CBC') => {
const encoder = new TextEncoder();
let keyMaterial = await window.crypto.subtle.importKey(
'raw',
encoder.encode(contraseña),
{ name: 'PBKDF2' },
false,
['deriveKey']
);
return await window.crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: encoder.encode(sal),
iterations: iteraciones,
hash
},
keyMaterial,
{ name: algoritmo, length: longitud },
false,
['encrypt', 'decrypt']
);
}
Con esta función obtenemos la key para cifrar y descifrar la información. En este caso el algoritmo usado por defecto es AES-CBC.
Encriptando información
Ahora veamos como cifrar con AES usando JavaScript. Hasta este punto ya tendremos la clave, entonces creamos una función que recibe la contraseña y el texto plano a cifrar.
const encriptar = async (contraseña, textoPlano) => {
const encoder = new TextEncoder();
const sal = window.crypto.getRandomValues(new Uint8Array(16));
const vectorInicializacion = window.crypto.getRandomValues(new Uint8Array(16));
const bufferTextoPlano = encoder.encode(textoPlano);
const clave = await derivacionDeClaveBasadaEnContraseña(contraseña, sal, 100000, 256, 'SHA-256');
const encrypted = await window.crypto.subtle.encrypt(
{ name: "AES-CBC", iv: vectorInicializacion },
clave,
bufferTextoPlano
);
return bufferABase64([
...sal,
...vectorInicializacion,
...new Uint8Array(encrypted)
]);
};
Primero creamos la clave en la línea 6, con una sal aleatoria obtenida con getRandomValues
que es una función criptográficamente segura; hacemos lo mismo con el vector de inicialización.
Después invocamos a crypto.subtle.encrypt
pasándole el algoritmo, la clave derivada de la contraseña y un búfer a partir del texto plano.
Finalmente regresamos ese búfer convertido a base64, ya que recuerda que al final la encriptación no distingue texto o archivos, simplemente ve una colección de bits.
Convertimos a base64 porque es una forma sencilla de codificar la información de un búfer; y ya esta cadena podemos almacenarla en cualquier lugar o transportarla.
Codificando y decodificando en Base64
Estas funciones son opcionales. Tú podrías almacenar el búfer de la manera que prefieras (como un blob, tomando los enteros y separándolos por coma, etcétera).
Solo recuerda que al momento de desencriptar debes recuperar el búfer original.
Por ello es que existen estas funciones que permiten codificar y decodificar el búfer. Nota que aquí no estamos haciendo nada de encriptación o cosas de seguridad, simplemente convertimos el búfer a una cadena amigable.
const bufferABase64 = buffer => btoa(String.fromCharCode(...new Uint8Array(buffer)));
const base64ABuffer = buffer => Uint8Array.from(atob(buffer), c => c.charCodeAt(0));
Desencriptar con JavaScript
Llegados a este punto veremos la función para desencriptar, que igualmente necesita la contraseña (luego vamos a sacar la clave derivada) y la cadena en base64.
Te repito que no es necesario el paso de la base64 pero es una manera fácil de guardar y mostrar cadenas.
const desencriptar = async (contraseña, encriptadoEnBase64) => {
const decoder = new TextDecoder();
const datosEncriptados = base64ABuffer(encriptadoEnBase64);
const sal = datosEncriptados.slice(0, LONGITUD_SAL);
const vectorInicializacion = datosEncriptados.slice(0 + LONGITUD_SAL, LONGITUD_SAL + LONGITUD_VECTOR_INICIALIZACION);
const clave = await derivacionDeClaveBasadaEnContraseña(contraseña, sal, 100000, 256, 'SHA-256');
const datosDesencriptadosComoBuffer = await window.crypto.subtle.decrypt(
{ name: "AES-CBC", iv: vectorInicializacion },
clave,
datosEncriptados.slice(LONGITUD_SAL + LONGITUD_VECTOR_INICIALIZACION)
);
return decoder.decode(datosDesencriptadosComoBuffer);
}
Nota: recuerda que la sal y el iv están en la información, por ello es que los recuperamos en la línea 4 y 5. La verdadera información está después de esos dos parámetros.
Poniendo todo junto
Entonces básicamente podemos cifrar y descifrar información con JavaScript de una manera sencilla para el usuario. Lo único que necesitamos es…
- Para cifrar necesitamos la contraseña como string y la información que vamos a proteger como string.
- Para descifrar necesitamos la contraseña como string y la información cifrada como string en base64.
El código fuente de la demostración está aquí.
Conclusión
Asegurar la información confidencial es importante. Con esta API moderna y segura de JavaScript podemos encriptar información usando AES, y después guardarla en el navegador, en Firebase, en un servidor, etcétera.
Podemos estar seguros de que nadie más podrá acceder a la información descifrada al menos que cuenten con la contraseña.
Se me ocurre que podrías usar esto para cosas confidenciales como un gestor de contraseñas, un diario personal, notas seguras, etcétera.
A ver si después hago algunos posts usando esta API. Mientras tanto basta con la demostración que ya dejé al inicio del post.
Por aquí te dejo más tutoriales de JavaScript en mi blog.
Hola, gracias por tu post, tengo una duda; teniendo en cuenta que el js se ejecuta del lado de cliente que mecanismos de seguridad se pueden implementar para que una persona no replique el mismo código y finalemente poder desencriptar el mensaje. Gracias.
Mientras la clave de desencriptación se mantenga en secreto, nadie podrá desencriptar el mensaje
Una duda que pasa si esta web se cae, todos los mensajes que cifre ya no podria decifrarlos ?
y el codigo JS de ahi funciona tal cual como el de la pagina para cifrar y desifrar ?
como sea esta Exeelente tu contenido crack ! gracias
Hola. Depende de dónde aloje los mensajes encriptados, como es encriptación del lado del cliente no importa si la web deja de estar disponible, ya que incluso se podría crear una PWA para guardar en caché todos los assets (https://parzibyte.me/blog/2021/11/23/crear-publicar-progressive-web-app-convertir-app-web-pwa/)
En cuanto al código, el de la demostración funciona para encriptar y desencriptar sin problema.
Saludos!