Click here to Skip to main content
15,867,308 members
Articles / Programming Languages / C#
Article

Steganography IX - The Cross-Format Solution

Rate me:
Please Sign up or sign in to vote.
4.36/5 (15 votes)
29 Nov 2004CPOL5 min read 71.8K   1.7K   28   14
Merging the pieces into one application.

Introduction

If you have read the earlier articles of this series, you know how to hide binary data in five kinds of media. Why not hide a long message in multiple carriers: one image, two sounds, one .NET assembly, three MIDI sequences, and an AVI video? In this article, we are going to merge all the modules from this series into one big application, and add a few enhancements:

  • The user is able to specify the exact count of bits that shall be hidden in each carrier unit (pixel, wave sample, etc.).
  • The noise generator confuses pictures and wave sounds, so that the key cannot be reconstructed from the carrier file and the unused original file.

Interface Definitions

The carrier files have different formats, and they hide the secret messages in different ways. But all that little differences are internal stuff, basically all files are the same, we need only one class to describe them:

Image 1

The file names are chosen by the user. The carrier is read from SourceFileName, changed and saved to DestinationFileName. NoisePercent is needed for file types that can contain noise (images, videos, sounds). The user can specify how much percent of the file should be covered with noise, for details read the Noise Generator section. CountBitsToHidePerCarrierUnit is chosen by the user, too. Higher values allow more data per carrier, but smaller values make the manipulations less obvious.

The other values are calculated by the application. CountCarrierUnits and CountUseableCarrierUnits are the numbers of carrier units in general (pixel in the image, methods in an assembly, and so on), and units that can be used due to the distribution key. CountBytesToHide is the number of bytes that actually will be hidden in the file. It is calculated from the message, the number of useable units in this file, and the count of units in all files:

C#
carrierFile.CountBytesToHide = messageLength 
  * (carrierFile.CountUseableCarrierUnits / countAllUseableCarrierUnits);

SourceStream is used only for recorded waves, because they are stored in a MemoryStream, not in a file.

After the user has selected a source file, the GUI has to know if this kind of file can contain noise, and how many bits per unit it can carry. A FileType factory can help with that. FileType.CreateFileType returns an object with all the properties we need to adapt the dialog:

Image 2

But, when we finally hide the secret message, don't we need to know what exactly a carrier unit is? No, only the specialized utility classes have to know something about the content. Outside these classes, there are only FileType and FileUtility. FileUtility.CreateFileUtility takes a file name and returns an instance of the correct class.

Image 3

Gathering Carrier Units

Before we start hiding a message, we have to know if the carrier files are big enough. That means, whenever a key or a carrier file is selected, we must count the carrier units and useable carrier units. Counting units is easy, because we don't have to know what exactly we are counting:

C#
CarrierFile carrierFile;
long countAvailableUnits = 0;
for(int n=0; n<listviewCarrierFiles.Items.Count; n++){
    carrierFile = (CarrierFile)listviewCarrierFiles.Items[n].Tag;

    //create a FileUtility which takes care of the details
    FileUtility utility = FileUtility.CreateFileUtility(carrierFile);

    //carrier units * for how many bits they can be used
    carrierFile.CountUseableCarrierUnits =
        utility.CountUseableUnits(key) * 
        carrierFile.CountBitsToHidePerCarrierUnit;

    countAvailableUnits += carrierFile.CountUseableCarrierUnits;
}
//display the result in a custom control
displayCountOfUnitsHide.CountAvailableCarrierUnits = countAvailableUnits;

Once we have a list of files with enough carrier units, the interesting part begins. First, we build a list of all available carrier files:

