Valkuilen en uitdagingen bij cryptografie in Java

In de vorige editie van Java Magazine hebben we een introductie in cryptografie gegeven. In dit artikel gaan we verder in op de do’s en dont’s als het gaat om het gebruik van deze cryptografie in Java. 

Door: Nanne Baars & Jeroen Willemsen

In ons vorige artikel legden we het een en ander uit over symmetrische cryptografie. Zo zijn we ingegaan op block operation modes, ciphers, Initialization Vectors (IVs), Nonces en padding. Waar moet je in dat geval dan eigenlijk op letten? Laten we gaan kijken.

Tips voor block operation modes

Laten we maar direct met de belangrijkste aanbeveling starten als het gaat om de block operation modi. Als je AES gebruikt, gebruik dan nooit AES-ECB (Electronic CodeBook): Zoals je kan zien op de afbeelding hieronder van Wikipedia, is een encryptie met ECB (middelste afbeelding) een verschuiving van de bits, maar is de originele structuur nog steeds op te halen en is de content ook te herleiden.

Voorbeeld van problemen bij ECB

Van links naar rechts: geen encryptie toegepast, ECB encryptie toegepast, CBC of CTR encryptie toegepast. Er is wel een uitzondering: je kan ECB gebruiken als het te versleutelen bericht past in 1 blok van maximaal 128 bits, alleen komt een dergelijk klein bericht niet zo vaak voor.

Kies je voor AES-CBC (Cipher Block Chaining)? Voeg dan ook een HMAC toe: CBC is gevoelig voor een padding oracle attack, deze attack is gebaseerd op het veranderen van bytes in de padding om uiteindelijk een afleiding van de sleutel te kunnen doen en het bericht te kunnen decrypten. Om dit te voorkomen, dien je een HMAC toe te passen op de ciphertext. Wordt de ciphertext dan aangepast, dan zal de HMAC validatie falen. Zorg ervoor dat je gebruikmaakt van de combinatie “Encrypt then MAC”. Alle andere vormen (“MAC then encrypt”, “MAC and encrypt”) zijn kwetsbaar. Controleer altijd eerst de MAC en ga dan pas verder met ontsleuteling van het bericht.

Hoe dan ook: zorg ervoor dat je bij decryptie zo min mogelijk feedback deelt, zoals ook te zien is in onze voorbeelden op Github. Als je wel meer specifieke decryptie fouten deelt, wordt het veel gemakkelijker voor een aanvaller om je ciphertext te ontsleutelen als bijvoorbeeld de HMAC mist.

Gebruik je AES-GCM (Gallois Counter-Mode)? Bij GCM kan je het beste de GCMParameterSpec klasse te gebruiken in plaats van de IVParameterSpec. Met de GCMParameterSpec kun je context geven aan je ciphertext: bijvoorbeeld door user-ID op te nemen als associated text kan je een link maken tussen het subject waar de data over gaat en de ciphertext.

Nog even terug naar IVs en Nonces bij AES

Er is een belangrijk verschil op te merken tussen een IV en een Nonce (number used ONCE). Een IV moet random zijn en een Nonce kan ook gebaseerd zijn op een counter. Dit wordt extra belangrijk bij AES-GCM. AES-GCM is een voorbeeld van AEAD (Authenticated Encryption with Associated Data), waar een Nonce verplicht is. Deze Nonce kan gewoon een counter zijn, het is daarbij wel belangrijk om dit nummer exact één keer te gebruiken, anders is er een aanval op de gebruikte sleutel. Het voert te ver om in dit artikel hieraan aandacht te besteden, zie [1] voor meer informatie.

In [2] kun je de aanbeveling lezen waarom de IV random moet zijn, ook een voorspelbare IV (toegestaan bij een Nonce) levert problemen op. Laten we kijken naar AES CBC waarbij gebruik wordt gemaakt van een counter als IV. Stel Mallory krijgt van Alice de uitdaging om de inhoud van het bericht te raden, stel een veld heeft een vast aantal waarden: “ongehuwd, gehuwd, samenwonend”. Mallory heeft natuurlijk een kans van één op drie om het juist te raden, maar laten we eens kijken of Mallory dit altijd kan winnen:

In AES CBC is encryptie als volgt gedefinieerd:

Ci = E(key, Pi ⊕ Ci-1)
C0 = IV

In ons geval: Calice = E(key, Palice ⊕ IValice) = E(key, getrouwd ⊕ IValice)

