C# Quellcode - 19.5 Kb

Worum geht es?

Dieser Artikel erweitert die Anwendung aus Teil 1 um drei Funktionen:

  • Kennwörter für jeden Schlüssel
  • Mehrere Träger-Bitmaps

Mehrere Schlüssel

Symmetrische Schlüssel bringen ein zentrales Problem mit sich: Wie übermittelt man sie? Jeder, der den Schlüssel in die Finger bekommt, kann jede einer Nachrichten entschlüsseln. Also müssen wir es schwierig machen, den Schlüssel überhaupt zu entwenden. Eine Möglichkeit wäre, die Schlüssel relativ kurz zu halten und gar nicht zu speichern. Aber ein kurzer Schlüssel hinterlässt einen Fußabdruck in der verschlüsselten Datei, in unserem Fall wäre dieser als regelmäßiges Rauschmuster zu sehen.
Warum nicht ein kurzes Kennwort mit einem langen Schüssel kombinieren? Man kann den langen Schlüssel in einer Datei speichern und ihn mit dem kurzen Kennwort verschlüsseln, welches man nur im eigenen Kopf speichert.

Ein bestimmtes Kennwort gehört immer zu seiner Schlüssel-Datei:

public struct FilePasswordPair{
        public String fileName;
        public String password;

        public FilePasswordPair(String fileName, String password){
                this.fileName = fileName;
                this.password = password;
        }
}

Bevor wir die Schlüssel verwenden können, müssen die Schlüssel/Kennwort-Paare kombiniert werden.
In diesem Beispiel enthält die Schlüssel-Datei den Text "hello-world". Wann immer eine Nachricht mit dem "hello-world"-Schlüssel versteckt oder ausgelesen wird, gibt man das Kennwort "nothing" ein. Die Anwendung XOR-kombiniert die Bytes des Schlüssels mit denen des Kennworts, und wiederholt das Kennwort dabei so oft wie nötig.

protecting a key file with a password

Hier ist das gleiche 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 ){
                //Schlüssel-Byte mit entsprechendem Kennwort-Byte kombinieren
                currentByte = currentByte ^ key.password[passwordIndex];

                //Ergebnis zum Schlüssel-Stream hinzufügen
                resultStream.WriteByte((byte)currentByte);

                //Weiter zum nächsten (ggf. zurück zum ersten) Zeichen
                passwordIndex++;
                if(passwordIndex == key.password.Length){
                        passwordIndex = 0;
                }
        }

        fileStream.Close();
        resultStream.Seek(0, SeekOrigin.Begin);
        return resultStream;
}

Der entstandene Stream ist genauso lang wie die Schlüssel-Datei, wird aber niemals irgendwo gespeichert. Natürlich ist diese Verschlüsselung nicht wirklich sicher, darum werden wir mehr Schlüssel und mehr Kennwörter verwenden. Bevor der Empfänger die Nachricht extrahieren kann, muss er alle Schlüssel besitzen und alle Kennwörter wissen. Wenn es jemandem gelingt, einen Schlüssel zu kopieren oder zwei Kennwörter zu erraten, besteht noch kein Grund zu Panik, solange die anderen Schlüssel noch sicher sind.
Das heisst, bevor wir Daten verstecken oder auslesen, werden wir wie oben dargestellt Schlüssel-Streams zusammensetzen, und dann alle Streams zu einem kombinieren:

combining three stream
 

private static MemoryStream GetKeyStream(FilePasswordPair[] keys){
        //Schlüssel und ihre Kennwörter XOR-kombinieren
        MemoryStream[] keyStreams = new MemoryStream[keys.Length];
        for(int n=0; n<keys.Length; n++){
                keyStreams[n] = CreateKeyStream(keys[n]);
        }

        //Leeren Stream initialisieren
        MemoryStream resultKeyStream = new MemoryStream();

        //Länge des längsten Stream finden
        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){
                                        //Stream zuende - Datei schließen
                                        //Der letzte Lauf (n==maxLength) schließt den letzten Stream
                                        keyStreams[streamIndex].Close();
                                        keyStreams[streamIndex] = null;
                                }else{
                                        //Ein Byte in den Ergebnis-Stream kopieren
                                        resultKeyStream.WriteByte( (byte)readByte );
                                }
                        }
                }
        }

        return resultKeyStream;
}

Wie man sieht, müssen wir den Algorithmus nicht weiter ändern. Wir rufen einfach GetKeyStream auf, bevor wir eine Nachricht verstecken/lesen, dann können wir den vollständigen Schlüssel an HideOrExtract() weitergeben.
Danach haben wir ein Bild, das die ganze versteckte Information enthält. Wieder stehen wir vor dem Transfer-Problem.

