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;
FileUtility utility = FileUtility.CreateFileUtility(carrierFile);
carrierFile.CountUseableCarrierUnits =
utility.CountUseableUnits(key) *
carrierFile.CountBitsToHidePerCarrierUnit;
countAvailableUnits += carrierFile.CountUseableCarrierUnits;
}
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;
for(int n=0; n<carrierFiles.Length; n++){
carrierFiles[n] = (CarrierFile)listview.Items[n].Tag;
if(messageLength > 0){
carrierFiles[n].CountBytesToHide = (Int32)Math.Ceiling(
(float)messageLength *
((float)carrierFiles[n].CountUseableCarrierUnits
/ countAvailableUnits) );
sumCountBytesToHide += carrierFiles[n].CountBytesToHide;
if(carrierFiles[n].CountBytesToHide > maxCountBytesToHide)
{
maxCountBytesToHide = carrierFiles[n].CountBytesToHide;
indexMaxCountBytesToHide = n;
}
}
else
{
carrierFiles[n].CountBytesToHide = 0;
}
}
if(sumCountBytesToHide > messageLength){
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:
Stream message = GetMessageStream();
Stream key = GetKeyStream();
float countAvailableUnits =
displayCountOfUnitsHide.CountAvailableCarrierUnits;
long messageLength = message.Length + (4*lvHideCarriers.Items.Count);
CarrierFile[] carrierFiles = ListCarrierFiles(
lvHideCarriers,
messageLength,
countAvailableUnits);
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:
Stream message = new MemoryStream();
Stream key = GetKeyStream();
CarrierFile[] carrierFiles = ListCarrierFiles(lvExtractCarriers, 0, 0);
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);
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){
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{
StreamReader reader = new StreamReader(message, Encoding.Unicode);
txtExtractMsgText.Text = reader.ReadToEnd();
reader.Close();
}
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 :-)