De taak aan Mallory is nu: gegeven het versleutelde bericht van Alice(Calice) kan Mallory achterhalen dat Alice de waarde “getrouwd” heeft versleuteld met 100% zekerheid? Dat kan, Mallory heeft kennis van de IV gebruikt door Alice, Mallory weet ook de volgende IV(IVmallory) dus als Mallory het volgende bericht maakt:

Pmallory = getrouwd ⊕ IVmallory ⊕ IValice

Als we dit versleutelen krijgen we:

Cmallory = E(key, Pmallory ⊕ IVmallory) = E(key, (getrouwd ⊕ IVmallory ⊕ IValice) ⊕ IVmallory)

IVmallory ⊕ IVmallory kunnen we tegen elkaar wegstrepen dus:

Cmallory = E(key, getrouwd ⊕ IValice)

Nu kan Mallory dus kijken of Cmallory gelijk is Calice als dit zo is, weet Mallory dat Alice de waarde “getrouwd” heeft gebruikt.

Dit voorbeeld is te vinden in onze Github repository[3].

Werk je toch met AES-GCM en kan je een onvoorspelbaar eenmalig gebruikte Nonce niet garanderen? Probeer dan random IVs met een beperkt gebruik van de sleutel. Je mag namelijk ook die IV niet twee keer gebruiken. Dit wordt lastiger, omdat de meeste Java implementaties maar 96 bits aan effectieve IV lengte hebben. Dit betekent, volgens [4], dat je maximaal 2^32 berichten mag encrypten met dezelfde sleutel. Dit kan nog eens een stuk minder worden door het volgende verschijnsel: op het moment dat je meerdere services hebt die een eigen secure random initialiseren en die vervolgens gebruiken om een random IV te genereren, dan is de kans aanwezig dat deze secure-random periodes overlap hebben op de korte termijn. Daarom is het aan te bevelen om het aantal encryptie operaties met dezelfde key meer te limiteren.

Algemene tips

Welke algoritmes zijn oké? Als laatste is het van belang om de juiste ciphers te kiezen: gebruik, in geval van twijfel, alleen AES-128, AES-256, CHACHA-20 in combinatie met de HMAC Poly-1305. Randomize IVs: een weggevertje, maar zorg ervoor dat je altijd random IVs gebruikt. Roteer sleutels. Niet alleen vanwege zwakheden in implementaties (zoals bij GCM), maar ook omdat je niet zeker weet wanneer de sleutel gecompromitteerd wordt. Op mobiel? Beveilig de sleutel! Zorg ervoor dat je de Trusted Execution Environment/Secure Enclave kan gebruiken om de sleutel te beveiligen. Kan dat niet? Zorg ervoor dat de sleutel alleen tijdelijk op het apparaat is. Ruim hem ook altijd zelf op na gebruik: zeroize de byte array van de sleutel.

Asymmetrische encryptie: do’s en dont’s

Ga geen grote plaintext blocken encrypten: asymmetrische cryptografie is niet bedoeld voor grote blokken plaintext (meer dan de capaciteit van één block van de sleutel). Wil je toch grote blokken plaintext versleutelen? Gebruik dan het principe van envelope encryptie. Dan genereer je een symmetrische sleutel die je gebruikt voor het encrypten van de plaintext en je gebruikt asymmetrische cryptografie voor het encrypten van de symmetrische sleutel. Vervolgens verstuur je de versleutelde symmetrische sleutel en het versleutelde bericht. De ontvanger kan dan met de private sleutel de symmetrische sleutel decrypten om vervolgens het versleutelde bericht te decrypten.

Gebruik geen PKCS#1 als padding: PKCS-1 padding is gevoelig voor padding oracle attacks. OAEP is dat veel minder. Moet je alsnog gebruik maken van PKCS-1? Zorg ervoor dat je generieke errors teruggeeft op het moment dat de padding niet klopt. Geef je namelijk specifieke errors terug, dan kan de aanvaller een padding oracle attack uitvoeren.

RSA? Start met RSA-2048: Gebruik geen RSA-1024. Wil je up to date blijven qua key length? NIST publiceert af en toe daar updates over. Kijk eens op keylength.com om daar snel een overzicht van te krijgen.

Let op: bij RSA is het heel gewoon om ECB als mode te gebruiken. Tenslotte kan je bij RSA-2048 ook 2048 bits in het block encrypten. Roteer je sleutel! Net als bij symmetrische encryptie geldt hier hetzelfde: roteer je sleutel met regelmaat. De periode van rotatie is afhankelijk van het risico: hoog risico? Jaarlijks, laag risico? Een jaar of drie?

RSA en Java: Let op. RSA sleutels zijn gebaseerd op Big Integers. Dit betekent dat de sleutel dus eigenlijk altijd in het geheugen blijft vanaf dat hij geïnitialiseerd is. Wil je de sleutel niet altijd in je geheugen? Maak dan gebruik van de Trusted Execution Environment/Secure Enclave op mobiel of maak gebruik van libraries zoals Libsodium.

Wil je toch graag gebruik maken van elliptische cryptografie? Kijk dan bij de website Safe Curves [5] om te zien of de curve daadwerkelijk te gebruiken is.

HMAC

Bij een HMAC draait alles om het gebruik van het juiste hashing algoritme en de juiste sleutellengte. Doordat een hash functie snel te berekenen is, is een brute-force attack een reëel gevaar waar je rekening mee moeten houden. In [6] is te lezen dat de lengte van de sleutel minimaal gelijk moet zijn aan de lengte van het hash output lengte. In de praktijk houden weining libraries rekening met deze eis, bijvoorbeeld een willekeurige JWT library (alg = H256) gooit geen exceptie als de lengte onder de grens is. Een voorbeeld van een dergelijke kwetsbaarheid is te vinden in WebGoat [7] in het onderdeel JWT tokens.

Signature, HMACs: do’s & dont’s

Een signature ≠ HMAC: iedereen met een beetje ervaring merkt al snel dat signature implementaties een stuk trager zijn dan HMACs. Het is dan ook vaak verleidelijk om een signature te verruilen voor een HMAC. Let wel op dat je dan non-repudiation kwijt bent: indien de verifiërende partij een andere is dan de schrijvende partij, dan zal de verifiërende partij ook ineens berichten kunnen maken bij een HMAC. Dit is niet het geval bij een signature.

Gebruik je signatures met RSA? Maak gebruik van de juiste padding: RSA PSS (Probabilistic Signature Scheme) is een betere vorm van padding om daadwerkelijk de veiligheid van een RSA-gebaseerde signature te versterken.

Timing attacks

Indien je met gevoelige data werkt, zorg er dan voor dat de je een implementatie kiest die timing-attack-resistant is. Bij een timing attack wordt het verschil in responstijd gebruikt door de aanvaller om iets af te leiden over de data die verstuurd wordt. Bijvoorbeeld: indien een HMAC validatie bij de eerste fout faalt en niet verder gaat of indien een padding check bij de eerste check faalt en stopt met de decryptie, dan leert de aanvaller daar meteen van. Daarom is het goed om timing resistant implementaties te gebruiken: een gefaalde decryptie moet net zolang duren als een succesvolle decryptie.

Je security provider

Als laatste: Java maakt gebruik van meerdere security providers voor de daadwerkelijke implementatie van de cryptografische operatie. Zorg ervoor dat de juiste gebruikt wordt. Op Android betekent dat: patch je security provider en definieer niet welke je wil gebruiken. Op de back-end betekent dat: maak gebruik van van BouncyCastle en vergeet deze niet in een static block als provider toe te voegen (`Security.addProvider(..) ` ). Let wel op: als je meerdere security providers op je classpath hebt welke dezelfde cipher/signature/HMAC supporten, moet je wel de provider opgeven. Doe je dat niet, dan kan de operatie wel eens door een andere provider uitgevoerd worden die ook support heeft voor de desbetreffende configuratie. Zie ook de voorbeelden onze Github repository [8].

Heb je het vorige artikel gemist of wil je nog even terug naar de basis voorbeelden? Kijk dan gerust even in onze repository.
Door:
Nanne Baars | software developer bij Xebia.
Jeroen Willemsen | Principal Security Architect bij Xebia Security.

[1] https://tools.ietf.org/id/draft-irtf-cfrg-gcmsiv-08.html

[2] CWE-329: http://cwe.mitre.org/data/definitions/329.html

[3] https://github.com/nbaars/java-magazine-article/

[4] NIST Special Publication: https://dx.doi.org/10.6028/NIST.SP.800-38D

[5] Safe Curves website: https://safecurves.cr.yp.to/

[6] https://tools.ietf.org/html/rfc2104#section-3

[7] https://webgoat.github.io/WebGoat/

[8] https://github.com/nbaars/java-magazine-article/