paint-brush
Esteganografía: cómo ocultar texto en imágenes usando JavaScriptpor@andriiromasiun
1,024 lecturas
1,024 lecturas

Esteganografía: cómo ocultar texto en imágenes usando JavaScript

por Andrii Romasiun9m2024/06/26
Read on Terminal Reader

Demasiado Largo; Para Leer

La esteganografía es un método para ocultar mensajes secretos en otro archivo no secreto. Los usuarios pueden cargar una imagen para leer el mensaje oculto que contiene o codificar ellos mismos un mensaje en una imagen. Este artículo describe cómo implementar dicho motor de codificación utilizando JavaScript.
featured image - Esteganografía: cómo ocultar texto en imágenes usando JavaScript
Andrii Romasiun HackerNoon profile picture
0-item

¿Qué es la esteganografía?

Imagina que quieres enviar un mensaje secreto a un amigo, pero el canal que deseas utilizar está comprometido y monitoreado. Podrías usar algo de cifrado, pero eso despertaría sospechas en las personas que monitorean tus conversaciones, por lo que tendrías que usar algo más.

Hoy en día, la esteganografía es un método para ocultar mensajes secretos en otro archivo no secreto (como la imagen de un gato), de modo que si envías ese archivo, no será detectado. La esteganografía no se limita a ocultar texto en imágenes y generalmente significa "ocultar información secreta en otro mensaje u objeto físico no secreto": puede ocultar algunos mensajes en audio, video u otros textos usando, por ejemplo, la transposición de columnas.


La esteganografía también puede resultar extremadamente útil en muchos otros casos; por ejemplo, puede ser una buena alternativa a las marcas de agua en documentos confidenciales para protegerlos contra fugas.


Hay muchas formas de ocultar información en imágenes, desde simplemente agregar el texto al final del archivo hasta ocultarlo en los metadatos. En este artículo, quiero cubrir un método más avanzado de esteganografía, bajando al nivel binario y ocultando mensajes dentro de los límites de la imagen misma.

Construyendo un motor esteganográfico

La interfaz de usuario

Para mi ejemplo de esteganografía, decidí usar JavaScript porque es un lenguaje de programación potente que se puede ejecutar en un navegador.


He creado una interfaz sencilla que permite a los usuarios cargar una imagen para leer el mensaje oculto que contiene o codificar ellos mismos un mensaje en una imagen.


 <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Steganography</title> <style> body { font-family: Arial, sans-serif; text-align: center; margin-top: 50px; } textarea { width: 300px; height: 100px; } button { margin: 10px; } #outputImage { margin-top: 20px; max-width: 100%; } </style> </head> <body> <h1>Steganography Example</h1> <input type="file" id="upload" accept="image/*"><br> <canvas id="canvas" style="display:none;"></canvas><br> <textarea id="text" placeholder="Enter text to encode"></textarea><br> <button id="encode">Encode Text</button> <button id="decode">Decode Text</button> <p id="decodedText"></p> <img id="outputImage" alt="Output Image"> <script src="./script.js"></script> </body> </html>


Para usarlo, los usuarios pueden simplemente seleccionar una imagen que quieran manipular e intentar decodificar parte del texto de ella o codificarla y descargar la imagen más tarde.



Procesamiento de imágenes

Para trabajar con imágenes en JavaScript, podemos utilizar la API de Canvas . Proporciona muchas funciones diferentes para manipular y dibujar imágenes, animaciones o incluso gráficos y vídeos de juegos.


Canvas API se utiliza principalmente para gráficos 2D. Si desea trabajar con gráficos 3D acelerados por hardware, puede utilizar la API WebGL (que, dicho sea de paso, también utiliza el elemento <canvas>).


 const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); const image = new Image();


Para leer el archivo de imagen del sistema de archivos y agregarlo al contexto del lienzo, podemos usar la API FileReader . Nos permite leer fácilmente el contenido de cualquier archivo almacenado en el ordenador del usuario sin necesidad de una biblioteca personalizada.

 function handleFileUpload(event) { const reader = new FileReader(); reader.onload = function (e) { image.src = e.target.result; image.onload = function () { canvas.width = image.width; canvas.height = image.height; ctx.drawImage(image, 0, 0); }; }; reader.readAsDataURL(event.target.files[0]); }


