Skip to content

ImageSteg

Katatunga edited this page Feb 9, 2021 · 9 revisions

Diese Klasse (nebst Unterklassen) dient zur steganographischen Verschlüsselung von Nachrichten in verlustfreien Bildformaten (PNG, BMP, GIF). Nachrichten werden bei der Enkodierung als Bytearray überreicht und bei der Dekodierung als solches zurückgegeben, ihr Inhalt spielt keine Rolle.

Unterstützte Formate

Die Klasse erkennt das Format des überreichten Bildes und gibt das Bild nach erfolgter Bearbeitung im selben Format als Bytearray zurück. Je nach Format wird aus zwei unterschiedlichen Algorithmen zur Verschlüsselung gewählt:

Format Bittiefe Algorithmus
PNG 24 - 32 Bit RandomLSB
BMP (intransparent) 24 - 32 Bit RandomLSB
GIF 8 Bit (indexed) ColorCouples

Andere Formate werden nicht unterstützt. BMP mit transparenten Pixeln werden nicht unterstützt (siehe Erklärungen).

Verwendete Algorithmen

Die verwendeten Algorithmen wurden im Rahmen dieses Projektes entwickelt. Die Erklärungen zu ihrer Funktionsweise und die Implementationen finden sich unter den folgenden Links:

Funktionsweise Implementation
RandomLSB PixelBit
ColorCouples PixelIndex

Verwendung des Seeds

Die Methoden decode(...) und encode(...) können jeweils mit oder ohne Übergabe eines Seeds verwendet werden. Der Seed beeinflusst die pseudorandomisierte Auswahl der für die Verschlüsselung verwendeten Pixel (siehe Erklärungen). Sein Wert (oder seine Nicht-Verwendung) muss also bei En- und Dekodierung übereinstimmen und kann daher als eine Art Passwort gesehen werden. Wird kein Seed explizit angegeben, verwendet ImageSteg stattdessen einen (festen) Standard-Seed.

Konstruktor

Bei Erstellung eines Objektes der Klasse ImageSteg können dem Konstruktor zwei Optionen (Boolean) übergeben werden, deren Bedeutung nachfolgend erklärt wird. Die Nutzung des default-Konstruktors ImageSteg() entspricht ImageSteg(useDefaultHeader = true, useTransparent = false).

boolean useDefaultHeader

kurz:

  • true: der versteckten Nachricht wird ein (nicht zusätzlich verschlüsselter) Header hinzugefügt, die Methoden decode(...) und isSteganographicData(...) können bei der Entschlüsselung verwendet werden
  • false: die Nachricht wird unverändert verschlüsselt, bei der Dekodierung muss decodeRaw(...) verwendet werden. isSteganographicData(...) gibt immer false zurück.

Die versteckte Nachricht kann zusätzlich mit einem Header versehen werden, der die Identifikation dieses Projekts und die Länge der versteckten Nachricht beinhaltet. Um die Methoden decode(...) und isSteganographicData(...) bei der Dekodierung nutzen zu können, muss diese Option auf true gesetzt werden.

Die Inklusion eines Headers vermindert jedoch die Sicherheit der versteckten Nachricht. Sollte ein Angreifer den verwendeten Algorithmus kennen, muss er "lediglich" alle möglichen seeds (Datentyp long) durchgehen. Sobald er den Header lesen kann, ist auch der Rest der Nachricht korrekt extrahiert worden, selbst wenn die Nachricht zusätzlich verschlüsselt wurde.

Um dies zu vermeiden, kann auf den Header verzichtet werden. Zur Dekodierung muss dann allerdings die Methode decodeRaw(...) verwendet werden und die Länge der Nachricht (in Bytes) bekannt sein. Die Methode isSteganographicData(...) gibt bei einem so bearbeiteten Bild immer false zurück.

boolean useTransparent:

kurz:

  • betrifft nur PNG
  • true: vollständig transparente Pixel werden zur Verschlüsselung benutzt
  • false: vollständig transparente Pixel werden nicht zur Verschlüsselung benutzt

Die o.g. Formate können vollständig durchsichtige (transparente) Pixel enthalten. Sollten auch in diesen Informationen verschlüsselt werden, wird die Kapazität des Bildes zur Aufnahme von Informationen zwar erhöht, die Verschlüsselung an sich ist dadurch jedoch auffälliger. Diese Option betrifft nur PNG. Bei einem GIF wird sie ignoriert, da sich nachfolgende Frames in animierten GIFs auf die Transparenz ihrer Vorgänger verlassen. BMP mit transparenten Pixeln werden nicht unterstützt (siehe Erklärungen).

Diese Option verändert die Auswahl der durch den Seed bestimmten Pixel. Ihr Wert muss bei En- und Dekodierung also übereinstimmen. Die Verwendung teilweise transparenter Pixel wird nicht beeinflusst.

Code-Beispiele ImageSteg

Nutzung des Default Konstruktors (mit Default Header, ohne Nutzung vollständig transparenter Pixel)

// Variables
// -------------------------------------------------
byte[] message2Hide = "Hello World".getBytes();
byte[] imageFileAsByteArray = Files.readAllBytes(new File("/path/to/anImage.png").toPath());
Steganography steganography = new ImageSteg();
long seed = 1234L;  // optional, can be used to influence how the message is hidden in the image

