Click here to Skip to main content
15,895,817 members
Articles / Programming Languages / C#

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 72.2K   1.7K   28  
Merging the pieces into one application.
/* This class has been written by
 * Corinna John (Hannover, Germany)
 * picturekey@binary-universe.net
 * 
 * You can use the code in any context you like,
 * as long as you do not delete this comment.
 * 
 * Please send me a little feedback about what you're
 * using this code for and what changes you'd like to
 * see in later versions. (And please excuse my bad english)
 * 
 * WARNING: This is experimental code.
 * Some bugs and flaws have been left in there,
 * to keep the code readable to people who want
 * to understand the algorithm.
 * Please do not expect "Release Quality".
 * */

using System;
using System.IO;

namespace SteganoDemo
{
	/// <summary>Hides/extracts data in/from a wave stream</summary>
	public class WaveUtility : FileUtility
	{
		/// <summary>Format of the source file</summary>
		private WaveFormat carrierFormat;
		
		/// <summary>Position of the "data" chunk in the source file</summary>
		private long dataPosition;
		
		/// <summary>
		/// The read-only stream.
		/// Clean wave for hiding,
		/// Carrier wave for extracting
		/// </summary>
		private Stream sourceStream;
		
		/// <summary>Stream to receive the edited carrier wave</summary>
		private Stream destinationStream;
		
		/// <summary>bits per sample / 8</summary>
		private int bytesPerSample;

		public override void Initialize(CarrierFile carrierFile){
			base.Initialize(carrierFile);
			ReadHeader();
		}

		public override long CountUnits(){
			return carrierFile.CountCarrierUnits;
		}

		private string ReadChunk(BinaryReader reader) {
			byte[] ch = new byte[4];
			reader.Read(ch, 0, ch.Length);
			return System.Text.Encoding.ASCII.GetString(ch);
		}

		/// <summary>ReadChunk(reader) - Changed to CopyChunk(reader, writer)</summary>
		/// <param name="reader">source stream</param>
		/// <returns>four characters</returns>
		private string CopyChunk(BinaryReader reader, BinaryWriter writer) {
			byte[] ch = new byte[4];
			reader.Read(ch, 0, ch.Length);
			
			//copy the chunk
			writer.Write(ch);
			
			return System.Text.Encoding.ASCII.GetString(ch);
		}


		private void ReadHeader() {
			Stream sourceStream;
			if(carrierFile.SourceStream == null){
				sourceStream = new FileStream(carrierFile.SourceFileName, FileMode.Open, FileAccess.Read);
			}else{
				sourceStream = carrierFile.SourceStream;
				carrierFile.SourceStream.Seek(0, SeekOrigin.Begin);
			}
			BinaryReader reader = new BinaryReader(sourceStream);
			
			if (ReadChunk(reader) != "RIFF")
				throw new Exception("Invalid file format");

			reader.ReadInt32(); // File length minus first 8 bytes of RIFF description, we don't use it

			if (ReadChunk(reader) != "WAVE")
				throw new Exception("Invalid file format");

			if (ReadChunk(reader) != "fmt ")
				throw new Exception("Invalid file format");

			int len = reader.ReadInt32();
			if (len < 16) // bad format chunk length
				throw new Exception("Invalid file format");

			carrierFormat = new WaveFormat(22050, 16, 2); // initialize to any format
			carrierFormat.wFormatTag = reader.ReadInt16();
			carrierFormat.nChannels = reader.ReadInt16();
			carrierFormat.nSamplesPerSec = reader.ReadInt32();
			carrierFormat.nAvgBytesPerSec = reader.ReadInt32();
			carrierFormat.nBlockAlign = reader.ReadInt16();
			carrierFormat.wBitsPerSample = reader.ReadInt16(); 

			// advance in the stream to skip the wave format block 
			len -= 16; // minimum format size
			while (len > 0) {
				reader.ReadByte();
				len--;
			}

			// assume the data chunk is aligned
			String chunk;
			do{
				chunk = ReadChunk(reader);
			}while(sourceStream.Position < sourceStream.Length && chunk != "data");
			
			if (sourceStream.Position >= sourceStream.Length)
				throw new Exception("Invalid file format");

			//read length of the data chunk
			//carrierFile.CountCarrierUnits = reader.ReadInt32() / carrierFormat.wBitsPerSample / 8;
			int dataLength = reader.ReadInt32();
			carrierFile.CountCarrierUnits = dataLength / (carrierFormat.wBitsPerSample / 8);
			
			this.dataPosition = sourceStream.Position;
			this.bytesPerSample = carrierFormat.wBitsPerSample / 8;
		}

