Esconder información en imagen de JavaScript con esteganografía

Esconder información en imágenes con JavaScript y Esteganografía

Siguiendo con los tutoriales de esteganografía para ocultar información en una foto sin que el ojo humano lo pueda notar vamos a ver cómo aplicar dicha técnica con JavaScript en el lado del cliente.

Con el tutorial de hoy vas a aprender a usar la Esteganografía en imágenes con JS desde el navegador web sin necesidad de instalar nada. Serás capaz de ocultar cualquier mensaje secreto en la imagen sin que se vea alterada.

Esconder información en imagen de JavaScript con esteganografía
Esconder información en imagen de JavaScript con esteganografía

Si combinas lo expuesto en este post con el artículo de encriptación de información en JavaScript vas a poder encriptar y esconder mensajes usando Esteganografía.

Bonus: de hecho la misma imagen que acompaña este post tiene un mensaje oculto. Descarga la imagen y lee su contenido: https://parzibyte.me/blog/wp-content/uploads/2024/06/Esconder-informacion-en-imagen-de-JavaScript-con-esteganografia.png

Puedes acceder a la demostración ya mismo en el siguiente enlace sin salir de tu navegador. Prueba seleccionando una imagen y ocultando un mensaje, después regresa a este post para saber cómo fue programado: https://parzibyte.github.io/ejemplos-javascript/esteganografia/

Explicación de esteganografía

Para esconder texto en una imagen escondemos un bit de información en el bit menos significativo de un nivel de color. Las imágenes se componen de pixeles, y cada pixel tiene el nivel Rojo, Verde y Azul, además del alfa en algunos casos.

Lo que hacemos es modificar el LSB de ese nivel de color. Al alterar solo en 1 al nivel, la imagen no se ve alterada; el ojo humano no lo puede notar, y en algunos casos hay coincidencias que hacen que el color ni siquiera sea alterado.

Si quieres saber más sobre el algoritmo en sí te invito a leer mi post sobre cómo funciona la esteganografía y también ver el siguiente vídeo:

Ahora que ya conoces los fundamentos veamos cómo aplicar la esteganografía con JavaScript del lado del cliente.

Esteganografía con JavaScript del lado del cliente

Pixeles

Comenzamos obteniendo los pixeles de una imagen, lo cual ya te he mostrado en otro post. Estoy usando un OffscreenCanvas global:

const dibujarImagenEnCanvasGlobal = async (imagen) => {
    nombreImagenSinExtension = extraerNombreSinExtension(imagen.name);
    extensionImagen = extraerExtensionDeArchivo(imagen.name);
    const imagenComoBitmap = await createImageBitmap(imagen);
    canvasFueraDePantalla = new OffscreenCanvas(imagenComoBitmap.width, imagenComoBitmap.height);
    contextoCanvas = canvasFueraDePantalla.getContext("2d");
    contextoCanvas.drawImage(imagenComoBitmap, 0, 0, imagenComoBitmap.width, imagenComoBitmap.height);
}

const obtenerImageData = () => {
    return contextoCanvas.getImageData(0, 0, canvasFueraDePantalla.width, canvasFueraDePantalla.height);
}

Imágenes soportadas

He probado con imágenes PNG, JPG y WEBP. En todas funciona perfectamente; pero no estoy usando el canal alfa en ninguna. Explicaré este punto del canal alfa más adelante.

Ocultar información

Comencemos viendo cómo esconder un mensaje en la imagen ahora que ya tenemos los pixeles. En este caso la imagen será seleccionada por el usuario en un input de tipo file, aunque la misma puede venir de cualquier lugar.

Convertimos el mensaje a un arreglo de bits, es decir, de ceros y unos. El tipo de dato será number, aunque ahora que lo pienso podría ser boolean.

const obtenerBitsDeMensaje = (mensaje) => {
    const bits = [];
    for (let indiceByte = 0; indiceByte < mensaje.length; indiceByte++) {
        // Obtener entero ASCII de la letra (byte) actual...
        const charCode = mensaje.charCodeAt(indiceByte);
        // Recorrer cada bit
        for (let indiceBit = 7; indiceBit >= 0; indiceBit--) {
            const bit = (charCode >> indiceBit) & 1;
            //console.log(`Charcode ${charCode} con índice bit ${indiceBit} y bit valor ${bit}`);
            bits.push(bit);
        }
    }
    return bits;
}

Además de los bits para el mensaje también necesitamos los 8 bits para el mensaje de la terminación. Una vez que tenemos todos los bits comenzamos a esconderlos modificando el LSB de cada nivel por el bit actual:

