Stellen Sie sich vor, Sie möchten einem Freund eine geheime Nachricht schicken, aber der Kanal, den Sie verwenden möchten, ist kompromittiert und wird überwacht. Sie könnten eine Verschlüsselung verwenden, aber das würde den Verdacht der Leute erregen, die Ihre Gespräche überwachen, also müssten Sie etwas anderes verwenden.
Heutzutage ist Steganografie eine Methode, um geheime Nachrichten in einer anderen, nicht geheimen Datei (z. B. einem Katzenbild) zu verstecken, sodass diese Datei, wenn Sie sie senden, nicht erkannt wird. Steganografie beschränkt sich nicht nur auf das Verstecken von Text in Bildern, sondern bedeutet im Allgemeinen „das Verstecken geheimer Informationen in einer anderen, nicht geheimen Nachricht oder einem physischen Objekt“: Sie können einige Nachrichten in Audio-, Video- oder anderen Texten verstecken, indem Sie beispielsweise eine Spaltentransposition verwenden.
Auch in vielen anderen Fällen kann die Steganografie äußerst nützlich sein. Sie kann beispielsweise eine gute Alternative zum Wasserzeichen in vertraulichen Dokumenten sein, um diese vor dem Verlust der Kenntnis der Öffentlichkeit zu schützen.
Es gibt viele Möglichkeiten, Informationen in Bildern zu verbergen, vom einfachen Anhängen des Textes an das Ende der Datei bis hin zum Verstecken in den Metadaten. In diesem Artikel möchte ich eine fortgeschrittenere Methode der Steganografie behandeln, die bis auf die binäre Ebene geht und Nachrichten innerhalb der Grenzen des Bildes selbst versteckt.
Für mein Beispiel zur Steganographie habe ich mich für JavaScript entschieden, da es eine leistungsstarke Programmiersprache ist, die in einem Browser ausgeführt werden kann.
Ich habe eine einfache Schnittstelle entwickelt, die es Benutzern ermöglicht, ein Bild hochzuladen, um die darin versteckte Nachricht zu lesen oder selbst eine Nachricht in einem Bild zu verschlüsseln.
<html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Steganography</title> <style> body { font-family: Arial, sans-serif; text-align: center; margin-top: 50px; } textarea { width: 300px; height: 100px; } button { margin: 10px; } #outputImage { margin-top: 20px; max-width: 100%; } </style> </head> <body> <h1>Steganography Example</h1> <input type="file" id="upload" accept="image/*"><br> <canvas id="canvas" style="display:none;"></canvas><br> <textarea id="text" placeholder="Enter text to encode"></textarea><br> <button id="encode">Encode Text</button> <button id="decode">Decode Text</button> <p id="decodedText"></p> <img id="outputImage" alt="Output Image"> <script src="./script.js"></script> </body> </html>
Um es zu verwenden, können Benutzer einfach ein Bild auswählen, das sie bearbeiten möchten, und entweder versuchen, einen Text daraus zu dekodieren, oder es kodieren und das Bild später herunterladen.
Um mit Bildern in JavaScript zu arbeiten, können wir die Canvas-API verwenden. Sie bietet viele verschiedene Funktionen zum Bearbeiten und Zeichnen von Bildern, Animationen oder sogar Spielegrafiken und Videos.
Die Canvas-API wird hauptsächlich für 2D-Grafiken verwendet. Wenn Sie mit 3D-Grafiken mit Hardwarebeschleunigung arbeiten möchten, können Sie die WebGL-API verwenden (die übrigens auch das <canvas>-Element verwendet).
const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); const image = new Image();
Um die Bilddatei aus dem Dateisystem zu lesen und sie dem Canvas-Kontext hinzuzufügen, können wir die FileReader-API verwenden. Sie ermöglicht es uns, den Inhalt jeder auf dem Computer des Benutzers gespeicherten Datei problemlos zu lesen, ohne dass eine benutzerdefinierte Bibliothek erforderlich ist.
function handleFileUpload(event) { const reader = new FileReader(); reader.onload = function (e) { image.src = e.target.result; image.onload = function () { canvas.width = image.width; canvas.height = image.height; ctx.drawImage(image, 0, 0); }; }; reader.readAsDataURL(event.target.files[0]); }
Es liest eine Datei und zeichnet das Bild in dieser Datei in unseren zuvor definierten 2D-Canvas-Kontext. Danach können wir entweder Text in dieses Bild kodieren oder versuchen, den Text aus dem Bild zu lesen.
Bilder bestehen aus Pixeln und jedes Pixel enthält Informationen über seine Farben. Wenn ein Bild beispielsweise mit dem RGBA-Modell codiert wird, enthält jedes Pixel 4 Bytes an Informationen darüber, wie viel Rot, Grün, Blau und Alpha (Deckkraft) es darstellt.
Um Text in einem Bild zu kodieren, könnten wir einen dieser Kanäle verwenden (zum Beispiel den Alphakanal). Da diese Informationen im Binärsystem dargestellt werden (wie 01001100), könnten wir das letzte Bit auf das ändern, was wir brauchen. Es wird als Least Significant Bit (LSB) bezeichnet und seine Änderung bewirkt nur minimale Änderungen am Bild selbst, sodass es von einem Menschen nicht mehr zu unterscheiden ist.
Stellen Sie sich nun vor, wir haben einen Text wie „Hallo“ und möchten ihn in ein Bild kodieren. Der Algorithmus hierfür wäre
Wandeln Sie den „Hallo“-Text in Binärcode um.
Iterieren Sie durch die Bytes der Bilddaten und ersetzen Sie das LSB dieser Bytes durch ein Bit aus dem Binärtext (jedes Pixel enthält 4 Bytes Daten für jede der Farben. In meinem Beispiel möchte ich den Opazitätskanal des Bildes ändern, daher würde ich jedes 4. Byte iterieren).
Fügen Sie am Ende der Nachricht ein Nullbyte hinzu, damit wir beim Dekodieren wissen, wann wir aufhören müssen.
Wenden Sie geänderte Bildbytes auf das Bild selbst an.
Zuerst müssen wir den Text, den wir kodieren möchten, vom Benutzer nehmen und einige grundlegende Validierungen daran durchführen.
const text = document.getElementById("text").value; if (!text) { alert("Please enter some text to encode."); return; }
Dann müssen wir den Text in Binärcode umwandeln und eine Leinwand des Bildes erstellen, in das wir diesen Text kodieren möchten.
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imgData.data; let binaryText = ""; for (let i = 0; i < text.length; i++) { let binaryChar = text.charCodeAt(i).toString(2).padStart(8, "0"); binaryText += binaryChar; }
Dazu können wir einfach über jedes Zeichen iterieren und mithilfe der Funktion charCodeAt
einen Unicode-Index abrufen. Dieser Unicode wird dann in Binärzahlen umgewandelt und aufgefüllt, sodass er dieselbe Länge wie jedes andere Zeichen hat.
Beispielsweise wird der Buchstabe „H“ in Unicode als 72 dargestellt. Wir konvertieren diese Zahl dann in eine Binärzahl (1001000) und fügen am Anfang Nullen hinzu (01001000), um sicherzustellen, dass alle Buchstaben dieselbe Länge (8 Bit) haben.
Dann müssen wir am Ende der Nachricht ein Null-Byte hinzufügen, um sicherzustellen, dass wir beim Entschlüsseln zwischen dem echten Text und den zufälligen Pixeldaten des Bildes unterscheiden können.
binaryText += "00000000";
Anschließend müssen wir einige grundlegende Überprüfungen durchführen, um sicherzustellen, dass das Bild über genügend Pixel zum Kodieren unserer Nachricht verfügt, sodass es nicht zu einem Überlauf kommt.
if (binaryText.length > data.length / 4) { alert("Text is too long to encode in this image."); return; }
Und dann kommt der interessanteste Teil, die Kodierung der Nachricht. Das Datenarray, das wir zuvor definiert haben, enthält Pixelinformationen in Form von RGBA-Werten für jedes Pixel im Bild. Wenn das Bild also RGBA-kodiert ist, wird jedes Pixel darin durch 4 Werte des Datenarrays dargestellt; jeder Wert stellt dar, wie viel Rot, Grün und Blau dieses Pixel hat.
for (let i = 0; i < binaryText.length; i++) { data[i * 4] = (data[i * 4] & 0b11111110) | parseInt(binaryText[i]); } ctx.putImageData(imgData, 0, 0); const outputImage = document.getElementById("outputImage"); outputImage.src = canvas.toDataURL();
Im obigen Code iterieren wir über unseren binär codierten Text. data[i * 4]
findet ein Byte, das wir ändern müssen, und da wir nur die Bytes eines bestimmten Kanals ändern möchten, multiplizieren wir die Variable i
mit 4
um darauf zuzugreifen.
Die Operation data[i * 4] & 0b11111110
setzt das niedrigstwertige Bit auf 0
. Wenn data[i * 4]
beispielsweise binär 10101101
ist, dann ergibt die Operation 10101101 & 11111110
10101100
. Dadurch wird sichergestellt, dass das niedrigstwertige Bit auf 0
gesetzt ist, bevor wir weitere Manipulationen damit vornehmen.
Das parseInt(binaryText[i])
ist ein aktuelles Bit aus der binär codierten Zeichenfolge; es ist entweder 1
oder 0
. Wir können dieses Bit dann mit einer bitweisen ODER-Operation ( |
) auf das LSB setzen. Wenn beispielsweise der linke Teil des bitweisen ODER 10101100
ist und binaryText[i]
1
ist, dann würde 10101100 | 00000001
10101101
ergeben. Wenn das aktuelle Bit 0
wäre, würde das ODER 10101100
ergeben. Aus diesem Grund mussten wir zunächst das LSB löschen.
Sobald die Nachricht codiert ist, können wir sie in die aktuelle Leinwand einfügen und mit der Methode canvas.toDataURL
in HTML rendern.
Der Prozess der Dekodierung eines Bildes ist eigentlich viel einfacher als die Kodierung. Da wir bereits wissen, dass wir nur den Alphakanal kodiert haben, können wir einfach jedes vierte Byte durchlaufen, das letzte Bit lesen, es zu unserem endgültigen String verketten und diese Daten von binär in einen Unicode-String umwandeln.
Zuerst müssen wir die Variablen initialisieren. Da imgData
bereits mit den Bildinformationen gefüllt ist (wir rufen ctx.drawImage
jedes Mal auf, wenn wir eine Datei aus dem Dateisystem lesen), können wir sie einfach in die Datenvariable extrahieren.
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imgData.data; let binaryText = ""; let decodedText = "";
Dann müssen wir über jedes 4. Byte des Bildes iterieren, das letzte Bit lesen und es mit der Variable binaryText
verketten.
for (let i = 0; i < data.length; i += 4) { binaryText += (data[i] & 1).toString(); }
data[i]
ist das codierte Byte, und um das LSB zu extrahieren, können wir den bitweisen AND-Operator ( &
) verwenden. Er nimmt zwei Werte und führt eine AND-Operation an jedem Paar entsprechender Bits durch. Indem wir data[i]
mit 1
vergleichen, isolieren wir im Grunde das niederwertigste Bit aus den Pixelinformationen, und wenn das LSB 1
ist, dann ist das Ergebnis einer solchen Operation 1
Wenn das LSB 0
ist, wäre das Ergebnis ebenfalls 0
.
Nachdem wir alle LSBs gelesen und in der Variable binaryText
gespeichert haben, müssen wir sie von Binärtext in Klartext umwandeln. Da wir wissen, dass jedes Zeichen aus 8 Bits besteht (erinnern Sie sich, wie wir padStart(8, "0")
verwendet haben, um allen Zeichen die gleiche Länge zu geben?), können wir jedes 8. Zeichen der binaryText
durchlaufen.
Dann können wir die Operation .slice()
verwenden, um das aktuelle Byte aus dem binaryText
basierend auf unserer Iteration zu extrahieren. Die Binärzeichenfolge kann mit der Funktion parseInt(byte, 2)
in eine Zahl umgewandelt werden. Dann können wir prüfen, ob das Ergebnis 0
(ein Nullbyte) ist – wir stoppen die Umwandlung und fragen das Ergebnis ab. Andernfalls können wir herausfinden, welches Zeichen der Unicode-Zahl entspricht, und es unserer Ergebniszeichenfolge hinzufügen.
for (let i = 0; i < binaryText.length; i += 8) { let byte = binaryText.slice(i, i + 8); if (byte.length < 8) break; // Stop if the byte is incomplete let charCode = parseInt(byte, 2); if (charCode === 0) break; // Stop if we hit a null character decodedText += String.fromCharCode(charCode); }
Der dekodierte Text kann dann sicher einem Benutzer angezeigt werden:
document.getElementById("decodedText").textContent = decodedText;
Ich habe den vollständigen Code, der in diesem Artikel verwendet wurde, in meinem GitHub-Repository gelassen. Sie können gerne damit herumspielen. Es gibt viele Dinge, die verbessert werden könnten :)
Steganographie ist eine sehr leistungsfähige Technik und kann in vielen verschiedenen Anwendungsfällen eingesetzt werden, angefangen bei der Dokumentenüberprüfung, der Verhinderung von Datenlecks, der KI-Bildüberprüfung, der DRM-Verwaltung von Musikdateien und vielem mehr. Diese Technik kann sogar auf Videos, Spiele oder sogar Rohtext angewendet werden, daher denke ich, dass sie ein enormes Potenzial hat.
Im Zeitalter von NFTs und Blockchains ist es noch interessanter zu sehen, welche Anwendungsfälle es geben wird und wie sich diese Technik weiterentwickeln wird.