Introduction
This article enhances the last article Steganography - Hiding messages in the Noise of a Picture. It adds three important features to the application:
- Multiple key files
- Password protection for each key file
- Multiple carrier images
Background
Before reading this, you should have read part one, Steganography - Hiding messages in the Noise of a Picture. This article uses the application described in part one, and adds functionality to spread the message over multiple carrier images, use more than one key file, and protect each key with a password.
Multiple keys
Symmetric keys contain a central problem: how do you transfer the key? Anyone who gets his hands on the key is able to decrypt every message you send or receive. So we have to make it difficult to steal the key. One idea might be keeping the keys short and don�t save them at all. But a short key always leaves a footprint in the encrypted file. In our case, it wouldn't be visible as a regular noise pattern. Why not combine a short password with a long key? You can store a long key in a file, and encrypt that key using a short password which is stored only in your brain.
A password always belongs to its key file, just like that:
public struct FilePasswordPair{
public String fileName;
public String password;
public FilePasswordPair(String fileName, String password){
this.fileName = fileName;
this.password = password;
}
}
Before we can use the keys, the file/password pairs have to be combined. In this example, the key file contains the text "hello-world". Whenever you hide or extract a message with the "hello-world"-key, you type the password "nothing". The application XOR-combines the bytes of the key file with the bytes of the password, repeating the password again and again.
Here is the same thing in C#:
public static MemoryStream CreateKeyStream(FilePasswordPair key){
FileStream fileStream = new FileStream(key.fileName, FileMode.Open);
MemoryStream resultStream = new MemoryStream();
int passwordIndex = 0;
int currentByte = 0;
while( (currentByte = fileStream.ReadByte()) >= 0 ){
currentByte = currentByte ^ key.password[passwordIndex];
resultStream.WriteByte((byte)currentByte);
passwordIndex++;
if(passwordIndex == key.password.Length){
passwordIndex = 0;
}
}
fileStream.Close();
resultStream.Seek(0, SeekOrigin.Begin);
return resultStream;
}
The resulting stream is as long as your key file, but never stored anywhere. Of course, this encryption is not really safe, so we�ll use more keys and more passwords. Before the recipient is able to extract the message, he must have all the keys and know all the passwords. If somebody manages to copy one key or guess two passwords, there's no need to panic as long as the other key files are still safe. That means, before hiding or extracting data, we create each key stream as shown above, and then combine all streams into one key stream:
private static MemoryStream GetKeyStream(FilePasswordPair[] keys){
MemoryStream[] keyStreams = new MemoryStream[keys.Length];
for(int n=0; n<keys.Length; n++){
keyStreams[n] = CreateKeyStream(keys[n]);
}
MemoryStream resultKeyStream = new MemoryStream();
long maxLength = 0;
foreach(MemoryStream stream in keyStreams){
if( stream.Length > maxLength ){ maxLength = stream.Length; }
}
int readByte = 0;
for(long n=0; n<=maxLength; n++){
for(int streamIndex=0; streamIndex<keyStreams.Length; streamIndex++){
if(keyStreams[streamIndex] != null){
readByte = keyStreams[streamIndex].ReadByte();
if(readByte < 0){
keyStreams[streamIndex].Close();
keyStreams[streamIndex] = null;
}else{
resultKeyStream.WriteByte( (byte)readByte );
}
}
}
}
return resultKeyStream;
}
As you can see, we don�t have to change the algorithm at all. We just call GetKeyStream
before hiding or extracting the message, then we can pass the complete key stream to HideOrExtract()
. After that, we have one picture containing all hidden information. Again, we are facing the problem of transfer.
Spread the information over the images
If you're really paranoid, you won�t trust your mailbox. Of course, you won�t trust the postman either. That means, you don�t dare sending the carrier bitmap in one piece. That is no problem anymore, because you can hide one message in many bitmaps. It is quite similar to writing text across a couple of pages. According to this application, it means spreading the pixels over multiple images.
You can send each image in a separate E-mail, post them in different mailboxes, or store them on different discs. The GUI allows selecting carrier bitmaps the same way as selecting key files. The selection is stored as an array of CarrierImage
s.
public struct CarrierImage{
public String sourceFileName;
public String resultFileName;
public long countPixels;
public bool useGrayscale;
public long messageBytesToHide;
public CarrierImage(String sourceFileName,
String resultFileName, long countPixels,
bool useGrayscale)
{
this.sourceFileName = sourceFileName;
this.resultFileName = resultFileName;
this.countPixels = countPixels;
this.useGrayscale = useGrayscale;
this.messageBytesToHide = 0;
}
}
Larger images can hide more bytes (more pixels) than smaller images. This application uses the most simple distribution:
for(int n=0; n<imageFiles.Length; n++){
float pixels = (float)imageFiles[n].countPixels / (float)countPixels;
imageFiles[n].messageBytesToHide =
(long)Math.Ceiling( (float)messageLength * pixels );
}
Now, we start with the first carrier bitmap, loop over the message, hide a number of bytes, switch to the second carrier bitmap, and so on.
Point pixelPosition = new Point(1,0);
int countBytesInCurrentImage = 0;
int indexBitmaps = 0;
for(int messageIndex=0; messageIndex<messageLength; messageIndex++){
if(countBytesInCurrentImage ==
imageFiles[indexBitmaps].messageBytesToHide)
{
indexBitmaps++;
pixelPosition.Y = 0;
countBytesInCurrentImage = 0;
bitmapWidth = bitmaps[indexBitmaps].Width-1;
bitmapHeight = bitmaps[indexBitmaps].Height-1;
if(pixelPosition.X > bitmapWidth){ pixelPosition.X = 0; }
}
countBytesInCurrentImage++;
}
In the end, we must save the new images. Each image can be saved using a different format (BMP, TIFF or PNG). The new format has nothing to do with the format of the original image file. That means, you can select a BMP, two PNG and a TIFF file as carrier images, and save the results into three TIFF and one PNG file.
for(indexBitmaps=0; indexBitmaps<bitmaps.Length; indexBitmaps++){
if( ! extract ){
SaveBitmap( bitmaps[indexBitmaps],
imageFiles[indexBitmaps].resultFileName );
}
}
private static void SaveBitmap(Bitmap bitmap, String fileName){
String fileNameLower = fileName.ToLower();
System.Drawing.Imaging.ImageFormat format =
System.Drawing.Imaging.ImageFormat.Bmp;
if((fileNameLower.EndsWith("tif"))||
(fileNameLower.EndsWith("tiff")))
{
format = System.Drawing.Imaging.ImageFormat.Tiff;
}
else if(fileNameLower.EndsWith("png"))
{
format = System.Drawing.Imaging.ImageFormat.Png;
}
Image img = new Bitmap(bitmap);
bitmap.Dispose();
img.Save(fileName, format);
img.Dispose();
}
Lazy users
Somebody asked me for a compiled application. So, if (for whatever reasons) you do not want to mess with the code, you can now play with the binary, available with the downloads.