Kryptographie mit Google Tink
Kryptographie in der eigenen Anwendung nicht selbst neu zu erfinden ist sicherlich den meisten Entwicklern bekannt, warnen schließlich nicht nur die OWASP Top 10 vor diesem immer noch verbreiteten Sicherheitsproblem. Doch trotz "Standard-Kryptographie" bleibt deren Verwendung häufig eine Herausforderung. Mit Google Tink steht nun seit einiger Zeit eine weitere Bibliothek zur Verfügung, die verspricht, die Verwendung kryptographischer Operationen weiter zu vereinfachen.
Trotz zahlreicher Erleichterungen bei der Verwendung von Kryptographie hat man es als Java-Entwickler nicht leicht: Neben üblicherweise noch verständlichen Angaben zu Typ (symmetrisch, asymmetrisch, hybrid), dazu passendem Schlüssel und dessen Schlüssellänge, müssen teilweise noch Modus samt Padding und weitere Parameter festgelegt werden. Nicht zu vergessen die zahlreichen Exceptions, die bei der Initialisierung und Durchführung kryptographischer Operationen auftreten und behandelt werden müssen. Und schließlich die eher operativen Aufgaben wie die sichere Speicherung von Schlüsseln und Keystores und natürlich auch der regelmäßige (!) Schlüsselaustausch.
Eine Ursache hierfür sind die vielfältigen kryptographischen Möglichkeiten in Java und der Verzicht auf sichere Defaults. Als Entwickler ist man dadurch gezwungen, jegliche Konfiguration teils bis ins kleinste Detail selbst vorzunehmen. An einigen Stellen mag das von Vorteil sein, wer allerdings "nur" auf eine sichere Verschlüsselung ohne weitere Vorgaben angewiesen ist, sieht sich mit einer unnötigen Komplexität konfrontiert. Trotz der Verwendung eines an sich sicheren Algorithmus können dann Sicherheitsprobleme auftreten; der sicher aussehende Ciphertext lässt sich vielleicht ohne Kenntnis des Schlüssels wieder entschlüsseln, Daten können ohne Auswirkung auf die digitale Signatur manipuliert werden. Neben der Reduktion der notwendigen Konfiguration muss eine sichere Krypto-Bibliothek deshalb zusätzlich ihre unsichere Verwendung verhindern oder zumindest soweit möglich erschweren.
Sichere Defaults
Neu sind diese Anforderungen wahrlich nicht; an ihrer Lösung wurde in der Vergangenheit bereits häufiger gearbeitet: Etwa mit Apache Shiro [1] oder dem ebenfalls von Google stammenden Keyczar [2]. Vor allem Keyczar enthielt einige interessante Ideen rund um sichere Defaults und ein integriertes Schlüsselmanagement samt Versionierung. Es krankte allerdings an der Verwendung eines eigenen JSON-basierten Schlüsselformats und konnte sich nie wirklich durchsetzen. Die Weiterentwicklung ist daher bereits vor einigen Jahren wieder eingeschlafen.
Der Trend zur Verwendung sicherer Defaults und einer damit in weiten Teilen unnötigen zusätzlichen Konfiguration setzt sich in Tink ungebrochen fort. Seit gut zwei Jahren ist Google Tink [3] als Open-Source-Krypto-Bibliothek verfügbar und wird bereits in verschiedenen Google-Produkten eingesetzt – darunter AdMob, Android Pay und Google Android Search App. Mit der aktuellen Version 1.2 hat diese Bibliothek eine Stabilität erreicht, die ihr inoffizieller Vorgänger Keyczar nie erreicht hat.
Sichere kryptographische Operationen verspricht Google Tink dabei nicht nur durch die Verwendung von sicheren Defaults. Mit einher geht gleichzeitig eine deutliche Reduktion der verfügbaren Algorithmen und deren Optionen auf die derzeit als sicher akzeptierten.
Google Tink – Grundlagen
Die folgenden Beispiele basieren auf der Java-Version von Google Tink und sind vollständig auf GitHub im Projekt "crypto-tink" verfügbar [4]. Neben der in diesem Artikel beschriebenen Java-Variante steht Tink für Android (Java), C++, Objective-C und Go zur Verfügung.
Die Integration in ein Java-Projekt gestaltet sich bei der Verwendung von Maven (alternativ Gradle) entsprechend einfach.
Listing 1:
<dependency>
<groupId>com.google.crypto.tink</groupId>
<artifactId>tink</artifactId>
<version>1.2.1</version>
</dependency>
Damit Tink in einer Anwendung eingesetzt werden kann, muss die gesamte Bibliothek zunächst mit TinkConfig.register(); initialisiert werden. Sofern nur bestimmte Teile der angebotenen Funktionalität benötigt werden, lässt sich etwa mit AeadConfig.register(); nur ein Subset initialisieren. Hierbei kann gleichzeitig die Bibliothek verkleinert werden; die Dateigröße reduziert sich entsprechend.
Kryptographische Operationen können anschließend über die in Tink"Primitive" genannten Interfaces durchgeführt werden [5]. Diese Primitive sind thread-safe und daher zustandslos. Verschiedene Primitive stehen beispielsweise für die symmetrische Verschlüsselung mittels Authenticated Encryption with Associated Data (AEAD) und Message Authentication Codes (MAC) zur Verfügung. Digitale Signaturen und die hybride Verschlüsselung werden ebenfalls unterstützt.
Im Gegensatz zur üblichen Kryptographie mit Java muss sich ein Entwickler bei Google Tink nur um eine einzige mögliche Exception kümmern: GeneralSecurityException wird als Checked Exception von den meisten Methoden geworfen. Im Falle einer Exception verbergen sich hierin dann die Details wie etwa decryption failed.
Symmetrische Verschlüsselung
Zum Einstieg in Google Tink bietet sich die symmetrische Verschlüsselung mit dem AES-Algorithmus an. Wie in allen folgenden Beispielen werden die Keys bzw. Keysets on-the-fly erstellt und existieren damit nur zur Laufzeit. Zusätzlich zur Erstellung im Code können Keys bzw. Keysets mit dem "Tinkey" genannten Kommandozeilentool erstellt und verwaltet werden [6]. Zumindest derzeit muss Tinkey allerdings noch selbst gebaut werden.
Ganz im Stile von Keyczar speichert auch Tink Schlüsselmaterial im JSON-Format und ist damit zum üblichen Java KeyStore im "p12"-Format (bzw. dessen proprietären Vorgänger "jks") inkompatibel. Ein von Tink generiertes Keyset mit einem Schlüssel für den AES-Algorithmus ist in Listing 2 abgebildet.
Listing 2:
{
"primaryKeyId": 2053217586,
"key": [{
"keyData": {
"typeUrl": "type.googleapis.com/google.crypto.tink.AesGcmKey",
"keyMaterialType": "SYMMETRIC",
"value": "GhAleMkvfukTy+ySgGUGYAy1"
},
"outputPrefixType": "TINK",
"keyId": 2053217586,
"status": "ENABLED"
}]
}
Neben dem eigentlichen Schlüssel gehören auch Metadaten zum Keyset. Diese bestehen im Beispiel nur aus der Referenz auf den Primary Key des Keysets. Alle Schlüssel sind im Klartext vorhanden, und werden somit auch im Klartext gespeichert. Dazu dient die Klasse CleartextKeysetHandle und deren write- bzw. read-Methoden. Um hier einem Angreifer das Leben nicht unnötig leicht zu machen, sollte man tunlichst auf die angebotene Integration eines Cloud-KMS (AWS KMS, Google Cloud KMS) oder den Android Keystore zurückgreifen und jegliches Schlüsselmaterial mit einem Masterkey sicher verschlüsseln. Hierfür kommt dann etwa die Klasse AwsKmsClient ins Spiel, mit der sich AWS KMS in die eigene Anwendung integrieren lässt. AWS kümmert sich dann um die sichere Aufbewahrung des Masterkeys.
Ver- und Entschlüsselung gestalten sich anschließend sehr einfach, wie Listing 3 zeigt. Nach der Erstellung des AES-Keysets mit dem AeadKeyTemplate kann die Verschlüsselung über ein KeysetHandle (eine Art Wrapper für Keysets) durchgeführt werden. Neu ist hier lediglich der byte-Array associatedData, der bei der Ver- und Entschlüsselung zum Einsatz kommt und in beiden Fällen identisch sein muss. Man könnte deswegen an einem zusätzlichen Schlüssel denken. Tatsächlich stellt associatedData aber die Authentizität des Senders und die Integrität der übermittelten Daten sicher. Dieser byte-Array ist optional und kann durch null ersetzt werden.
Listing 3:
public static void main(String[] args) {
AeadDemo demo = new AeadDemo();
byte[] associatedData = "my key".getBytes();
byte[] plainText = "Some demo data".getBytes();
try {
KeysetHandle keysetHandle = demo.generateKey();
byte[] cipherText = demo.encrypt(keysetHandle, plainText, associatedData);
byte[] plainText = demo.decrypt(keysetHandle, cipherText, associatedData);
// ...
} catch (GeneralSecurityException ex) {
log.error("Failure during Tink usage", ex);
}
}
private KeysetHandle generateKey() throws GeneralSecurityException {
return KeysetHandle.generateNew(AeadKeyTemplates.AES128_GCM);
}
private byte[] encrypt(KeysetHandle keysetHandle, byte[] plainText, byte[] associatedData) throws GeneralSecurityException {
Aead aead = AeadFactory.getPrimitive(keysetHandle);
return aead.encrypt(plainText, associatedData);
}
private byte[] decrypt(KeysetHandle keysetHandle, byte[] cipherText, byte[] associatedData) throws GeneralSecurityException {
Aead aead = AeadFactory.getPrimitive(keysetHandle);
return aead.decrypt(cipherText, associatedData);
}
Dieses Muster aus KeyTemplate und dazu passender Factory zieht sich durch die gesamte Tink-Bibliothek. Ein Entwickler findet sich damit schnell zurecht, egal ob nun eine symmetrische oder hybride Verschlüsselung verwendet wird.
Hybride Verschlüsselung
Die hybride Verschlüsselung ist der symmetrischen sehr ähnlich, allerdings durch die notwendige Verwendung eines privaten und eines öffentlichen Schlüssels etwas aufwändiger. Hierbei muss zunächst der private Schlüssel mit dem HybridKeyTemplate erstellt werden, bevor aus diesem der öffentliche Schlüssel abgeleitet werden kann (Listing 4).
Listing 4:
private KeysetHandle generatePrivateKey() throws GeneralSecurityException {
return KeysetHandle.generateNew(HybridKeyTemplates.ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM);
}
private KeysetHandle generatePublicKey(KeysetHandle privateKeysetHandle) throws GeneralSecurityException {
return privateKeysetHandle.getPublicKeysetHandle();
}
Der öffentliche Schlüssel dient anschließend zur Verschlüsselung, mit dem privaten Schlüssel kann nur der rechtmäßige Besitzer den Ciphertext wieder entschlüsseln (Listing 5).
Listing 5:
private byte[] encrypt(KeysetHandle publicKeysetHandle, byte[] plainText, byte[] associatedData) throws GeneralSecurityException {
HybridEncrypt hybridEncrypt = HybridEncryptFactory.getPrimitive(publicKeysetHandle);
return hybridEncrypt.encrypt(plainText, associatedData);
}
private byte[] decrypt(KeysetHandle privateKeysetHandle, byte[] cipherText, byte[] associatedData) throws GeneralSecurityException {
HybridDecrypt hybridDecrypt = HybridDecryptFactory.getPrimitive(privateKeysetHandle);
return hybridDecrypt.decrypt(cipherText, associatedData);
}
Digitale Signaturen
Mit digitalen Signaturen hat man in der täglichen Java-Entwicklung vermutlich deutlich seltener zu tun als mit der Ver- und Entschlüsselung von Daten. Gerade deshalb ist es aber auch hier wichtig, möglichst ohne große Fehlermöglichkeiten durch deren Erstellung geführt zu werden. Bei Google Tink ist diese Operation vergleichbar mit der Erstellung einer hybriden Verschlüsselung: privater und öffentlicher Schlüssel werden wie zuvor generiert. Als Algorithmus kommt der "Elliptic Curve Digital Signature Algorithm" (ECDSA) zum Einsatz (Listing 6).
Listing 6:
private KeysetHandle generatePrivateKey() throws GeneralSecurityException {
return KeysetHandle.generateNew(SignatureKeyTemplates.ECDSA_P256);
}
private KeysetHandle generatePublicKey(KeysetHandle privateKeysetHandle) throws GeneralSecurityException {
return privateKeysetHandle.getPublicKeysetHandle();
}
Das Signieren erfolgt anschließend mit dem privaten Schlüssel (Listing 7).
Listing 7:
private byte[] sign(KeysetHandle privateKeysetHandle, byte[] message) throws GeneralSecurityException {
PublicKeySign signer = PublicKeySignFactory.getPrimitive(privateKeysetHandle);
return signer.sign(message);
}
Lediglich die Verifizierung der Signatur weicht etwas vom bisherigen Vorgehen ab, ist aber vergleichbar mit dem ebenfalls von Google Tink unterstützten "Hash-based Message Authentication Code" (HMAC). Anstelle einer Rückgabe eines Boolean wird bei einer ungültigen Signatur eine GeneralSecurityException geworfen, die entsprechend im Programmfluss behandelt werden muss (Listing 8).
Listing 8:
private boolean verify(KeysetHandle publicKeysetHandle, byte[] signature, byte[] message) {
try {
PublicKeyVerify verifier = PublicKeyVerifyFactory.getPrimitive(publicKeysetHandle);
verifier.verify(signature, message);
return true;
} catch (GeneralSecurityException ex) {
log.error("Signature is invalid", ex);
}
return false;
}
Schlüsselrotation
Neben der Verwendung sicherer Algorithmen müssen die eingesetzten Schlüssel möglichst regelmäßig ausgetauscht werden. Idealerweise stehen die alten Versionen dabei weiterhin "passiv" zur Verfügung: Sie können damit weiterhin zur Entschlüsselung bzw. zur Verifizierung einer Signatur eingesetzt werden. Deren "aktive" Verwendung etwa bei der Verschlüsselung ist aber nicht mehr möglich. In Tink wird die Schlüsselrotation dazu zunächst durch einen simplen rotate()-Aufruf mit dem unterstützten KeyTemplate auf dem Keyset ausgelöst (Listing 9).
Listing 9:
KeysetHandle keysetHandle = KeysetManager.withKeysetHandle(privateKeysetHandle)
.rotate(HybridKeyTemplates.ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM)
.getKeysetHandle();
Anschließend wird das Keyset am Ende der Liste um den neuen Schlüssel erweitert und die Referenz auf den Primary Key aktualisiert (Listing 10).
Listing 10:
{
"primaryKeyId": 1363841166,
"key": [{
"keyData": {
"typeUrl": "type.googleapis.com/google.crypto.tink.EciesAeadHkdfPrivateKey",
"keyMaterialType": "ASYMMETRIC_PRIVATE",
"value": "EooBEkQKBAgCEAMSOhI4CjB..."
},
"outputPrefixType": "TINK",
"keyId": 58098115,
"status": "ENABLED"
},
{
"keyData": {
"typeUrl": "type.googleapis.com/google.crypto.tink.EciesAeadHkdfPrivateKey",
"keyMaterialType": "ASYMMETRIC_PRIVATE",
"value": "EosBEkQKBAgCEAMSOhI4CjB0e..."
},
"outputPrefixType": "TINK",
"keyId": 1363841166,
"status": "ENABLED"
}]
}
Jedes Keyset muss zu einem einzigen "Primitive" gehören; das Mischen von beispielsweise symmetrischen und hybriden Algorithmen ist daher nicht erlaubt. Um den nun nicht mehr gewünschten ersten Schlüssel (oder einen beliebigen anderen) zu deaktivieren, muss der "KeysetManager" erneut aufgerufen werden (Listing 11).
Listing 11:
KeysetHandle keysetHandle = KeysetManager.withKeysetHandle(keysetHandle)
.disable(keysetHandle.getKeysetInfo().getKeyInfo(0).getKeyId())
.getKeysetHandle();
Sofern es sich nicht um den einzigen Schlüssel zw. Primary Key im Keyset handelt, ändert sich der Status des Keys anschließend von ENABLED zu DISABLED.
Fazit
Die Verwendung kryptographischer Operationen in der eigenen Anwendung kann mit Google Tink spürbar vereinfacht werden. Dessen sichere Defaults verhindern dabei, dass unsichere Implementierungen in die Produktion gelangen. Mit der damit einhergehenden Reduktion auf wenige ausgewählte Algorithmen und Konfigurationsoptionen werden viele Anwendungen keine Probleme haben. Schwerwiegender dürfte das als Schlüsselformat eingesetzte JSON wiegen: wer auf den Austausch mit anderen Java-Anwendungen im Standard-Java-Format "p12" angewiesen ist, muss nun einige zusätzliche Verrenkungen durchführen. Sein volles Potenzial rund um die einfache Krypto-Anwendung kann Google Tink voll ausspielen, wenn alle beteiligten Anwendungen auf die Tink-Bibliothek setzen oder kein Austausch mit anderen Anwendungen erforderlich ist.