Nachdem wir nun in Bitmaps, MIDI Tracks und .NET Assemblies Daten versteckt haben, vermisst Du vielleicht ein wichtiges Dateiformat. Vielleicht vermisst Du die Dateien, die sehr viele Bytes verstecken können, ohne größer zu werden, und sich in wenigen Sekunden erstellt lassen, so dass man keine Originaldateien auf der Festplatte speichern muss. Es ist an der Zeit, Wave Audio zur Liste hinzuzufügen.
Dieser Artikel verwendet Code aus A full-duplex audio player in C# using the waveIn/waveOut APIs.
Hast Du schon einmal eine Wave Date im HEX Editor angeschaut? Sie beginnt so, und geht mit unlesbaren Binärdaten weiter:
Jede RIFF Datei beginnt mit dem Text "RIFF", gefolgt von der
Int32
Länge der gesamten Datei:
Die nächsten Felder sagen, dass diese RIFF Datei Wave-Daten enthält, und öffnen den Format-Chunk:
Die Länge des folgenden Format-Chunks muss für PCM Dateien 16 sein:
Jetzt wird mit einer WAVEFORMATEX
Struktur das Format angegeben:
Nach dem Format-Chunk können noch Extra-Informationen stehen.
Der interessante Teil beginnt mit dem data
Chunk.
Der Daten-Chunk enthält alle Wave Samples. Dass heißt, der Rest der Datei besteht aus reinen Audio-Daten. Kleine Änderungen können eventuell hörbar sein, aber nicht die Datei zerstören.
Eine Nachricht in Wave Samples zu verstecken funktioniert fast genauso wie in den Pixeln einer Bitmap. Wieder verwenden wir einen Schlüssel-Stream, um eine Anzahl von Trägereinheiten (Samples/Pixel) zu überspringen, greifen eine Trägereinheit heraus, setzen ein Bit der Nachricht in ihr niedrigstes Bit, und schreiben die geänderte Einheit in den Ziel-Stream. Nachdem die ganze Nachricht so versteckt wurde, kopieren wir den Rest des Träger-Streams.
public void Hide(Stream messageStream, Stream keyStream){ byte[] waveBuffer = new byte[bytesPerSample]; byte message, bit, waveByte; int messageBuffer; //receives the next byte of the message or -1 int keyByte; //distance of the next carrier sample //Schleife über die Nachricht, jedes Byte verstecken while( (messageBuffer=messageStream.ReadByte()) >= 0 ){ //read one byte of the message stream message = (byte)messageBuffer; //für jedes Bit in [message] for(int bitIndex=0; bitIndex<8; bitIndex++){ //ein Byte vom Schlüssel-Stream lesen keyByte = GetKeyValue(keyStream); //[keyByte] Samples überspringen for(int n=0; n<keyByte-1; n++){ //ein Sample aus dem sauberen Stream in den Träger-Stream kopieren sourceStream.Copy( waveBuffer, 0, waveBuffer.Length, destinationStream); } //ein Sample aus dem Wave-Stream lesen sourceStream.Read(waveBuffer, 0, waveBuffer.Length); waveByte = waveBuffer[bytesPerSample-1]; //nächstes Bit des aktuellen Nachrichten-Bytes holen... bit = (byte)(((message & (byte)(1 << bitIndex)) > 0) ? 1 : 0); //...und ins letzte Bit des Samples schreiben if((bit == 1) && ((waveByte % 2) == 0)){ waveByte += 1; }else if((bit == 0) && ((waveByte % 2) == 1)){ waveByte -= 1; } waveBuffer[bytesPerSample-1] = waveByte; //Ergebnis in den Ziel-Stream schreiben destinationStream.Write(waveBuffer, 0, bytesPerSample); } } //Rest der Wave unverändert kopieren //... }
Wieder verwenden wir den Schlüssel-Stream, um die richtigen Samples zu finden, genauso wie vorher beim Verstecken der Nachricht. Dann lesen wir das letzte Bit des jeweiligen Samples und schieben es ins aktuelle Byte der Nachricht. Wenn das Byte vollständig ist, schreiben wir es in den Nachrichten-Stream, und machen mit dem Nächsten weiter.
public void Extract(Stream messageStream, Stream keyStream){
byte[] waveBuffer = new byte[bytesPerSample];
byte message, bit, waveByte;
int messageLength = 0; //expected length of the message
int keyByte; //distance of the next carrier sample
while( (messageLength==0 || messageStream.Length<messageLength) ){
//Nachrichten-Byte zurücksetzen
message = 0;
//für jedes Bit in [message]
for(int bitIndex=0; bitIndex<8; bitIndex++){
//ein Byte vom Schlüssel-Stream lesen
keyByte = GetKeyValue(keyStream);
//[keyByte] Samples auslassen
for(int n=0; n<keyByte; n++){
//ein Sample aus dem Wave-Stream lesen
sourceStream.Read(waveBuffer, 0, waveBuffer.Length);
}
waveByte = waveBuffer[bytesPerSample-1];
//letztes Bit des Samples holen...
bit = (byte)(((waveByte % 2) == 0) ? 0 : 1);
//...und ins Nachrichten-Byte schreiben
message += (byte)(bit << bitIndex);
}
//rekonstruiertes Byte zur Nachricht hinzufügen
messageStream.WriteByte(message);
if(messageLength==0 && messageStream.Length==4){
//die ersten 4 Bytes enthalten die Länge der Nachricht
//...
}
}
}
Die originalen, sauberen Trägerdateien aufzuheben, kann gefährlich sein. Jemand der eine Trägerdatei mit geheimer Nachricht darin hat, und es schafft, die Original-Datei ohne Nachricht zu bekommen, kann einfach die beiden Dateien vergleichen, die Abstände zwischen jeweils zwei unterschiedlichen Samples zählen, und so schnell den Schlüssel rekonstruieren.
Deshalb müssen wir die sauberen Trägerdateien löschen und zerstören nachdem sie einmal verwendet wurden, oder einen Sound on the fly aufzeichnen. Dank Ianier Munoz’ WaveInRecorder ist es kein Problem, Wave-Daten aufzunehmen und eine Nachricht darin zu verstecken, bevor irgendetwas auf der Festplatte gespeichert wird. Es gibt keine Original-Datei, um die wir uns Sorgen machen müssten. Im Hauptformular kann der Benutzer wählen, ob er eine vorhandene Wave-Datei verwenden, oder hier und jetzt einen Sound aufzeichnen möchte. Wenn er einen einzigartigen, nicht reproduzierbaren Sound aufnehmen will, kann er ein Mikrofon anschließen und sprechen/spielen/… was immer ihm einfällt:
if(rdoSrcFile.Checked){ //eine .wav Datei als Träger verwenden //beschwer dich nachher nicht, du wurdest schließlich gewarnt sourceStream = new FileStream(txtSrcFile.Text, FileMode.Open); }else{ //einen Träger-Klang aufzeichnen frmRecorder recorder = new frmRecorder(countSamplesRequired); recorder.ShowDialog(this); sourceStream = recorder.RecordedStream; }
frmRecorder
ist eine kleine Oberfläche für den WaveIn Recorder,
die die aufgenommenen Samples mitzählt und einen Stop-Button aktiviert, sobald der
Sound lang genug ist um die angegebene Nachricht zu verstecken.
Der neue Sound wird in einem MemoryStream
abgelegt und an
WaveUtility
weitergereicht. Von da an ist es egal, wo der Stream her kam,
WaveUtility
macht keinen Unterschied zwischen aus Dateien gelesenen und on the fly
aufgezeichneten Klängen.
WaveUtility utility = new WaveUtility(sourceStream, destinationStream); utility.Hide(messageStream, keyStream);