Click here to Skip to main content
Click here to Skip to main content

Steganography IX - The Cross-Format Solution

, 29 Nov 2004 CPOL
Rate this:
Please Sign up or sign in to vote.
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:

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:

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:

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.

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:

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:

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:

//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:

//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.

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

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...

...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:

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 Smile | :)

License

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

Share

About the Author

Corinna John
Software Developer
Germany Germany
Corinna lives in Hannover/Germany (CeBIT City) and works as a Delphi developer, though her favorite language is C#.

Comments and Discussions

 
GeneralImage to Avi Format Pinmembereg_Anubhava25-Aug-09 19:09 
GeneralRe: Image to Avi Format PinmemberCorinna John25-Aug-09 21:05 
GeneralRe: Image to Avi Format Pinmembereg_Anubhava25-Aug-09 21:14 
GeneralProblem AVI PinmemberMusharaf Zaheer Qureshi24-Jan-07 7:12 
GeneralRe: Problem AVI PinmemberCorinna John25-Jan-07 8:58 
GeneralVS2005 Compiler Warning PinmemberMoomansun8-Sep-06 7:52 
GeneralReal world applications PinmemberDouglas Troy29-Jun-05 8:13 
Generalhey, you are crazy! PinmemberUnruled Boy29-Jun-05 5:01 
GeneralYeah, I am crazy :-) PinmemberCorinna John29-Jun-05 5:39 
QuestionVC6++ HELP HOW??? Pinmembercnncnn18-Aug-04 20:24 
GeneralC'est magnifique! PinsussInverarity30-Jul-04 4:56 
GeneralRe: C'est magnifique! PinmemberCorinna John31-Jul-04 11:22 
Generalcomplex data hiding... PinmemberHumanOsc29-May-04 2:05 
GeneralRe: complex data hiding... PinmemberCorinna John31-May-04 8:31 

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

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

| Advertise | Privacy | Mobile
Web01 | 2.8.141022.2 | Last Updated 29 Nov 2004
Article Copyright 2004 by Corinna John
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid