Learn how to conceal messages or information within files using C#.

Getting started with Steganography (hide information) on Images with C#

Steganography is the practice of hiding text within files to produce a stegotext. The goal of steganography is to allow someone to converse covertly in such a way that an attacker cannot tell whether or not there is hidden meaning to their conversation as no one without programming knowledge would think that there are messages hidden in images. Steganography works by replacing bits of unused data in regular computer files with bits of your own data. In this case, the data will be the plain text, and the unused data is the least significant bits (LSBs) in the image pixels.

In this article, we'll show you how to hide encrypted information within an image file (JPG,PNG etc) in C#.

1. Create required helper classes

You will need to create the 2 classes and add them to your C# project. The first one is SteganographyHelper. This class is in charge of hiding information on a Bitmap and to retrieve it. It has a helper method to create a version of an image without indexed pixels too:

using System;
using System.Drawing;

class SteganographyHelper
{
    enum State
    {
        HIDING,
        FILL_WITH_ZEROS
    };

    /// <summary>
    /// Creates a bitmap from an image without indexed pixels
    /// </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;
    }
}

The second one is the StringCipher class. With this class we'll encrypt the information that you want to hide on the files, this will obviously increase the protection of your secret data. All of its methods are static so you won't need to create a new instance everytime you encrypt or decrypt something:

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

class StringCipher
{
    // This constant is used to determine the keysize of the encryption algorithm in bits.
    // We divide this by 8 within the code below to get the equivalent number of bytes.
    private const int Keysize = 256;

    // This constant determines the number of iterations for the password bytes generation function.
    private const int DerivationIterations = 1000;

    public static string Encrypt(string plainText, string passPhrase)
    {
        // Salt and IV is randomly generated each time, but is preprended to encrypted cipher text
        // so that the same Salt and IV values can be used when decrypting.  
        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)
    {
        // Get the complete stream of bytes that represent:
        // [32 bytes of Salt] + [32 bytes of IV] + [n bytes of CipherText]
        var cipherTextBytesWithSaltAndIv = Convert.FromBase64String(cipherText);
        // Get the saltbytes by extracting the first 32 bytes from the supplied cipherText bytes.
        var saltStringBytes = cipherTextBytesWithSaltAndIv.Take(Keysize / 8).ToArray();
        // Get the IV bytes by extracting the next 32 bytes from the supplied cipherText bytes.
        var ivStringBytes = cipherTextBytesWithSaltAndIv.Skip(Keysize / 8).Take(Keysize / 8).ToArray();
        // Get the actual cipher text bytes by removing the first 64 bytes from the cipherText string.
        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())
        {
            // Fill the array with cryptographically secure random bytes.
            rngCsp.GetBytes(randomBytes);
        }
        return randomBytes;
    }
}

2. Conceal and retrieve information on image

With the providen helper classes of the first step, you will be able to conceal your secret information on images and retrieve it within seconds:

A. Conceal and encrypt data

To hide information on a file you will need to declare a password. This password allows you to retrieve the information from your file later. Then store the information that you want to hide on the image on a string variable, this will be encrypted with the same password too. As next, create two variables that will store the path to the files (the original and the new file that will be created with the information) and proceed to create a new instance of the SteganographyHelper.

Encrypt the data using your password and the helper, this will generate another string (the one that will be stored on the image). Now it's important to create a version of the image without indexed pixels using the CreateNonIndexedImage method of the helper. If you don't do it, the usage of the algorithm will throw (with some images, not all of them) the following exception:

SetPixel is not supported for images with indexed pixel formats.

Using the MergeText method of the helper, hide the encrypted text in the non indexed version of the image and store it wherever you want:

// Declare the password that will allow you to retrieve the encrypted data later
string _PASSWORD = "password";

// The String data to conceal on the image
string _DATA_TO_HIDE = "Hello, no one should know that my password is 12345";

// Declare the path where the original image is located
string pathOriginalImage = @"C:\Users\sdkca\Desktop\image_example.png";
// Declare the new name of the file that will be generated with the hidden information
string pathResultImage = @"C:\Users\sdkca\Desktop\image_example_with_hidden_information.png";

// Create an instance of the SteganographyHelper
SteganographyHelper helper = new SteganographyHelper();

// Encrypt your data to increase security
// Remember: only the encrypted data should be stored on the image
string encryptedData = StringCipher.Encrypt(_DATA_TO_HIDE, _PASSWORD);

// Create an instance of the original image without indexed pixels
Bitmap originalImage = SteganographyHelper.CreateNonIndexedImage(Image.FromFile(pathOriginalImage));
// Conceal the encrypted data on the image !
Bitmap imageWithHiddenData = SteganographyHelper.MergeText(encryptedData, originalImage);

// Save the image with the hidden information somewhere :)
// In this case the generated file will be image_example_with_hidden_information.png
imageWithHiddenData.Save(pathResultImage);

In this case, the information that we stored on the image is a simple string namely "... no one should know that my password is ...".

B. Retrieve and decrypt data

To retrieve the data from the image created on the previous step, you only need the ExtractText method of the helper. This will return the encrypted information that you can easily decrypt using the previously set password:

// The password used to hide the information on the previous step
string _PASSWORD = "password";

// The path to the image that contains the hidden information
string pathImageWithHiddenInformation = @"C:\Users\sdkca\Desktop\image_example_with_hidden_information.png";

// Create an instance of the SteganographyHelper
SteganographyHelper helper = new SteganographyHelper();

// Retrieve the encrypted data from the image
string encryptedData = SteganographyHelper.ExtractText(
    new Bitmap(
        Image.FromFile(pathImageWithHiddenInformation)
    )
);

// Decrypt the retrieven data on the image
string decryptedData = StringCipher.Decrypt(encryptedData, _PASSWORD);

// Display the secret text in the console or in a messagebox
// In our case is "Hello, no one should know that my password is 12345"
Console.WriteLine(decryptedData);
//MessageBox.Show(decryptedData);

Note that in this implementation we aren't aware of errors or an user interface so is up to you to implement it. Besides, you may like to wrap the code to be execute in another thread to prevent your UI from being freezed.

Happy coding !


Senior Software Engineer at Software Medico. Interested in programming since he was 14 years old, Carlos is a self-taught programmer and founder and author of most of the articles at Our Code World.

Sponsors