Sicherheitsaspekte werden bei der Produktion von Software immer wichtiger. Inzwischen stehen sie gleichberechtigt neben den funktionalen Anforderungen und bestimmen nicht selten die Software-Architektur. Sicherheitsfragen umfassen ein weites Feld, sei es der unberechtigte Zugriff Dritter auf Daten, unbemerkte Änderungen daran oder das Ausführen ungewollter Fremdprogramme.
Um dies zu verhindern, bietet Java in der Security-Bibliothek eine Reihe von Funktionalitäten an. Das reicht von der klassischen Verschlüsselung über digitale Signaturen bis hin zu eingeschränkten Rechten für Java-Programme selbst. Dies ist Teil der normalen Java-Distributionen (Open JDK und Oracle-Java) und in den Paketen und »javax.security«
und »javax.crypto«
zu finden.
Den Aufbau der Pakete gibt die Java Cryptography Architecture (JCA, [1]) vor. Den Kern bildet eine Handvoll Klassen für Sicherheitsaufgaben wie etwa Verschlüsselung oder Signatur. Die Klasseninstanzen erzeugt der Programmierer nicht wie üblich mit »new«
, sondern er ruft sie bei einer Factory-Methode für den gewünschten Algorithmus ab. Dadurch ist pro Aufgabe unabhängig von der Anzahl verfügbarer Algorithmen nur noch eine Klasse erforderlich.
Krypto-Alternative
Diese Flexibilität reicht über die Algorithmen hinaus bis hin zur verwendeten Implementierung. So steht als Alternative zu Oracles Umsetzung unter anderem die umfangreichere und MIT-lizenzierte Ausgabe der Programmierergruppe Legion of the Bouncy Castle [2] zur Verfügung. Der Preis für diese Flexibilität liegt auf der Hand: Anfragen mit falschen Namen bestraft Java mit der Ausnahme »NoSuchAlgorithmException«
. Abbildung 1 zeigt die in der JDK-Version 7 verfügbaren Algorithmen, das zugehörige Programm findet sich in den Beispiel-Quelltexten zu diesem Artikel [3].
Abbildung 1: Schon die normale Java-VM ermöglicht die Auswahl unter 40 verschiedenen Algorithmen und Schlüsselformaten.
Es liegt nahe, sensible Daten durch Verschlüsseln zu schützen. Symmetrische Verfahren wie der Advanced-Encryption-Algorithmus (AES) verwenden dabei zweimal den gleichen Schlüssel: Um die Originaldaten in eine unlesbare Form zu verwandeln und um sie wieder zu entschlüsseln (Abbildung 2).
Abbildung 2: Bei symmetrischen Verfahren kommt zum Ver- und Entschlüsseln der gleiche Schlüssel zur Anwendung.
Geheim!
Den typischen Aufbau für Ver- und Entschlüsselprogramme in Java zeigt Listing 1. Es führt die Operationen über ein »Cipher«
-Objekt durch, das es für den gewünschten Algorithmus über die »getInstance()«
-Methode erzeugt (Zeile 1). Das Beispiel nutzt AES mit einer Schlüssellänge von 128 Bit. Passend zu »Cipher«
erzeugt der »Keygenerator«
einen Schlüssel, entweder – wie im Listing – als zufälligen Einmalschlüssel oder basierend auf einem Passwort. Vor der Verwendung muss der Programmierer das Cipher-Objekt mit dem Schlüssel für die gewünschte Richtung initialisieren (Zeile 8).
Symmetrische Verschlüsselung mit AES
01 Cipher cipher = Cipher.getInstance("AES-128");
02
03 KeyGenerator kgen = KeyGenerator.getInstance("AES-128");
04 kgen.init(128);
05
06 SecretKey skey = kgen.generateKey();
07
08 cipher.init(Cipher.ENCRYPT_MODE, skey);
09
10 String klartext = "Die Botschaft";
11 System.out.println("Klartext " + klartext );
12
13 byte[] zuVerschluesseln =klartext.getBytes();
14 // Die Verschlüesselung
15 byte[] verschluesselt = cipher.doFinal( zuVerschluesseln );
16 // Kodieren für einen sicheren Texttransport
17 String verschluesseltB64 = new String( Base64.encodeBase64( verschluesselt ) );
18 System.out.println( "Verschlüsselt " + verschluesseltB64 );
19
20 // Dekodieren
21 byte[] verschluesselt2 = Base64.decodeBase64( verschluesseltB64.getBytes() );
22
23 // Die Entschlüsselung mit dem gleichen Schlüssel
24 cipher.init(Cipher.DECRYPT_MODE, skey);
25
26 byte[] entschluesselt = cipher.doFinal(verschluesselt2);
27 System.out.println("Entschlüsselt " + new String(entschluesselt) );
Alle kryptographischen Methoden arbeiten auf der Basis von Byte-Arrays, Texte muss ein Programm daher wie in Zeile 13 konvertieren. Zur eigentlichen Verschlüsselung nutzt der Java-Entwickler bei kleinen Mengen direkt die »doFinal()«
-Methode der Cipher-Klasse (Zeile 15). Für größere Mengen bietet sich das Verschlüsseln mit »CipherOutputStream«
beziehungsweise »CipherInputStream«
an. Der durchgeleitete Datenstrom wird dann im Stream mit dem »Cipher«
-Objekt ver- oder entschlüsselt.
Möchte der Entwickler die verschlüsselten Daten wieder als Text übertragen, sind Byte-Arrays sehr unhandlich. Hier hat sich das Kodieren als Base64 bewährt. Die Bytes (-128 bis 127) werden dabei auf die in allen Systemen übertragbaren Buchstaben des englischen Alphabets abgebildet (Zeile 17). Leider enthält Java keinen Base64-Encoder im öffentlichen API, das Beispiel verwendet deshalb die Base64-Klasse aus der Apache-Commons-Codec-Bibliothek [4].
Die Entschlüsselung durchläuft die Schritte in umgekehrter Reihenfolge: Das Programm dekodiert zunächst Base64 in ein Byte-Array und entschlüsselt dieses per Cipher-Objekt. Das Cipher-Objekt muss der Java-Entwickler für beide Richtungen mit den gleichen Parametern und Schlüsseln erstellen, ansonsten schlägt die Entschlüsselung fehl.