Introducción a la esteganografía (ocultar información) en imágenes con C#

Introducción a la esteganografía (ocultar información) en imágenes con C#

La esteganografía es la práctica de ocultar texto dentro de archivos para producir un stegotexto. El objetivo de la esteganografía es permitir que alguien converse de forma encubierta de tal manera que un atacante no pueda saber si hay un significado oculto en su conversación, ya que nadie sin conocimientos de programación pensaría que hay mensajes ocultos en imágenes. La esteganografía funciona reemplazando bits de datos no utilizados en archivos de computadora normales con bits de sus propios datos. En este caso, los datos serán el texto sin formato y los datos no utilizados son los bits menos significativos (LSB) en los píxeles de la imagen.

En este artículo, le mostraremos cómo ocultar información cifrada dentro de un archivo de imagen (JPG, PNG, etc.) en C#.

1. Cree las clases auxiliares necesarias

Deberá crear las 2 clases y agregarlas a su proyecto C #. El primero es SteganographyHelper. Esta clase se encarga de ocultar información sobre un Bitmapy recuperarla. También tiene un método auxiliar para crear una versión de una imagen sin píxeles indexados:

using System;
using System.Drawing;

class SteganographyHelper
{
    enum State
    {
        HIDING,
        FILL_WITH_ZEROS
    };

    /// <summary>
    /// Crea un mapa de bits a partir de una imagen sin píxeles indexados
    /// </summary>
    /// <param name="src"></param>
    /// <returns></returns>
    public static Bitmap CreateNonIndexedImage(Image src)
    {
        Bitmap newBmp = new Bitmap(src.Width, src.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);

        using (Graphics gfx = Graphics.FromImage(newBmp))
        {
            gfx.DrawImage(src, 0, 0);
        }

        return newBmp;
    }

    public static Bitmap MergeText(string text, Bitmap bmp)
    {
        State s = State.HIDING;

        int charIndex = 0;
        int charValue = 0;
        long colorUnitIndex = 0;

        int zeros = 0;

        int R = 0, G = 0, B = 0;

        for (int i = 0; i < bmp.Height; i++)
        {
            for (int j = 0; j < bmp.Width; j++)
            {
                Color pixel = bmp.GetPixel(j, i);

                pixel = Color.FromArgb(pixel.R - pixel.R % 2,
                    pixel.G - pixel.G % 2, pixel.B - pixel.B % 2);

                R = pixel.R; G = pixel.G; B = pixel.B;

                for (int n = 0; n < 3; n++)
                {
                    if (colorUnitIndex % 8 == 0)
                    {
                        if (zeros == 8)
                        {
                            if ((colorUnitIndex - 1) % 3 < 2)
                            {
                                bmp.SetPixel(j, i, Color.FromArgb(R, G, B));
                            }

                            return bmp;
                        }

                        if (charIndex >= text.Length)
                        {
                            s = State.FILL_WITH_ZEROS;
                        }
                        else
                        {
                            charValue = text[charIndex++];
                        }
                    }

                    switch (colorUnitIndex % 3)
                    {
                        case 0:
                            {
                                if (s == State.HIDING)
                                {
                                    R += charValue % 2;

                                    charValue /= 2;
                                }
                            }
                            break;
                        case 1:
                            {
                                if (s == State.HIDING)
                                {
                                    G += charValue % 2;

                                    charValue /= 2;
                                }
                            }
                            break;
                        case 2:
                            {
                                if (s == State.HIDING)
                                {
                                    B += charValue % 2;

                                    charValue /= 2;
                                }

                                bmp.SetPixel(j, i, Color.FromArgb(R, G, B));
                            }
                            break;
                    }

                    colorUnitIndex++;

                    if (s == State.FILL_WITH_ZEROS)
                    {
                        zeros++;
                    }
                }
            }
        }

        return bmp;
    }

    public static string ExtractText(Bitmap bmp)
    {
        int colorUnitIndex = 0;
        int charValue = 0;

        string extractedText = String.Empty;

        for (int i = 0; i < bmp.Height; i++)
        {
            for (int j = 0; j < bmp.Width; j++)
            {
                Color pixel = bmp.GetPixel(j, i);

                for (int n = 0; n < 3; n++)
                {
                    switch (colorUnitIndex % 3)
                    {
                        case 0:
                            {
                                charValue = charValue * 2 + pixel.R % 2;
                            }
                            break;
                        case 1:
                            {
                                charValue = charValue * 2 + pixel.G % 2;
                            }
                            break;
                        case 2:
                            {
                                charValue = charValue * 2 + pixel.B % 2;
                            }
                            break;
                    }

                    colorUnitIndex++;

                    if (colorUnitIndex % 8 == 0)
                    {
                        charValue = reverseBits(charValue);

                        if (charValue == 0)
                        {
                            return extractedText;
                        }

                        char c = (char)charValue;

                        extractedText += c.ToString();
                    }
                }
            }
        }

        return extractedText;
    }