Lee un archivo y dibuja la imagen de ese archivo en nuestro contexto de lienzo 2D previamente definido, después de lo cual podemos codificar algo de texto en esa imagen o intentar leer el texto de la imagen.

Ocultar texto en imágenes

Las imágenes están formadas por píxeles y cada píxel contiene información sobre sus colores. Por ejemplo, si una imagen se codifica utilizando el modelo RGBA, cada píxel contendría 4 bytes de información sobre cuánto rojo, verde, azul y alfa (opacidad) representa.


Para codificar algún texto en una imagen, podríamos usar uno de estos canales (por ejemplo, el canal alfa). Como esta información está representada en el sistema binario (como 01001100), podríamos cambiar el último bit a lo que necesitemos. Se llama Bit Menos Significativo (LSB) y cambiarlo provoca un cambio mínimo en la imagen misma, haciéndola indistinguible de un ser humano.


Ahora imagina que tenemos un texto como "Hola" y queremos codificarlo en una imagen. El algoritmo para hacer esto sería


  1. Convierta el texto "Hola" a binario.


  2. Itere a través de los bytes de datos de la imagen y reemplace el LSB de esos bytes con un bit del texto binario (cada píxel contiene 4 bytes de datos para cada uno de los colores; en mi ejemplo, quiero cambiar el canal de opacidad de la imagen). , por lo que iteraría cada 4º byte).


  3. Añade un byte nulo al final del mensaje para que al decodificar sepamos cuándo parar.


  4. Aplique bytes de imagen modificados a la imagen misma.


Primero, debemos tomar del usuario el texto que queremos codificar y realizar algunas validaciones básicas.

 const text = document.getElementById("text").value; if (!text) { alert("Please enter some text to encode."); return; }


Luego, necesitamos convertir el texto a binario y crear un lienzo de la imagen en la que vamos a codificar este texto.

 const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imgData.data; let binaryText = ""; for (let i = 0; i < text.length; i++) { let binaryChar = text.charCodeAt(i).toString(2).padStart(8, "0"); binaryText += binaryChar; }


Para hacer esto, simplemente podemos iterar sobre cada uno de los caracteres y obtener un índice Unicode usando la función charCodeAt . Luego, este Unicode se convierte a binario y se rellena para que tenga la misma longitud que cualquier otro carácter.


Por ejemplo, la letra "H" se representa como 72 en Unicode; Luego convertimos este número a binario (1001000) y agregamos 0 al principio (01001000) para asegurarnos de que todas las letras tengan la misma longitud (8 bits).


Luego, debemos agregar un byte nulo al final del mensaje para asegurarnos de que cuando lo desciframos, podamos distinguir entre el texto real y los datos de píxeles aleatorios de la imagen.

 binaryText += "00000000";


Luego, necesitamos hacer una validación básica para asegurarnos de que la imagen tenga suficientes píxeles para codificar nuestro mensaje y que no se desborde.

 if (binaryText.length > data.length / 4) { alert("Text is too long to encode in this image."); return; }


Y luego viene la parte más interesante, la codificación del mensaje. La matriz de datos que definimos anteriormente contiene información de píxeles en forma de valores RGBA para cada píxel de la imagen. Entonces, si la imagen está codificada en RGBA, cada píxel estaría representado por 4 valores de la matriz de datos; cada valor representa cuánto rojo, verde y azul tiene ese píxel.

 for (let i = 0; i < binaryText.length; i++) { data[i * 4] = (data[i * 4] & 0b11111110) | parseInt(binaryText[i]); } ctx.putImageData(imgData, 0, 0); const outputImage = document.getElementById("outputImage"); outputImage.src = canvas.toDataURL();


En el código anterior, iteramos sobre nuestro texto codificado en binario. data[i * 4] encuentra un byte que necesitamos modificar, y como solo queremos modificar los bytes de un canal en particular, multiplicamos la variable i por 4 para acceder a él.


