Wave-Dateien

C# Quellcode - 20.8 Kb

Worum geht es?

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.

Das Wave Dateiformat

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.

Die Nachricht verstecken

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

Die Nachricht auslesen

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

Einen Klang aufzeichnen

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);
Follow me on Mastodon