Die Information über mehrere Bilder verteilen

Wenn Du richtig paranoid bist, wirst Du Deiner Mailbox nicht vertrauen. Natürlich wirst Du auch nicht dem Briefträger vertrauen. Das bedeutet, dass Du es nicht wagst, die Träger-Bitmap in einem Stück zu verschicken.
Das ist kein Problem mehr, denn Du kannst eine Nachricht in mehreren Bitmaps verstecken, als würde man einen Text über mehrere Seiten schreiben. Auf diese Anwendung bezogen heisst es, die Pixel über mehrere Bilder zu verteilen.

multiple sheets of paper multiple images

Du kannst jedes Bild in einer separaten eMail verschicken, in verschiedenen Postfächern hinterlegen, oder auf verschiedenen Festplatten speichern. Die Oberfläche erlaubt es, mehrere Träger-Bitmaps genauso wie Schlüssel-Dateien auszuwählen. Die Auswahl wird in einem Array vom Typ CarrierImage gespeichert.

public struct CarrierImage{
        //Name des "sauberen" Bildes
        public String sourceFileName;
        //Dateiname für das neue Bild
        public String resultFileName;
        //Breite * Höhe
        public long countPixels;
        //Farbiges (false) oder monochromes (true) Rauschen in diesem Bild erzeugen
        public bool useGrayscale;
        //Anzahl der Bytes, die in diesem Bild versteckt werden - wird von HideOrExtract() gesetzt
        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;
        }
}

Größere Bilder können mehr Bytes (in mehr Pixeln) fassen als kleinere Bilder. Diese Beispiel-Anwendung verwendet die einfachste mögliche Verteilung:

//Anzahl der Bytes berechnen, für die dieses Bild verwendet wird
for(int n=0; n<imageFiles.Length; n++){
        //pixels = Anzahl der Pixel im Bild / Anzahl verfügbarer Pixel insgesamt
        float pixels = (float)imageFiles[n].countPixels / (float)countPixels;
        //Das Bild wird (Länge der Nachricht * pixels) Bytes verstecken
        imageFiles[n].messageBytesToHide = (long)Math.Ceiling( (float)messageLength * pixels );
}

Jetzt beginnen wir mit der ersten Träger-Bitmap, laufen durch die Nachricht, verstecken eine bestimmte Anzahl an Bytes, wechseln zur zweiten Träger-Bitmap, und immer so weiter.

//Aktuelle Position im Träger-Bitmap
//Start bei 1, weil (0,0) die Länge der Nachricht enthält
Point pixelPosition = new Point(1,0);

//Anzahl der Bytes, die in diesem Bild bereits versteckt wurden
int countBytesInCurrentImage = 0;

//Index des aktuell verwendeten Bildes
int indexBitmaps = 0;

//Nachricht durchlaufen und jedes Byte verstecken
for(int messageIndex=0; messageIndex<messageLength; messageIndex++){
        //Position des nächsten Pixels berechnen
        //...

        //Weiter zur nächsten Bitmap
        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; }
        }

        //Ein Byte verstecken oder extrahieren
        //...

        countBytesInCurrentImage++;
}

Zum Schluss müssen wir die neuen Bilder speichern. Jedes Bild kann in einem anderen Format gespeichert werden (bmp, tif oder png). Das neue Format hat nichts mit dem Format der Original-Datei zu tun. Das heisst, Du kannst eine BMP-, zwei PNG und eine TIFF-Datei als Träger-Bilder auswählen, und die Ergebnisse in drei TIFF- und einer PNG-Datei speichern.

//...
        for(indexBitmaps=0; indexBitmaps<bitmaps.Length; indexBitmaps++){
                if( ! extract ){ //Extrations-Modus ändert keine Bilder
                        //Entstandenes Bild speichern und Original schließen
                        SaveBitmap( bitmaps[indexBitmaps], imageFiles[indexBitmaps].resultFileName );
                }
        }
//...

private static void SaveBitmap(Bitmap bitmap, String fileName){
        String fileNameLower = fileName.ToLower();

        //Format anhand der Erweiterung bestimmen
        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;
        }

        //Bitmap kopieren
        Image img = new Bitmap(bitmap);

        //Datei schließen
        bitmap.Dispose();
        //Neue Bitmap speichern
        img.Save(fileName, format);
        img.Dispose();
}