La operación data[i * 4] & 0b11111110 establece el bit menos significativo en 0 . Por ejemplo, si data[i * 4] es 10101101 en binario, entonces la operación 10101101 & 11111110 da como resultado 10101100 . Esto asegura que el LSB esté establecido en 0 antes de realizar más manipulaciones con él.


parseInt(binaryText[i]) es un bit actual de la cadena codificada en binario; es 1 o 0 . Luego podemos establecer este bit en LSB usando una operación bit a bit OR ( | ). Por ejemplo, si la parte izquierda del OR bit a bit es 10101100 y el binaryText[i] es 1 , entonces 10101100 | 00000001 daría como resultado 10101101 . Si el bit actual fuera 0 , entonces el OR daría como resultado 10101100 . Por eso tuvimos que eliminar el LSB en primer lugar.


Una vez codificado el mensaje, podemos colocarlo en el lienzo actual y representarlo en HTML usando el método canvas.toDataURL .



Decodificando mensajes ocultos de imágenes

El proceso de decodificar una imagen es en realidad mucho más sencillo que codificar. Como ya sabemos que solo hemos codificado el canal alfa, podemos simplemente iterar sobre cada cuarto byte, leer el último bit, concatenarlo en nuestra cadena final y convertir estos datos de binario a una cadena Unicode.


Primero, necesitamos inicializar las variables. Dado que imgData ya contiene la información de la imagen (llamamos a ctx.drawImage cada vez que leemos un archivo del sistema de archivos), simplemente podemos extraerlo en la variable de datos.


 const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imgData.data; let binaryText = ""; let decodedText = "";


Luego necesitamos iterar sobre cada cuarto byte de la imagen, leer el último bit y concatenarlo con la variable binaryText .

 for (let i = 0; i < data.length; i += 4) { binaryText += (data[i] & 1).toString(); }


data[i] es el byte codificado y, para extraer el LSB, podemos usar el operador AND ( & ) bit a bit. Toma dos valores y realiza una operación AND en cada par de bits correspondientes. Al comparar data[i] con 1 , básicamente aislamos el bit menos significativo de la información del píxel, y si el LSB es 1 , entonces el resultado de dicha operación es 1 . Si el LSB es 0 , el resultado también sería 0 .


Una vez que hayamos leído todos los LSB y los hayamos almacenado en la variable binaryText , debemos convertirlo de binario a texto sin formato. Como sabemos que cada carácter consta de 8 bits (¿recuerdas cómo usamos padStart(8, "0") para que cada carácter tenga la misma longitud?), podemos iterar en cada octavo carácter del binaryText .


Luego podemos usar la operación .slice() para extraer el byte actual del binaryText según nuestra iteración. La cadena binaria se puede convertir en un número usando la función parseInt(byte, 2) . Luego podemos verificar si el resultado es 0 (un byte nulo): detenemos la conversión y consultamos el resultado. De lo contrario, podemos encontrar qué carácter corresponde al número Unicode y agregarlo a nuestra cadena de resultado.


 for (let i = 0; i < binaryText.length; i += 8) { let byte = binaryText.slice(i, i + 8); if (byte.length < 8) break; // Stop if the byte is incomplete let charCode = parseInt(byte, 2); if (charCode === 0) break; // Stop if we hit a null character decodedText += String.fromCharCode(charCode); }


El texto decodificado se puede mostrar de forma segura al usuario:

 document.getElementById("decodedText").textContent = decodedText; 



Dejé el código completo utilizado en este artículo en mi repositorio de GitHub ; siéntete libre de jugar con él. Hay muchas cosas que se podrían mejorar :)

Pensamientos finales

La esteganografía es una técnica muy poderosa y se puede aplicar a muchos casos de uso diferentes, desde la verificación de documentos, la prevención de fugas, la verificación de imágenes mediante IA, la gestión DRM de archivos de música y muchos más. Esta técnica se puede aplicar incluso a vídeos, juegos o incluso texto sin formato, por lo que creo que tiene un enorme potencial.

En la era de las NFT y las blockchains, es aún más interesante ver cómo encontrarán sus casos de uso y cómo evolucionará esta técnica.