    public static int reverseBits(int n)
    {
        int result = 0;

        for (int i = 0; i < 8; i++)
        {
            result = result * 2 + n % 2;

            n /= 2;
        }

        return result;
    }
}

El segundo es la clase StringCipher. Con esta clase encriptaremos la información que desea ocultar en los archivos, esto obviamente aumentará la protección de sus datos secretos. Todos sus métodos son estáticos, por lo que no necesitará crear una nueva instancia cada vez que cifre o descifre algo:

using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

class StringCipher
{
     // Esta constante se utiliza para determinar el tamaño de la clave del algoritmo de cifrado en bits.
     // Dividimos esto por 8 dentro del código siguiente para obtener el número equivalente de bytes.
    private const int Keysize = 256;

    // Esta constante determina el número de iteraciones para la función de generación de bytes de contraseña.
    private const int DerivationIterations = 1000;

    public static string Encrypt(string plainText, string passPhrase)
    {
         // Salt y IV se generan aleatoriamente cada vez, pero están preprended a texto cifrado cifrado
         // para que se puedan utilizar los mismos valores de Salt e IV al descifrar.
        var saltStringBytes = Generate256BitsOfRandomEntropy();
        var ivStringBytes = Generate256BitsOfRandomEntropy();
        var plainTextBytes = Encoding.UTF8.GetBytes(plainText);
        using (var password = new Rfc2898DeriveBytes(passPhrase, saltStringBytes, DerivationIterations))
        {
            var keyBytes = password.GetBytes(Keysize / 8);
            using (var symmetricKey = new RijndaelManaged())
            {
                symmetricKey.BlockSize = 256;
                symmetricKey.Mode = CipherMode.CBC;
                symmetricKey.Padding = PaddingMode.PKCS7;
                using (var encryptor = symmetricKey.CreateEncryptor(keyBytes, ivStringBytes))
                {
                    using (var memoryStream = new MemoryStream())
                    {
                        using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
                        {
                            cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);
                            cryptoStream.FlushFinalBlock();
                            // Create the final bytes as a concatenation of the random salt bytes, the random iv bytes and the cipher bytes.
                            var cipherTextBytes = saltStringBytes;
                            cipherTextBytes = cipherTextBytes.Concat(ivStringBytes).ToArray();
                            cipherTextBytes = cipherTextBytes.Concat(memoryStream.ToArray()).ToArray();
                            memoryStream.Close();
                            cryptoStream.Close();
                            return Convert.ToBase64String(cipherTextBytes);
                        }
                    }
                }
            }
        }
    }

    public static string Decrypt(string cipherText, string passPhrase)
    {
        // Obtenga el flujo completo de bytes que representan:
        // [32 bytes of Salt] + [32 bytes of IV] + [n bytes of CipherText]
        var cipherTextBytesWithSaltAndIv = Convert.FromBase64String(cipherText);
        // Obtenga los saltbytes extrayendo los primeros 32 bytes de los bytes cipherText proporcionados.
        var saltStringBytes = cipherTextBytesWithSaltAndIv.Take(Keysize / 8).ToArray();
        // Obtenga los bytes IV extrayendo los siguientes 32 bytes de los bytes cipherText proporcionados.
        var ivStringBytes = cipherTextBytesWithSaltAndIv.Skip(Keysize / 8).Take(Keysize / 8).ToArray();
        // Obtenga los bytes de texto cifrados reales eliminando los primeros 64 bytes de la cadena cipherText.
        var cipherTextBytes = cipherTextBytesWithSaltAndIv.Skip((Keysize / 8) * 2).Take(cipherTextBytesWithSaltAndIv.Length - ((Keysize / 8) * 2)).ToArray();

        using (var password = new Rfc2898DeriveBytes(passPhrase, saltStringBytes, DerivationIterations))
        {
            var keyBytes = password.GetBytes(Keysize / 8);
            using (var symmetricKey = new RijndaelManaged())
            {
                symmetricKey.BlockSize = 256;
                symmetricKey.Mode = CipherMode.CBC;
                symmetricKey.Padding = PaddingMode.PKCS7;
                using (var decryptor = symmetricKey.CreateDecryptor(keyBytes, ivStringBytes))
                {
                    using (var memoryStream = new MemoryStream(cipherTextBytes))
                    {
                        using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read))
                        {
                            var plainTextBytes = new byte[cipherTextBytes.Length];
                            var decryptedByteCount = cryptoStream.Read(plainTextBytes, 0, plainTextBytes.Length);
                            memoryStream.Close();
                            cryptoStream.Close();
                            return Encoding.UTF8.GetString(plainTextBytes, 0, decryptedByteCount);
                        }
                    }
                }
            }
        }
    }

    private static byte[] Generate256BitsOfRandomEntropy()
    {
        var randomBytes = new byte[32]; // 32 Bytes will give us 256 bits.
        using (var rngCsp = new RNGCryptoServiceProvider())
        {
            // Llene la matriz con bytes aleatorios criptográficamente seguros.
            rngCsp.GetBytes(randomBytes);
        }
        return randomBytes;
    }
}

