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.
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:
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.
![]() |
![]() |
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(); }