// Encode
// -------------------------------------------------
byte[] steganographicImage = steganography.encode(imageFileAsByteArray, message2Hide);
// or byte[] steganographicImage = steganography.encode(imageFileAsByteArray, message2Hide, seed); // to use a custom seed

// Decode
// -------------------------------------------------
byte[] hiddenMessage = steganography.decode(steganographicImage);
// or byte[] hiddenMessage = steganography.decode(steganographicImage, seed); // if a custom seed was used

Nutzung ohne Default Header (aber mit transparenten Pixeln)

// Variables
// -------------------------------------------------
byte[] message2Hide = "Hello World".getBytes();
byte[] imageFileAsByteArray = Files.readAllBytes(new File("/path/to/anImage.png").toPath());
Steganography steganography = new ImageSteg(false, true);
long seed = 1234L;  // optional, can be used to influence how the message is hidden in the image

// Encode
// -------------------------------------------------
byte[] steganographicImage = steganography.encode(imageFileAsByteArray, message2Hide);
// or byte[] steganographicImage = steganography.encode(imageFileAsByteArray, message2Hide, seed); // to use a custom seed

// Decode
// -------------------------------------------------
int length = message2Hide.length; // must be known by decoding party
byte[] hiddenMessage = steganography.decodeRaw(length, steganographicImage);
// or byte[] hiddenMessage = steganography.decode(length, steganographicImage, seed); // if a custom seed was used

Erklärungen

Keine Unterstützung von BMP mit transparenten Pixeln

In diesem Projekt wird die native Java-Klasse "java.awt.image.BufferedImage" (BufferedImage) genutzt, um Nachrichten in Bildern zu verstecken. Das Einlesen und Schreiben von Bildern realisiert die Klasse "javax.imageio.ImageIO" und nutzt dabei sog. ImageReader (Reader) und ImageWriter (Writer).

Während nativ vorhandene Reader ein BMP mit transparenten Pixeln zwar einlesen können, existiert kein nativer Writer, der aus einem BufferedImage ein BMP mit transparenten Pixeln schreiben könnte. Daher werden diese nicht unterstützt.

Auswahl der Pixel

Die Auswahl der Pixel geschieht separat von ihrer Enkodierung über sog. Overlays. Diese lösen die Pixel aus ihren x-y-Koordinaten und repräsentieren sie als sequenzielle Folge, ähnlich einem Iterator. Diese Struktur ermöglicht die unkomplizierte Erweiterung oder Veränderung der Pixelreihenfolge und -auswahl, ohne dass der enkodierende Algorithmus verändert werden muss.

Da eine Verschlüsselung in Pixeln mit sequenzieller Reihenfolge (z.B. zeilenweise von oben nach unten) das Auffinden einer versteckten Nachricht relativ einfach machen würde, werden die Pixel unter Verwendung eines Seeds pseudorandomisiert ausgewählt. Dies sorgt für eine recht gleichmäßige Verteilung veränderter Pixel im Bild.

Daraus entstehen zwei Vorteile:

  • Da ein Pixel jeweils nur ein Bit repräsentiert und Inhalt sowie Format der Nachricht unbekannt sind, ist es beinahe unmöglich, die korrekte Reihenfolge ohne Kenntnis des Seeds zu bestimmen
  • Statt an einer bestimmten Stelle im Bild viele Pixelveränderungen vorzunehmen, werden diese über das gesamte Bild verteilt. Dadurch wird es deutlich schwieriger, überhaupt zu erkennen, dass in dem vorliegenden Bild eine Nachricht versteckt ist

Vereinfachtes Sequenzdiagramm für die Methode ImageSteg.encode(byte[] image, byte[] payload)

Sequenz Diagramm für den Aufruf der Methode ImageSteg.encode(image, payload)

Beteiligte Interfaces / Klassen:

Erweiterungen

Ausschluss von Pixeln in (großen) Farbflächen

Eine mögliche Erweiterung dieses Moduls wäre der Ausschluss von Pixeln (größerer) gleichfarbiger Flächen. Hierfür müsste ein Overlay erschaffen werden, dass solche Flächen erkennt und Pixel daraus nicht anbietet. Der jeweilige Algorithmus kann dabei unverändert bleiben.

Nachteile:

  • Da weniger Pixel genutzt werden, sinkt die Kapazität des Bildes
  • Der Prozess der Flächenidentifikation ist relativ aufwändig
  • Die zusätzliche Option würde das Interface oder den Konstruktor verkomplizieren

Vorteil:
Die steganographrische Bearbeitung wäre deutlich unauffälliger. Ohne diese Erweiterung können bestimmte steganalytische Verfahren dafür sorgen, dass einzelne Pixel erkannt werden können, die eine leicht andere Farbe haben als ihre direkten Nachbarn. Dadurch ist man zwar noch weit von der Entschlüsselung der versteckten Nachricht entfernt, ihre Anwesenheit würde jedoch bestätigt. Sollten solche Farbunterschiede jedoch nur in Randbereichen von Flächen auftreten, in denen ein gewisser Farbverlauf ohnehin zu erwarten ist, wird es bedeutend schwerer, dies zu ermitteln.