const bitsMensaje = obtenerBitsDeMensaje(mensaje).concat(bitsMensajeTerminacion);
let indicePixel = 0;
for (let indiceBit = 0; indiceBit < bitsMensaje.length; indiceBit++) {
    const bitDelMensaje = bitsMensaje[indiceBit];
    //console.log(`Ocultando ${bitDelMensaje} en el nivel con valor ${pixeles[indicePixel]} (posición ${indicePixel}) que será convertido a ${colocarLsbDeNumero(pixeles[indicePixel], bitDelMensaje)}`)
    pixeles[indicePixel] = colocarLsbDeNumero(pixeles[indicePixel], bitDelMensaje);
    indicePixel++;
    // Omitir canal alfa por ahora
    // Por ejemplo 3, 7, 11 son el alfa. Si le sumamos 1 son
    // 4,8,12 que ya se puede comparar para saber si es múltiplo
    // de 4
    if ((indicePixel + 1) % 4 === 0) {
        indicePixel++;
    }
}

En este caso estoy omitiendo el canal alfa porque desconozco qué pasaría si lo modifico según los distintos formatos de imagen. Eres libre de modificar el código para no omitir el canal alfa.

Una vez que hemos terminado de modificar los pixeles los volvemos a escribir en el canvas y lo descargamos:

const descargarCanvas = async () => {
    let fotoComoBlob = await canvasFueraDePantalla.convertToBlob();
    const a = document.createElement("a");
    const archivo = new Blob([fotoComoBlob], { type: mimeTypeAPartirDeExtension(extensionImagen) });
    const url = URL.createObjectURL(archivo);
    a.href = url;
    a.download = `${nombreImagenSinExtension}_con_mensaje.${extensionImagen}`;
    a.click();
    URL.revokeObjectURL(url);
}

Como siempre te digo: en este ejemplo concreto la imagen se está descargando, pero tú podrías mostrarla al usuario, enviarla a un servidor, a Telegram o cualquier otra cosa.

Leer mensaje oculto

Ya vimos cómo esconder un mensaje en una imagen con JavaScript, pero ahora veamos lo contrario: cómo leer el mensaje oculto en la imagen.

El proceso de los pixeles y el canvas es el mismo; lo que cambia ahora es que vamos a recorrer los pixeles y leer el bit menos significativo (LSB). Con ese bit vamos a ir armando un byte y, cuando lo tengamos, vamos a convertirlo a carácter para agregarlo a la cadena del mensaje resultante.

Nota importante: técnicamente aquí el byte es de tipo number y el carácter es de tipo string, ya que en JavaScript no tenemos tipado fuerte ni el tipo de dato char o byte, pero al final el resultado es el mismo, solo lo aclaro para no confundirte.

for (let indice = 0; indice < pixeles.length; indice++) {
    // Omitir canal alfa por ahora
    // Por ejemplo 3, 7, 11 son el alfa. Si le sumamos 1 son
    // 4,8,12 que ya se puede comparar para saber si es múltiplo
    // de 4
    //console.log({ indice });
    if ((indice + 1) % 4 === 0) {
        continue;
    }
    const lsbDelNivelDelColor = obtenerLsb(pixeles[indice]);
    codigoDelCaracterActual = establecerBitEnNumero(codigoDelCaracterActual, indiceBitEnCaracterActual, lsbDelNivelDelColor);
    //console.log(`Agregando LSB ${lsbDelNivelDelColor} del nivel ${pixeles[indice]} al número que hasta ahora es ${codigoDelCaracterActual} en el índice ${indiceBitEnCaracterActual}`);
    if (indiceBitEnCaracterActual === 0) {
        if (codigoDelCaracterActual === CODIGO_CARACTER_TERMINACION) {
            break;
        }
        const letra = String.fromCodePoint(codigoDelCaracterActual);
        //console.log(`Agregando número ${codigoDelCaracterActual} que es ${letra}`);
        mensaje += letra;
        codigoDelCaracterActual = 0;
        indiceBitEnCaracterActual = 8;
    }
    indiceBitEnCaracterActual--;
}

Por favor presta atención al código en la parte donde se compara el codigoDelCaracterActual (byte) con el código del carácter de terminación. Básicamente el carácter de terminación se escribe al final del mensaje secreto para saber en dónde detenerse al leerlo.

Cuando el ciclo se termine (carácter de terminación encontrado) tendremos el mensaje recuperado en dicha variable.

Poniendo todo junto

Ya te enseñé a ocultar y extraer texto secreto en imágenes con JavaScript. No olvides que, para que el mensaje prevalezca, el contenido ni la calidad de la imagen debe ser alterada.

El código completo está en GitHub: https://github.com/parzibyte/ejemplos-javascript/tree/master/esteganografia y la demostración en https://parzibyte.github.io/ejemplos-javascript/esteganografia/.

En este simple ejemplo ocultamos texto; pero al final el texto solo son un montón de bytes. Con este método podemos incrustar incluso archivos binarios, ejecutables, otra imagen y cualquier cosa que pueda ser representada por un montón de bytes; es decir, todo archivo existente.

La relación de tamaño es aproximadamente 3 pixeles de imagen por byte, pero si usáramos el canal alfa podríamos guardar un byte en 2 pixeles.

Por cierto, este mismo algoritmo ya está implementado con Python; lo escribí desde hace 6 años pero sigue funcionando a la fecha de escribir este artículo.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *