CogniCrypt: Kryptografie richtig nutzen

Beim Verwenden von Kryptografie-APIs lauern viele Fallstricke. Die Open-Source-Software CogniCrypt hilft beim Erstellen kryptografisch sicherer Anwendungen und analysiert bestehenden Quellcode auf Schwachstellen.

In Pocket speichern vorlesen Druckansicht 54 Kommentare lesen
CogniCrypt - Kryptografie richtig nutzen
Lesezeit: 14 Min.
Von
  • Johannes Späth
  • Anna-Katharina Wickert
  • Matthias Becker
  • Stefan Krüger
  • Eric Bodden
Inhaltsverzeichnis

Geräte und digitale Dienste sind inzwischen in allen Lebensbereichen und Branchen vernetzt. Das reicht von digitalen Sprachassistenten über intelligente Kühlschränke bis hin zur Stromversorgung. Die zunehmende Vernetzung erfordert insbesondere für die Geräte und Dienste sichere Software, die Kryptografie nutzt, um eine sichere Kommunikation und Datenspeicherung zu gewährleisten. Daher sind Grundkenntnisse zur Anwendung von Kryptografie in allen Bereichen der Softwareentwicklung wichtig.

In den Nachrichten tauchen dennoch immer wieder Schreckensmeldungen zu neuen Sicherheitsproblemen auf. Studien der letzten Jahre zeigen, dass die Mehrheit der Anwendungen, die kryptografische Komponenten nutzen, diese fehlerhaft benutzen. Die Autoren dieses Artikels haben mit dem von ihnen erstellten Werkzeug CogniCrypt aktuelle Java-Anwendungen und Apps – über 150.000 Softwareartifakte von Maven Central und 10.000 Android-Apps – systematisch analysiert. Das Ergebnis der Analyse ist, dass 95 Prozent der untersuchten Apps und 68 Prozent der getesteten Java-Anwendungen kryptografische Bibliotheken unsicher benutzen.

Diese Zahlen werfen die Frage auf, wieso Entwickler kryptografische Bibliotheken großflächig falsch benutzen. Zur Demonstration typischer Fallstricke soll folgender Code dienen:

public class Encryption {
byte[] salt = new byte[] {13, 12, -94, 0, 46,
3, -65, 73, -1, 7, -35};

private SecretKey generateKey(String password) {
PBEKeySpec pBEKeySpec =
new PBEKeySpec(password.toCharArray(),
salt, 16100, 256);

SecretKeyFactory secretKeyFactory =
SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] keyMaterial =
secretKeyFactory.generateSecret(pBEKeySpec).getEncoded();
//pBEKeySpec.clearPassword();

SecretKey encryptionKey =
new SecretKeySpec(keyMaterial, "AES");
return encryptionKey;
}

private byte[] encrypt(byte[] plainText,
SecretKey encryptionKey) {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, encryptionKey);
return cipher.doFinal(plainText);
}

public byte[] encryptData(byte[] plainText,
String password) {
return encrypt(plainText, generateKey(password));
}
}

Das Beispiel implementiert eine passwortbasierte Verschlüsselung (PBE, Password-Based Encryption) in drei Methoden. generateKey erzeugt auf Basis eines Passworts einen kryptografischen Schlüssel. Dafür definiert sie zunächst ein PBEKeySpec-Objekt, das das Passwort, einen Salt, eine Iterationszahl und die Länge des zu generierenden Schlüssels als Parameter bekommt. Das Objekt dient der Spezifikation der meisten benötigten Schlüsseleigenschaften. Im Anschluss generiert der Code zunächst den Schlüssel durch den Schlüsselableitungsalgorithmus PBKDF2 und legt anschließend AES als Verschlüsselungsalgorithmus fest.

Die zweite Methode encrypt führt die eigentliche Verschlüsselung mit Klartext und dem Schlüssel durch. Als Verschlüsselungsalgorithmus dient erneut AES. Die Methode gibt das Resultat der Verschlüsselung – den Chiffretext – zurück. Die dritte Methode encryptData ermöglicht die Verschlüsselung des Klartextes mit einem Passwort und benutzt die beiden zuvor beschriebenen Methoden.