C#
private CarrierFile[] ListCarrierFiles( ListView listview,
    long messageLength, float countAvailableUnits)
{
    CarrierFile[] carrierFiles = new CarrierFile[listview.Items.Count];
    long sumCountBytesToHide = 0;
    long maxCountBytesToHide = 0;
    int indexMaxCountBytesToHide = 0;

    //list carrier files
    for(int n=0; n<carrierFiles.Length; n++){
        //get file info from the list view
        carrierFiles[n] = (CarrierFile)listview.Items[n].Tag;

        if(messageLength > 0){

          //how many bytes do we want to hide in this file?
          carrierFiles[n].CountBytesToHide = (Int32)Math.Ceiling(
            (float)messageLength *
              ((float)carrierFiles[n].CountUseableCarrierUnits 
              / countAvailableUnits) );

          //which file hides the most bytes?
          sumCountBytesToHide += carrierFiles[n].CountBytesToHide;
          if(carrierFiles[n].CountBytesToHide > maxCountBytesToHide)
          {
            maxCountBytesToHide = carrierFiles[n].CountBytesToHide;
            indexMaxCountBytesToHide = n;
          }
        }
        else
        {
          //do not calculate anything, just list 
          //the files (used for extracting)
          carrierFiles[n].CountBytesToHide = 0;
        }
    }

    if(sumCountBytesToHide > messageLength){
        //correct incorrect count due to Math.Ceiling
        carrierFiles[indexMaxCountBytesToHide].CountBytesToHide
            -= (Int32)(sumCountBytesToHide - messageLength);
    }

    return carrierFiles;
}

Now that we have the complete list of carriers, we are able to cut off parts of the message and use the FileUtiliy classes to hide them:

C#
//create streams for message and key,
//the same way we did in the earlier articles
Stream message = GetMessageStream();
Stream key = GetKeyStream();

//this value has been calculated before, 
//retrieve it from the custom control
float countAvailableUnits = 
  displayCountOfUnitsHide.CountAvailableCarrierUnits;

//actual length + 4 bytes per carrier file 
//to store the length of the parts
long messageLength = message.Length + (4*lvHideCarriers.Items.Count);

//list the carriers
CarrierFile[] carrierFiles = ListCarrierFiles(
                                lvHideCarriers,
                                messageLength,
                                countAvailableUnits);

//loop over the carriers
FileUtility utility;
Stream messageWithCount;
for(int n=0; n<carrierFiles.Length; n++){
    messageWithCount = GetMessagePart(message, 
                           carrierFiles[n].CountBytesToHide);
    utility = FileUtility.CreateFileUtility(carrierFiles[n]);
    utility.Hide(messageWithCount, key);
}

Extracting the message later on is not much different, except we do not know the length of the message yet:

C#
//create a stream for the key,
//the message stream is empty now
Stream message = new MemoryStream();
Stream key = GetKeyStream();

//list the carriers
CarrierFile[] carrierFiles = ListCarrierFiles(lvExtractCarriers, 0, 0);

//loop over the carriers
FileUtility utility;
Stream messagePart = new MemoryStream();
byte[] buffer;
for(int n=0; n<carrierFiles.Length; n++){
    utility = FileUtility.CreateFileUtility(carrierFiles[n]);
    messagePart.SetLength(0);
    utility.Extract(messagePart, key);

    //add the extracted part to the entire message
    buffer = new byte[messagePart.Length];
    messagePart.Seek(0, SeekOrigin.Begin);
    messagePart.Read(buffer, 0, buffer.Length);
    message.Write(buffer, 0, buffer.Length);
}

messagePart.Close();
message.Seek(0, SeekOrigin.Begin);

if(rdoExtractMsgFile.Checked){
    //save the message to a file...
    FileStream fs = new FileStream(txtExtractMsgFile.Text, FileMode.Create);
    buffer = new byte[message.Length];
    message.Read(buffer, 0, buffer.Length);
    fs.Write(buffer, 0, buffer.Length);
    fs.Close();
    message.Close();
}else{
    //...or display the message - this dispalyschaos 
    //if the message was not Unicode encoded
    StreamReader reader = new StreamReader(message, Encoding.Unicode);
    txtExtractMsgText.Text = reader.ReadToEnd();
    reader.Close();
}

Adding a little Confusion - The Noise Generator

Should we do it or not? Noise in an image/sound - visible or not - is always a hint that the file may contain hidden data. But without additional noise, the key used to locate the pixels or samples can be reconstructed from the original file and the carrier file. We'll let the users decide for themselves how much of each file they want covered with random values.

Image 4

Let me demonstrate different amounts of noise in a bitmap. This is the original, message-free and noise-free image:

Image 5

If we use four bits per carrier unit, the noise generator will place random values only in the last four bits, too. You won't see much difference...

Image 6

...but the chaos is there, and every image processor can detect it in the lower four bits. Here are the same images, with the same message, the same key, and all eight bits noisy:

Image 7

Wave sounds are more sensitive to noise. In the lower bits, it is not hearable, but if you change eight bits of a sample (usually that are all bits for one channel), you cannot spread noise over more than 1 percent of the sound. For example, listen to this 8-bit mono sound [137 Kb]. It contains nothing but simple F major (yes, C# major would be funnier, but F is my favorite). With two percent of the samples replaced by random samples, the noise is very annoying.

  • 0.wav: F major, no noise
  • 001.wav: F major, 0.01%, 8 bit/sample changed
  • 025.wav: F major, 0.25%, 8 bit/sample changed
  • 01.wav: F major, 0.10%, 8 bit/sample changed
  • 05.wav: F major, 0.50%, 8 bit/sample changed
  • 1.wav: F major, 1.00%, 8 bit/sample changed
  • 2.wav: F major, 2.00%, 8 bit/sample changed

Of course, you should never change eight bits per carrier unit, one or two are far enough. I used 8 bits for these examples to make the noise visible/hearable for human eyes/ears.

I tried to give you an overview of the project in this article. If you do not understand the source code, feel free to ask me. If you find mistakes that are too bad to be tolerated even in experimental code (and this is an experimental application), please let me know how to correct them :-)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Germany Germany
Corinna lives in Hanover/Germany and works as a C# developer.

Comments and Discussions

 
GeneralImage to Avi Format Pin
Anubhava Dimri25-Aug-09 19:09
Anubhava Dimri25-Aug-09 19:09 
GeneralRe: Image to Avi Format Pin
Corinna John25-Aug-09 21:05
Corinna John25-Aug-09 21:05 
GeneralRe: Image to Avi Format Pin
Anubhava Dimri25-Aug-09 21:14
Anubhava Dimri25-Aug-09 21:14 
GeneralProblem AVI Pin
Musharaf Zaheer Qureshi24-Jan-07 7:12
Musharaf Zaheer Qureshi24-Jan-07 7:12 
GeneralRe: Problem AVI Pin
Corinna John25-Jan-07 8:58
Corinna John25-Jan-07 8:58 
GeneralVS2005 Compiler Warning Pin
Moomansun8-Sep-06 7:52
Moomansun8-Sep-06 7:52 
GeneralReal world applications Pin
Douglas Troy29-Jun-05 8:13
Douglas Troy29-Jun-05 8:13 
Generalhey, you are crazy! Pin
Huisheng Chen29-Jun-05 5:01
Huisheng Chen29-Jun-05 5:01 
anything that you cannot hide data in it?;P

Regards,
unruledboy@hotmail.com
GeneralYeah, I am crazy :-) Pin
Corinna John29-Jun-05 5:39
Corinna John29-Jun-05 5:39 
QuestionVC6++ HELP HOW??? Pin
cnncnn18-Aug-04 20:24
cnncnn18-Aug-04 20:24 
GeneralC'est magnifique! Pin
Inverarity30-Jul-04 4:56
sussInverarity30-Jul-04 4:56 
GeneralRe: C'est magnifique! Pin
Corinna John31-Jul-04 11:22
Corinna John31-Jul-04 11:22 
Generalcomplex data hiding... Pin
HumanOsc29-May-04 2:05
HumanOsc29-May-04 2:05 
GeneralRe: complex data hiding... Pin
Corinna John31-May-04 8:31
Corinna John31-May-04 8:31 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.