		/// <summary>Copy the header from [sourceStream] to [destinationStream]</summary>
		private void CopyHeader() {
			BinaryReader reader = new BinaryReader(sourceStream);
			BinaryWriter writer = new BinaryWriter(destinationStream);
			byte[] buffer = new byte[16];
			
			//copy "RIFF"+length+"WAVEfmt "
			reader.Read(buffer, 0, 16);
			writer.Write(buffer);

			Int32 len = reader.ReadInt32();
			writer.Write(len);
			
			//copy format
			reader.Read(buffer, 0, 16);
			writer.Write(buffer);
			
			// copy wave format block 
			len -= 16; // minimum format size
			writer.Write( reader.ReadBytes(len) );
			
			// assume the data chunk is aligned
			while(sourceStream.Position < sourceStream.Length && CopyChunk(reader, writer) != "data")
				;


			//copy length of data
			writer.Write( reader.ReadInt32() );
		}

		/// <summary>Write a new header and add wave data</summary>
		public static Stream CreateStream(WaveFormat format, Stream waveData) {
			MemoryStream stream = new MemoryStream();
			BinaryWriter writer = new BinaryWriter(stream);

			//write RIFF header
			writer.Write(System.Text.Encoding.ASCII.GetBytes("RIFF".ToCharArray()));
			writer.Write((Int32)(waveData.Length + 36)); //File length minus first 8 bytes of RIFF description
			writer.Write(System.Text.Encoding.ASCII.GetBytes("WAVEfmt ".ToCharArray()));
			writer.Write((Int32)16); //length of following chunk: 16

			//write format
			writer.Write((Int16)format.wFormatTag);
			writer.Write((Int16)format.nChannels);
			writer.Write((Int32)format.nSamplesPerSec);
			writer.Write((Int32)format.nAvgBytesPerSec);
			writer.Write((Int16)format.nBlockAlign);
			writer.Write((Int16)format.wBitsPerSample);

			writer.Write(System.Text.Encoding.ASCII.GetBytes("data".ToCharArray()));
			
			//write wave data
			writer.Write((Int32)waveData.Length);

			waveData.Seek(0, SeekOrigin.Begin);
			byte[] b = new byte[waveData.Length];
			waveData.Read(b, 0, (int)waveData.Length);
			writer.Write(b);

			writer.Seek(0, SeekOrigin.Begin);
			return stream;
		}

		private void CopyBitsToSample(int bitsPerUnit, byte messageByte, ref int messageBitIndex, ref byte waveByte){
			for(int carrierBitIndex=0; carrierBitIndex<bitsPerUnit; carrierBitIndex++){
				SetBit(messageBitIndex, messageByte, carrierBitIndex, ref waveByte);
				messageBitIndex++;
			}
		}

		/// <summary>
		/// Hide [messageStream] in [sourceStream],
		/// write the result to [destinationStream]
		/// </summary>
		/// <param name="messageStream">The message to hide</param>
		/// <param name="keyStream">
		/// A key stream that specifies how many samples shall be
		/// left clean between two changed samples
		/// </param>
		public override void Hide(Stream messageStream, Stream keyStream){
			byte[] waveBuffer = new byte[bytesPerSample];
			byte[] skipBuffer;
			byte message;
			int messageBuffer; //receives the next byte of the message or -1
			int keyByte; //distance of the next carrier sample
			int bitsPerUnit; //how many bits to hide in each sample

			//carrierFile.CountBitsToHidePerCarrierUnit has not been hidden yet
			bool isCountBitsPerUnitHidden = false;

			
			if(carrierFile.SourceStream == null){
				//copy source file
				FileStream sourceFileStream = new FileStream(carrierFile.SourceFileName, FileMode.Open, FileAccess.Read);
				byte[] buffer = new byte[sourceFileStream.Length];
				sourceFileStream.Read(buffer, 0, buffer.Length);
				sourceStream = new MemoryStream(buffer);
				sourceFileStream.Close();
			}else{
				sourceStream = carrierFile.SourceStream;
				carrierFile.SourceStream.Seek(0, SeekOrigin.Begin);
			}

			//open destination file
			destinationStream = new FileStream(carrierFile.DestinationFileName, FileMode.Create, FileAccess.Write);
			CopyHeader();


			if(carrierFile.NoisePercent > 0){ //add nonsense
				long dataPosition = sourceStream.Position;
				
				Random random = new Random();
				byte[] noiseByte = new byte[1];
				int maxIndexSamples = (int)((sourceStream.Length / bytesPerSample)-1);
				int countNoisySamples = (int)(maxIndexSamples * ((float)carrierFile.NoisePercent/100));
				int samplePostition;
				for(int n=0; n<countNoisySamples; n++){
					//get clean sample
					samplePostition = random.Next(0, maxIndexSamples) * bytesPerSample;
					sourceStream.Seek(samplePostition, SeekOrigin.Begin);
					sourceStream.Read(waveBuffer, 0, bytesPerSample);

					//add noise to the sample
					random.NextBytes(noiseByte);
					for(int messageBitIndex=0; messageBitIndex<8; ){
						CopyBitsToSample(carrierFile.CountBitsToHidePerCarrierUnit,
							noiseByte[0],
							ref messageBitIndex,
							ref waveBuffer[bytesPerSample-1]);

						sourceStream.Seek(samplePostition, SeekOrigin.Begin);
						sourceStream.Write(waveBuffer, 0, bytesPerSample);
					}
				}

				sourceStream.Seek(dataPosition,SeekOrigin.Begin);
			}

			// ----------------------------------------- Hide the Message

			long streamLength = messageStream.Length;
			while(messageStream.Position < streamLength){
				
				if(isCountBitsPerUnitHidden){
					//read one byte of the message stream
					messageBuffer = messageStream.ReadByte();
					message = (byte)messageBuffer;
					bitsPerUnit = carrierFile.CountBitsToHidePerCarrierUnit;
				}else{
					message = (byte)carrierFile.CountBitsToHidePerCarrierUnit;
					//use default bitcount to hide the correct bitcount
					bitsPerUnit = new WaveFileType().MinBitsPerCarrierUnit;
				}
				
				//for each bit in message
				for(int messageBitIndex=0; messageBitIndex<8; ){
					
					//read a byte from the key
					keyByte = GetKey(keyStream);
					
					//skip a couple of samples
					skipBuffer = new byte[keyByte * bytesPerSample];
					sourceStream.Read(skipBuffer, 0, skipBuffer.Length);
					destinationStream.Write(skipBuffer, 0, skipBuffer.Length);
					
					//read one sample from the wave stream
					sourceStream.Read(waveBuffer, 0, waveBuffer.Length);
					
					CopyBitsToSample(bitsPerUnit, message, ref messageBitIndex, ref waveBuffer[bytesPerSample-1]);
					
					//write the result to destinationStream
					destinationStream.Write(waveBuffer, 0, bytesPerSample);
				}

				isCountBitsPerUnitHidden = true;
			}

			//copy the rest of the wave without changes
			waveBuffer = new byte[sourceStream.Length - sourceStream.Position];
			sourceStream.Read(waveBuffer, 0, waveBuffer.Length);
			destinationStream.Write(waveBuffer, 0, waveBuffer.Length);

			sourceStream.Close();
			destinationStream.Close();
		}

		/// <summary>Extract a message from [sourceStream] into [messageStream]</summary>
		/// <param name="messageStream">Empty stream to receive the extracted message</param>
		/// <param name="keyStream">
		/// A key stream that specifies how many samples shall be
		/// skipped between two carrier samples
		/// </param>
		public override void Extract(Stream messageStream, Stream keyStream){
			byte[] waveBuffer = new byte[bytesPerSample];
			byte message, waveByte;
			int keyByte; //distance of the next carrier sample
			int bitsPerUnit; //how many bits are hidden in each sample

			//carrierFile.CountBitsToHidePerCarrierUnit has not been read yet
			bool isCountBitsPerUnitExtracted = false;

			sourceStream = new FileStream(carrierFile.SourceFileName, FileMode.Open, FileAccess.Read);
			sourceStream.Seek(dataPosition, SeekOrigin.Begin);
			
			//for(int n=0; (carrierFile.CountBytesToHide==0 || n<=carrierFile.CountBytesToHide); n++){
			long count = 0; //carrierFile.CountBytesToHide;
			for(; (count>=0 || carrierFile.CountBytesToHide==0); count--){
				message = 0; //clear the message-byte
				
				if(isCountBitsPerUnitExtracted){
					bitsPerUnit = carrierFile.CountBitsToHidePerCarrierUnit;
				}else{
					//use default bitcount to get the correct bitcount
					bitsPerUnit = new WaveFileType().MinBitsPerCarrierUnit;
				}

				//for each bit in message
				for(int messageBitIndex=0; messageBitIndex<8; ){ //messageBitIndex++){

					//read a byte from the key
					keyByte = GetKey(keyStream);
					
					//skip a couple of samples
					for(int skippedSamples=0; skippedSamples<keyByte; skippedSamples++){
						//read one sample from the wave stream
						sourceStream.Read(waveBuffer, 0, waveBuffer.Length);
					}
					
					//read one sample from the wave stream
					sourceStream.Read(waveBuffer, 0, waveBuffer.Length);
					
					waveByte = waveBuffer[bytesPerSample-1];
					
					for(int carrierBitIndex=0; carrierBitIndex<bitsPerUnit; carrierBitIndex++){
						AddBit(messageBitIndex, ref message, carrierBitIndex, waveByte);
						messageBitIndex++;
					}

				}

				if(! isCountBitsPerUnitExtracted){
					carrierFile.CountBitsToHidePerCarrierUnit = message;
					isCountBitsPerUnitExtracted = true;
				}else{
					//add the re-constructed byte to the message
					messageStream.WriteByte(message);
					if(carrierFile.CountBytesToHide==0 && messageStream.Length==4){
						//first Int32 contains the message's length
						messageStream.Seek(0, SeekOrigin.Begin);
						carrierFile.CountBytesToHide = new BinaryReader(messageStream).ReadInt32();
						count = carrierFile.CountBytesToHide;
						messageStream.Seek(0, SeekOrigin.Begin);
						messageStream.SetLength(0);
					}
				}

			}

			sourceStream.Close();
		}

		public override FileType GetFileType(){
			return new WaveFileType();
		}
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

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