In der vernetzten Welt spielt die Sicherheit von Daten eine entscheidende Rolle. Verschlüsselungstechniken sind unerlässlich, um die Vertraulichkeit von Informationen zu gewährleisten, sei es bei der Kommunikation zwischen Geräten im Internet oder beim Schutz sensibler Daten. Eine Verschlüsselung wandelt Klartext in eine unleserliche Form um, die nur von autorisierten Personen mit dem passenden Schlüssel wieder entschlüsselt werden kann. In diesem Beitrag zeigen wir, wie man die AES-128-Verschlüsselung im CBC-Modus auf dem ESP8266 implementieren kann, um Daten effektiv und sicher zu verschlüsseln.
Zum Kapitel springen Verschlüsselung mit AES-128 CBC
AES steht für Advanced Encryption Standard und ist einer der weltweit am häufigsten verwendeten Verschlüsselungsstandards. AES-128 bezieht sich dabei auf die Schlüsselgröße die für den Verschlüsselungsprozess verwendet wird - 128 Bit.
Der CBC-Modus - Cipher Block Chaining - ist ein Verschlüsselungsmodus, der sowohl einen Schlüssel als auch einen Initialisierungsvektor kurz IV verwendet. Der Schlüssel sorgt für die eigentliche Verschlüsselung, während der IV sicherstellt, dass gleiche Klartexte unterschiedliche verschlüsselte Ergebnisse erzeugen können.
Jeder zu verschlüsselnde Datenblock hängt von allen vorherigen Blöcken ab, was die Sicherheit erhöht. Da der CBC-Modus blockweise arbeitet und die Blöcke voneinander abhängig sind, muss immer der gesamte Datenblock vorliegen, bevor er verschlüsselt werden kann. Dies führt zu der Einschränkung, dass der CBC-Modus nicht für die Verschlüsselung von Streaming-Daten geeignet ist. Für Anwendungen, die eine Echtzeitverschlüsselung erfordern, wie eben bei einer Verschlüsselung von Streams, wären andere Verfahren wie der CFB-Modus - Cipher Feedback Mode - besser geeignet, da sie Zeichen für Zeichen verschlüsseln können.
Wenn wir den Text "quick brown fox jumps over the lazy dog"
mit einem AES-128 CBC-Algorithmus verschlüsseln, erhalten wir eine scheinbar zufällige Zeichenkette, die ohne den passenden Schlüssel und Initialisierungsvektor nicht entschlüsselt werden kann:
ue6Js6Fg0pBbwm2lF8XgLAlNeCTs58A5rfVXGqg9e6SLGZeOHzpCLt11Wiu+bPlu
In unserer ersten Implementierung verwenden wir einen statischen IV, was bedeutet, dass gleiche Klartexte auch gleiche verschlüsselte Ausgaben produzieren. Dies ist für bestimmte Anwendungen ausreichend und vereinfacht die Handhabung, erfordert jedoch besondere Sorgfalt bei der Verwaltung des IVs. In der zweiten Implementierung arbeiten wir mit einem dynamischen IV um unterschiedliche Ergebnisse bei ein und dem selben Klartext zu erhalten.
Zum Kapitel springen Installation der Bibliothek
Unsere AES-128 CBC-Implementierung für den ESP8266 ist aktuell nicht in der Arduino IDE Bibliotheksverwaltung verfügbar und muss daher manuell hinzugefügt werden. Dies lässt sich mit ein paar Schritten aber sehr leicht manuell erledigen.
Produktempfehlungen und -suche in Verbindung mit dem Amazon Partnerprogramm:
¹ Angaben ohne Gewähr. Bei einem Kauf über den Link erhalten wir eine Provision.
-
Lade dir zuerst die Bibliothek von meinem GitHub-Repository herunter.
-
Entpacke danach die .zip-Datei
-
Kopiere den entpackten Ordner in den entsprechenden Bibliothek-Ordner der Arduino IDE:
- Windows:
C:\Users\Benutzername\Documents\Arduino\libraries
- macOS:
~/Documents/Arduino/libraries
- Linux:
~/Arduino/libraries
- Windows:
-
Starte die Arduino IDE neu, damit die Bibliothek erkannt wird
Die erfolgreiche Installation kann überprüft werden, indem man die Beispiele der Arduino IDE aufruft. Dort sollte nun ein neues Beispiel aus meiner Library verfügar sein:
Arduino IDE
└── Datei
└── Beispiele
└── esp8266-aes-128-cbc
└── esp8266-aes-128-cbc
Zum Kapitel springen Ein einfaches Verschlüsselungsbeispiel
Der folgende Code zeigt, wie wir mit der Bibliothek einen Text verschlüsseln und anschließend wieder entschlüsseln können:
#include "AESCrypto.h"
// Key und Initialisierungsvecot als Strings
// 16 Zeichen lang
String aes_key_str = "73secret!JO?&2%n"; // 16 Zeichen
String aes_iv_str = "vectorImmaBossYA"; // 16 Zeichen
AESCrypto aesCrypto(aes_key_str, aes_iv_str);
void setup() {
Serial.begin(115200);
// Daten verschlüsseln
String encdata = aesCrypto.encrypt("quick brown fox jumps over the lazy dog");
Serial.println("encrypted:");
Serial.println(encdata);
// Daten entschlüsseln
String decdata = aesCrypto.decrypt(encdata);
Serial.println("decrypted:");
Serial.println(decdata);
}
void loop() {
// Keine Wiederholungen im Loop
}
Der Schlüssel (aes_key_str
) ist eine 16-stellige Zeichenkette, welche für die Verschlüsselung und Entschlüsselung verwendet wird. Er sollte geheim gehalten werden, da er der zentrale Sicherheitsmechanismus der Verschlüsselung ist.
Der Initialisierungsvektor (aes_iv_str
) ist eine 16-stellige Zeichenkette, die zur korrekten Initialisierung des CBC-Modus benötigt wird. Er sorgt dafür, dass gleiche Klartexte unterschiedliche verschlüsselte Ausgaben erzeugen, wenn er bei jedem Verschlüsselungsvorgang dynamisch generiert wird.
encrypted:
ue6Js6Fg0pBbwm2lF8XgLAlNeCTs58A5rfVXGqg9e6SLGZeOHzpCLt11Wiu+bPlu
decrypted:
quick brown fox jumps over the lazy dog
Dieser Code kann sowohl für den Sender- als auch für den Empfänger ESP8266 verwendet werden. Möchte man allerdings architekturübergreifend mit den Daten arbeiten, so benötigt man noch ein passenden Gegenstück in einer anderen Programmiersprache. Hier habe ich ein Code-Beispiel für Javascript innerhalb von Node.JS vorbereitet, welches direkt mit dem Codesnippet kompatibel ist. Es kommt ganz externe Bibliotheken aus.
Produktempfehlungen und -suche in Verbindung mit dem Amazon Partnerprogramm:
¹ Angaben ohne Gewähr. Bei einem Kauf über den Link erhalten wir eine Provision.
const crypto = require('crypto');
// 16-character KEY & IV
const aes_key_str = "73secret!JO?&2%n";
const aes_iv_str = "vectorImmaBossYA";
const cipher_key = Buffer.from(aes_key_str, 'utf8');
const cipher_iv = Buffer.from(aes_iv_str, 'utf8');
function encrypt(data){
const cipher = crypto.createCipheriv('aes-128-cbc', cipher_key, cipher_iv);
let crypted = cipher.update(data, 'utf-8', 'base64');
crypted += cipher.final('base64');
return crypted;
}
function decrypt(data){
const decipher = crypto.createDecipheriv('aes-128-cbc', cipher_key, cipher_iv);
let dec = decipher.update(data, 'base64', 'utf-8');
dec += decipher.final();
return dec;
}
// Example Usage
// let encryptedData = encrypt("quick brown fox jumps over the lazy dog");
// let decryptedData = decrypt("ue6Js6Fg0pBbwm2lF8XgLAlNeCTs58A5rfVXGqg9e6SLGZeOHzpCLt11Wiu+bPlu");
Zum Kapitel springen Sicherheit und Praxisrelevanz
AES-128 gilt als sicherer Algorithmus, der auch heute noch weit verbreitet ist. In Kombination mit dem CBC-Modus bietet er eine starke Verschlüsselung, die für viele Anwendungen ausreichend ist. Allerdings sollte man sich bewusst sein, dass die Sicherheit auch stark von der Geheimhaltung des Schlüssels und IVs abhängt. Werden diese kompromittiert, ist die Sicherheit der gesamten Kommunikation gefährdet.
Auf dem ESP8266, einem Mikrocontroller mit begrenzten Ressourcen, bietet AES-128 CBC eine gute Balance zwischen Sicherheit und Leistung. Für einfache IoT-Anwendungen ist diese Art der Verschlüsselung praxistauglich. Bei Anwendungen, die höhere Sicherheitsanforderungen haben, sollten jedoch weitere Maßnahmen, wie z.B. die regelmäßige Änderung von Schlüsseln, in Betracht gezogen werden.
Vorteile:
- Sicherheit: AES-128 CBC bietet eine robuste Verschlüsselung, die für viele Anwendungen mehr als ausreichend ist.
- Performance: Auf dem ESP8266 ist die Implementierung effizient genug, um in Echtzeitanwendungen eingesetzt zu werden.
- Einfachheit: Mit unserer
AESCrypto
-Bibliothek lässt sich die Verschlüsselung leicht implementieren.
Einschränkungen:
- Ressourcenverbrauch: Auf einem ESP8266, der nur begrenzte Ressourcen hat, kann die Verschlüsselung von großen Datenmengen zu Performanceproblemen führen.
- Erhöhte Payload-Größe: Durch die Verschlüsselung wird die Größe der übertragenen Daten erhöht. Bei Anwendungen mit strikten Größenbeschränkungen, wie ESP-NOW, sollte dies berücksichtigt werden. Verwende gerne unseren Byte-Counter um die Größe deines Payloads berechnen zu lassen
- Schlüsselmanagement: Die sichere Verwaltung von Schlüsseln und IVs bleibt eine Herausforderung und sollte nicht unterschätzt werden.
Zum Kapitel springen Dynamischer IV: Ein gängiges Verfahren für erhöhte Sicherheit
Zum Abschluss dieses Beitrags möchten wir noch auf den Einsatz eines dynamischen Initialisierungsvektors eingehen, was in vielen Anwendungsfällen eine gängige und empfohlene Praxis ist, um die Sicherheit der Verschlüsselung zu erhöhen. Ein dynamischer IV wird bei jeder Verschlüsselung neu generiert und zusammen mit den verschlüsselten Daten gespeichert oder übertragen. Dies stellt sicher, dass selbst identische Klartexte bei jedem Verschlüsselungsvorgang unterschiedliche Ausgaben erzeugen:
encrypted (IV + data): M6SGwPPSJzMD7Nhf1Htn0YnBX6I968u36mGNeOZQsZNkAFnLXdLuKQSxGab3zOwPv7wa55nWXSU0gBGB
decrypted: quick brown fox jumps over the lazy dog
encrypted (IV + data): fqTCgsdGaW7JaJ5JjX/jemkfvD5UWZmgAUBFTGYTcrmlVGLWT6HFtxtyG4rDcGE+7Bvzm/gy+6FRiTEf
decrypted: quick brown fox jumps over the lazy dog
encrypted (IV + data): L3bwmeXU7sS4mkVgMoLt5BbH5mJ4+YHMU+y8lelqtNaE70q0zo7t6HUEjekk4AWOGz2A9N6c4qA8gNXR
decrypted: quick brown fox jumps over the lazy dog
Da der IV bei jedem Verschlüsselungsvorgang anders ist, entstehen dadurch keine wiederkehrenden Muster in den verschlüsselten Daten, was die Anfälligkeit für bestimmte Arten von Angriffen verringert. Dies ist besonders wichtig, wenn ähnliche oder identische Daten mehrmals verschlüsselt werden müssen. Im folgenden Beispielcode wird der IV dynamisch generiert und kann zusammen mit den verschlüsselten Daten gesendet werden. Vor dem Entschlüsseln wird der IV anhand seiner Länge von 16 Bytes extrahiert.
#include "AESCrypto.h"
// Funktion um einen zufälligen IV zu generieren
String generateRandomIV() {
String iv = "";
for (int i = 0; i < 16; i++) {
iv += char(random(32, 127)); // Generiert zufällige ASCII Zeichen
}
return iv;
}
// Key als String; 16 Zeichen lang
String aes_key_str = "73secret!JO?&2%n"; // 16 Zeichen
void setup() {
Serial.begin(115200);
/////
///// Hier findet die Verschlüsselung statt
/////
// Generiert zufälligen IV für jede Verschlüsselung
String aes_iv_str = generateRandomIV();
AESCrypto aesCrypto(aes_key_str, aes_iv_str);
// Daten verschlüsseln mit dem zufälligen IV
String plaintext = "quick brown fox jumps over the lazy dog";
String encdata = aesCrypto.encrypt(plaintext);
// Zufälligen IV vor die verschlüsselten Daten anhängen
String combined_data = aes_iv_str + encdata;
Serial.print("encrypted (IV + data):");
Serial.println(combined_data);
/////
///// Hier findet die Entschlüsselung statt
/////
// IV aus den verschlüsselten Daten extrahieren
String aes_iv_str_2 = combined_data.substring(0, 16);
String encdata_2 = combined_data.substring(16);
AESCrypto aesCrypto2(aes_key_str, aes_iv_str_2);
// Daten entschlüsseln mit dem extrahierten IV
String decdata = aesCrypto2.decrypt(encdata_2);
Serial.print("decrypted:");
Serial.println(decdata);
}
void loop() {
// Keine Wiederholungen im Loop
}
Auch hier habe ich wieder ein passenden Gegenstück geschrieben in Javascript, welches nahtlos mit dem Code kompatibel ist.
const crypto = require('crypto');
// 16-stelliger Schlüssel
const aes_key_str = "73secret!JO?&2%n";
const cipher_key = Buffer.from(aes_key_str, 'utf8');
// Funktion um einen zufälligen 16-Byte-IV zu generieren
function generateRandomIV() {
let iv = '';
for (let i = 0; i < 16; i++) {
iv += String.fromCharCode(Math.floor(Math.random() * (126 - 32 + 1)) + 32); // Generiert zufällige ASCII Zeichen
}
return iv;
}
function encrypt(data) {
// Generiere einen zufälligen 16-Byte-IV mit Base64-kompatiblen Zeichen
const iv = generateRandomIV();
const aes_iv = Buffer.from(iv, 'utf8');
// Erstelle den Verschlüsselungsalgorithmus mit dem generierten IV
const cipher = crypto.createCipheriv('aes-128-cbc', cipher_key, aes_iv);
// Verschlüssele die Daten
let crypted = cipher.update(data, 'utf8', 'base64');
crypted += cipher.final('base64');
// Rückgabe: IV (als String) vor den verschlüsselten Daten
return iv + crypted;
}
function decrypt(data) {
// Extrahiere den IV (die ersten 16 Zeichen) und die verschlüsselten Daten
const aes_iv_str = data.substring(0, 16);
const aes_iv = Buffer.from(aes_iv_str, 'utf8');
const encrypted_data = data.substring(16);
// Erstelle den Entschlüsselungsalgorithmus mit dem extrahierten IV
const decipher = crypto.createDecipheriv('aes-128-cbc', cipher_key, aes_iv);
// Entschlüssele die Daten
let dec = decipher.update(encrypted_data, 'base64', 'utf8');
dec += decipher.final('utf8');
return dec;
}
// Beispielanwendung
// let encryptedData = encrypt("quick brown fox jumps over the lazy dog");
// console.log("Verschlüsselt:", encryptedData);
// let decryptedData = decrypt("9BerKNNbmZ;uP8k_Y7FTiavYSRIu3Zk5Iv0g7inxG6jO7g8OvI0KoAEGW23s1SnCMBgbmof+1DwjQeXv");
// console.log("Entschlüsselt:", decryptedData);
Mögliche Nachteile und Herausforderungen
- Komplexität in der Datenverarbeitung:
Da der IV bei jedem Verschlüsselungsvorgang neu generiert wird, muss er gemeinsam mit den verschlüsselten Daten zusammen gespeichert oder übertragen werden. Dies kann die Datenverarbeitungskomplexität leicht erhöhen, da zusätzliche Schritte erforderlich sind, um den IV korrekt zu handhaben. - Erhöhter Overhead:
Das Speichern oder Übertragen des IV zusammen mit den verschlüsselten Daten erhöht die Gesamtdatenmenge, was bei stark limitierten Bandbreiten oder Speicherplatz kritisch sein könnte.
In der Praxis ist die Verwendung eines dynamischen IVs eine bewährte Methode, um die Sicherheit von Verschlüsselungslösungen erheblich zu verbessern. Obwohl die Handhabung eines dynamischen IVs zusätzlichen Aufwand bedeuten kann, überwiegen die Sicherheitsvorteile in den meisten Fällen, insbesondere in sicherheitskritischen Anwendungen.