Durch die vielen für PBE benötigten Zwischenschritte wirkt der Quellcode recht aufgeblasen und umständlich. Unglücklicherweise ist der Code darüber hinaus zwar kompilier- und ausführbar, enthält aber vier Fehler bei der Verwendung der kryptografischen Bibliothek.

Fehler Nummer eins betrifft das genutzte Passwort. Der Konstruktor für PBEKeySpec erwartet das Passwort als char-Array. Das ist wichtig, weil Strings unter der Haube von Java unveränderbar sind: Bei jeder Änderung an einem String erstellt die Java Virtual Machine (JVM) ein neues Objekt. Daher verbleibt jeder String im Hauptspeicher, bis Javas Garbage Collector ihn entfernt. Bei sensiblen Daten ist das allerdings gefährlich. Sie sollten auf keinen Fall länger als absolut notwendig aus dem Speicher auslesbar sein. Im Gegensatz zu Strings lassen sich char-Arrays explizit leeren. Daher sollten Java-Programme Passwörter nie als Strings, sondern immer als char-Array ablegen.

Dass der Code das Passwort nicht direkt nach dem Verwenden aus dem Speicher löscht, ist der zweite Fehler. Die Klasse PBEKeySpec übernimmt das Entfernen nicht automatisch, stattdessen müssen Entwickler explizit PBEKeySpec.clearPassword() aufrufen. Der passende Aufruf ist als Kommentar im Code zu finden. Ohne ihn erfolgt das Löschen erst, wenn beziehungsweise falls der Garbage Collector das PBEKeySpec-Objekt aufräumt.

Der dritte Fehler betrifft den Salt. Salts erhöhen die Entropie von Eingaben in kryptografischen Funktionen. Entwickler sollten den Salt für jede Verwendung zufällig generieren, um Attacken mit Rainbow-Tables zu unterbinden. Statt den Salt zusammen mit dem Hash in der Nutzerdatenbank zu speichern, ist er im obigen Code konstant und lässt sich somit einfach durch Dekompilieren auslesen.

Zu guter Letzt ist die im Code verwendete Verschlüsselung unsicher und damit Fehler Nummer vier. AES ist grundsätzlich als Verschlüsselungsalgorithmus sicher. Da es sich aber um ein symmetrisches Blockverschlüsselungsverfahren handelt, wird der Klartext in Blöcke aufgeteilt und jeder Block einzeln verschlüsselt. Wie das geschieht, bestimmt der sogenannte Betriebsmodus der Verschlüsselung. Der im Beispiel gewählte Electronic Codebook Mode (ECB) beschränkt sich darauf, einzelne Klartextblöcke nacheinander zu verschlüsseln und die Chiffretextblöcke anschließend aneinanderzureihen. Für zwei Blöcke, die im Klartext gleich sind, sind auch die passenden Chiffretextblöcke gleich. Strukturelle Informationen über den Klartext bleiben somit im Ciffrat enthalten.

Die folgende Grafik zeigt schemenhaft die Problematik von ECB:

Das CogniCrypt-Logo (links) einmal mit dem Blockmodus ECB (mittleres Bild) und einmal mit einem sicheren Blockmodus (rechts) verschlüsselt.

Bei der Verschlüsselung des CogniCrypt-Logos mit ECB (mittleres Bild) bleibt die Struktur im Chiffrat zu erkennen. Bei einer Verschlüsselung mit einem sicheren Blockmodus ist keine Struktur des ursprünglichen Klartextes mehr zu erkennen. Daher gilt ECB als unsicher.

Die vier Fehlerquellen im Beispielcode beeinträchtigen die Sicherheit der Software in erheblichem Maße. Das Design der API, die die Spezifikation des Algorithmus und Verschlüsselungsmodus durch mit Schrägstrichen getrennte Strings verlangt, verleitet zu Fehlern. Zusätzlich erfordert die API kryptografisches Grundwissen. Es existiert keine szenariobasierte API mit Aufrufen wie FileEncryptor.encrypt(File). Stattdessen müssen Entwickler Schlüssel explizit erstellen und aktuelle sowie sichere Algorithmen auswählen. Dazu fehlt ihnen häufig das Wissen.