2. Ocultar y recuperar información en la imagen

Con las clases de ayuda proporcionadas del primer paso, podrá ocultar su información secreta en imágenes y recuperarla en segundos:

A. Ocultar y cifrar datos

Para ocultar información en un archivo, deberá declarar una contraseña. Esta contraseña le permite recuperar la información de su archivo más tarde. Luego, almacene la información que desea ocultar en la imagen en una variable de cadena, esto también se cifrará con la misma contraseña. A continuación, cree dos variables que almacenarán la ruta a los archivos (el archivo original y el nuevo que se creará con la información) y proceda a crear una nueva instancia del SteganographyHelper.

Cifre los datos usando su contraseña y el ayudante, esto generará otra cadena (la que se almacenará en la imagen). Ahora es importante crear una versión de la imagen sin píxeles indexados utilizando el CreateNonIndexedImagemétodo del ayudante. Si no lo hace, el uso del algoritmo arrojará (con algunas imágenes, no todas) la siguiente excepción:

SetPixel no es compatible con imágenes con formatos de píxeles indexados.

Usando el MergeTextmétodo del ayudante, oculte el texto encriptado en la versión no indexada de la imagen y guárdelo donde desee:

// Declare la contraseña que le permitirá recuperar los datos cifrados más tarde
string _PASSWORD = "password";

// Los datos de cadena para ocultar en la imagen
string _DATA_TO_HIDE = "Hello, no one should know that my password is 12345";

// Declare la ruta donde se encuentra la imagen original
string pathOriginalImage = @"C:\Users\sdkca\Desktop\image_example.png";
// Declare el nuevo nombre del archivo que se generará con la información oculta
string pathResultImage = @"C:\Users\sdkca\Desktop\image_example_with_hidden_information.png";

// Cree una instancia de SteganographyHelper
SteganographyHelper helper = new SteganographyHelper();

// Cifre sus datos para aumentar la seguridad
// Recuerde: solo los datos cifrados deben almacenarse en la imagen
string encryptedData = StringCipher.Encrypt(_DATA_TO_HIDE, _PASSWORD);

// Crea una instancia de la imagen original sin píxeles indexados
Bitmap originalImage = SteganographyHelper.CreateNonIndexedImage(Image.FromFile(pathOriginalImage));
// ¡Oculte los datos cifrados en la imagen!
Bitmap imageWithHiddenData = SteganographyHelper.MergeText(encryptedData, originalImage);

// Guarda la imagen con la información oculta en algún lugar :)
// En este caso, el archivo generado será image_example_with_hidden_information.png
imageWithHiddenData.Save(pathResultImage);

En este caso, la información que almacenamos en la imagen es una cadena simple, a saber, "... nadie debería saber que mi contraseña es ...".

B. Recuperar y descifrar datos

Para recuperar los datos de la imagen creada en el paso anterior, solo necesita el ExtractTextmétodo del ayudante. Esto le devolverá la información cifrada que puede descifrar fácilmente utilizando la contraseña establecida anteriormente:

// La contraseña utilizada para ocultar la información del paso anterior.
string _PASSWORD = "password";

// La ruta a la imagen que contiene la información oculta.
string pathImageWithHiddenInformation = @"C:\Users\sdkca\Desktop\image_example_with_hidden_information.png";

// Cree una instancia de SteganographyHelper
SteganographyHelper helper = new SteganographyHelper();

// Recupere los datos cifrados de la imagen
string encryptedData = SteganographyHelper.ExtractText(
    new Bitmap(
        Image.FromFile(pathImageWithHiddenInformation)
    )
);

// Descifre los datos recuperados en la imagen
string decryptedData = StringCipher.Decrypt(encryptedData, _PASSWORD);

// Mostrar el texto secreto en la consola o en un cuadro de mensaje
// En nuestro caso es "Hola, nadie debería saber que mi contraseña es 12345"
Console.WriteLine(decryptedData);
//MessageBox.Show(decryptedData);

Tenga en cuenta que en esta implementación no somos conscientes de los errores o de una interfaz de usuario, por lo que depende de usted implementarla. Además, es posible que desee ajustar el código que se ejecutará en otro hilo para evitar que se congele la interfaz de usuario.

Que te diviertas ❤️!

Esto podria interesarte

Conviertete en un programador más sociable