Elke ontwikkelaar loopt er vroeg of laat tegenaan: cryptografie. Bij de gedachte aan keyformaten, PKCS, SHA-256 en certificaten slaat menige ontwikkelaar de schrik om het hart. Gelukkig biedt het Java platform uitstekende abstracties hiervoor, maar het ontslaat je er niet van te begrijpen wat het precies is.
Cryptografie is vaak een onbegrepen gebied. Een wirwar aan terminologie, ingewikkelde wiskunde en complexe concepten zorgen ervoor dat we het als ontwikkelaar of negeren of (goed bedoeld) onveilige oplossingen bouwen met de aangeboden tools.
Gelukkig is dat niet nodig. De concepten achter cryptografie zijn best overzichtelijk en worden in de Java security API goed beschikbaar gemaakt. Het begrijpen van deze concepten maakt het makkelijker om samen te werken met security-experts. Bovendien kun je met deze terminologie altijd indruk maken op je baas!
Concept 1: hashing
In cryptografie hebben we het altijd over het omzetten van iets leesbaars in iets onleesbaars, wat eruit ziet als “ruis.” De eenvoudigste vorm hiervan is “hashing”. Dit is de transformatie van data van willekeurige lengte naar ogenschijnlijk betekenisloze fixed-lengte data. De bekende CRC-check is hier een voorbeeld van (listing 1).
Figuur 1. Een hashing functie maakt van een gegeven input een vaste lengte output en is niet omkeerbaar.
Een hashingfunctie is stabiel (zelfde input levert zelfde output) en onvoorspelbaar. Elke wijziging in de plaintext heeft een onvoorspelbare wijziging in de hash tot gevolg. Vanuit een gegeven hash is het lastig (“infeasible,” zoals cryptografen dat noemen) om de plaintext te achterhalen.
CRC32 crc = new CRC32();
crc.update("Java".getBytes());
long checksum = crc.getValue();
System.out.println(String.format("Checksum:\n %s", Long.toHexString(checksum)));
Listing 1
De Java security API streeft gelijkvormigheid na. Alle operaties op bulk-data volgen het stramien (1) initialiseren met getInstance(…) en/of init(…), (2) vullen met update(…), (3) afmaken en uitlezen met getValue(), doFinal(…) of digest. De operaties zijn potentieel duur en daarom vaak herbruikbaar met dezelfde instellingen, door de reset() methode aan te roepen.
Er zijn verschillende soorten hashing functies, die allemaal hun eigen toepassingen hebben. Checksums, zoals CRC, de Cyclic Redundancy Check, is een hele snelle hash die gebruikt wordt om integriteit van data vast te stellen, zoals in communicatieprotocollen. Het zwakke punt van checksums is dat ze goed bestand zijn tegen ruis in een communicatielijn, maar doordat ze vaak korte hashes opleveren met een relatief eenvoudig algoritme, zijn ze erg gevoelig voor collisions. Dit is het geval waarin twee verschillende plaintexts dezelfde hash opleveren. Als dit bewust gebeurt, door een bericht te veranderen zonder dat de hash verandert, spreken we van een collision attack. In het verleden is MD5 hier gevoelig voor bevonden.
Hashes die bestand zijn tegen moedwillige aanvallen worden cryptografische hashes genoemd, in Java een MessageDigest.
MessageDigest sha; try {
sha = MessageDigest.getInstance("SHA-256");
}
catch (NoSuchAlgorithmException e) {
return;
}
sha.update("Java".getBytes());
byte[] finalHash = sha.digest();
System.out.println(String.format("Final hash:\n %s", DatatypeConverter.printHe xBinary(finalHash)));
Listing 2
Listing 2 toont het gebruik van SHA-256. De Java security API is ‘uitbreidbaar’ opgezet en je kunt eigen typen message digests toevoegen, vandaar het String-argument in de getInstance(…) methode. De Java Language Specification bevat een lijst met verplichte methoden, waardoor dat we de NoSuchAlgorithmException veilig kunnen negeren.
SHA-256 is voor veel toepassingen op dit moment de populairste hash. Het is collision-bestendig, snel, goed te versnellen in hardware en er zijn geen bekende collision attacks. Hiermee is de integriteit van een bericht te waarborgen, maar niet de authenticiteit. Daarvoor bestaat een derde en laatste type hash, de Message Authentication Code (Listing 3).
Mac mac = Mac.getInstance("HmacSHA256")
Key secret = new SecretKeySpec("password".getBytes(), "HmacSHA256")
mac.init(secret)
byte[] digest = mac.doFinal("AuthenticatedMessages".getBytes())
System.out.println(String.format("MAC:\n %s", Util.toHex(digest)));
Listing 3
Bij een MAC wordt gebruik gemaakt van een vooraf uitgewisselde key. Verificatie gebeurt doordat de ontvanger dezelfde functie uitvoert, met dezelfde key, en de resultaten vergelijkt (Figuur 2).
Figuur 2. Een Message Authentication Code maakt een unieke hash door een key toe te voegen.
Keys
Het begrip key slaat op het geheime deel van een encryptiesysteem. Een key is bijna altijd een (groot) getal, wat aan de oppervlakte geen betekenis heeft (opaque). De Java security API heeft alle keys geabstraheerd tot één Key-object, wat op verschillende manieren verkregen kan worden. Een veelgebruikte vorm is het maken van een Key op basis van bestaand materiaal (Listing 4). Een KeySpec is een beschrijving van een key, waaruit vaak een key te genereren is.
Key key = new SecretKeySpec(
new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
"AES");
System.out.println(String.format("secret key from 0 input: %s", Util.toHex(key.getE ncoded())));
Listing 4
Een bijzondere vorm van het verkrijgen van keys is generatie op basis van een password (Listing 5). Encryptie-mechanismen als TrueCrypt of BitLocker en beveiligingsapplicaties als 1Password maken gebruik van een dergelijk mechanisme om vanuit jouw password een veilige key te genereren.
Omdat keys een vaste lengte hebben, en in niet meer dan bitruis zijn, werden hier in het verleden vaak cryptografische hashes voor gebruikt. Het nadeel hiervan is dat deze hashes gemaakt zijn om snel te zijn, wat brute-forcing van de key mogelijk maakt. Gebruik voor password hashing altijd een geschikte functie als PBKDF2 of scrypt–ook als je het resultaat enkel gebruikt voor authenticatie van de gebruiker.
SecretKeyFactory skf = SecretKeyFactory.getInstance( "PBKDF2WithHmacSHA1" );
PBEKeySpec spec = new PBEKeySpec( "password".toCharArray(),
new byte[] {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, 50,
128 );
Key key = skf.generateSecret(spec);
System.out.println(String.format("secret key from \"password\" using PBKDF2 with Hm acSHA256: %s", Util.toHex(key.getEncoded())));
Listing 5
Concept 2: symmetrische encryptie
Het onomkeerbaar versleutelen van data is goed om integriteit en authenticiteit vast te stellen, maar voor vertrouwelijkheid is het nodig om het versleutelde ook weer leesbaar te kunnen maken. Met het uitwisselen van een key kunnen we symmetrische encryptie bereiken. Dit is versleuteling waarbij dezelfde key zowel de encryptie als de decryptie kan doen.
Figuur 3. Bij symmetrische encryptie wordt dezelfde key gebruikt voor zowel encryptie als decryptie.
In Java gebruiken we hiervoor een Cipher: een object dat wordt geïnitialiseerd met instellingen en een key (Listing 6). Voor symmetrische encryptie zijn encryptie en decryptie vergelijkbaar, enkel de modus waarmee de Cipher.init(…) wordt aangeroepen verschilt.
byte[] data = "1234567890123456".getBytes();
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encrypted = cipher.doFinal(data); System.out.println(String.format("Encrypted version of \"%s\" with AES key 0: %s",
new String(data), Util.toHex(encrypted)));
Listing 6
In de praktijk zullen alle symmetrische encryptiealgoritmen die je gebruikt zogeheten block ciphers zijn: algoritmen die werken op een block data van gegeven lengte, zoals 128 bits in Listing 6.
Wil je minder data dan een geheel block verwerken, dan zal je een vorm van padding moeten toevoegen: opvulling om tot een geheel blok te komen. Door de NoPadding in Listing 6 te vervangen door PKCS5Padding wordt volledig deterministische padding toegevoegd, die ook door het decryptie-mechanisme wordt herkend.
Bij het versleutelen van meer data dan één block, bied je die aan de update(…) methode aan. De manier waarop meerdere blocks worden behandeld, wordt de mode genoemd. In ECB (Electronic Code Book) zal elk blok individueel versleuteld worden, wat ervoor zorgt dat dezelfde plaintext altijd dezelfde ciphertext oplevert. Omdat hiermee patronen uit de plaintext zichtbaar worden in de ciphertext, is dit onwenselijk.
Figuur 4. Data die versleuteld wordt met ECB vertoont patronen uit de plaintext. Met één van de andere modi wordt de data teruggebracht naar ruis.
Dit patroon wordt voorkomen door elk block het volgende te laten beïnvloeden. Hier zijn verschillende methoden voor, waarvan CBC (Cipher Block Chaining) de meest inzichtelijke is. Voor de encryptie van block 2 wordt een XOR operatie uitgevoerd met de ciphertext van block 1. Voor block 3 doen we hetzelfde met de ciphertext van block 1, enzovoorts.
Cryptografie bestaat vaak uit het verplaatsen van het probleem, tot het handelbaar wordt. Hier hebben we zo'n punt bereikt. Op het eerste block wordt een XOR uitgevoerd met een speciale waarde, de Initialization Vector, waardoor de keten compleet is. Deze wordt vaak ergens anders in het encryptie-schema bepaald.
Hoewel CBC de meest inzichtelijke mode is voor het verwerken van meer data dan één block, is het niet de meest veilige. In de praktijk zal je vaker GCM (Galois/Counter Mode) aantre?en.
Figuur 5: CBC mode met een initialization vector.
Key key = new SecretKeySpec(new byte[16], "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec iv = new IvParameterSpec(new byte[16]); cipher.init(Cipher.ENCRYPT_MODE, key, iv);
byte[] encrypted = cipher.doFinal("input".getBytes("UTF-8"));
cipher.init(Cipher.DECRYPT_MODE, key, iv); byte[] decrypted = cipher.doFinal(encrypted); System.out.println(new String(decrypted));
Listing 7
Symmetrische encryptie wordt veel gebruikt voor bulk-encryptie: het versleutelen van communitie als HTTPS of een VPN of data in rust op je harddisk. Het meest gebruikte algoritme, AES (Advanced Encryption Standard), is zo ontworpen dat het goed in hardware te versnellen is. Je telefoon heeft bijna altijd hardware aan boord waarmee AES encryptie snel en zuinig gedaan kan worden.
Het voornaamste nadeel van symmetrische encryptie voor communicatie is dat beide partijen het eens moeten zijn over de key die gebruikt gaat worden, waarmee key-distributie een potentiële aanvalsvector wordt.
Concept 3: asymmetrische encryptie
Met een keypair bestaande uit twee keys, eentje voor encryptie en eentje voor decryptie, kunnen we asymmetrische encryptie bereiken. Eén van de sleutels is public, en de andere is private. Dit mechanisme voorkomt het vooraf uitwisselen van keys en komt daarom veel voor in praktische cryptosystemen. Certificaten voor websites, PGP email-encryptie, maar bijvoorbeeld ook de chip in je bankpas zijn allemaal gebaseerd op asymmetrische encryptie.
Figuur 6. Bij asymmetrische encryptie wordt encryptie verzorgd door de ene key uit een keypair en decryptie door de andere.
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); keyPair = kpg.generateKeyPair();
byte[] data = "input".getBytes("UTF-8");
Cipher rsa = Cipher.getInstance("RSA"); rsa.init(Cipher.ENCRYPT_MODE, keyPair.getPublic()); byte[] encrypted = rsa.doFinal(data);
rsa.init(Cipher.DECRYPT_MODE, keyPair.getPrivate()); byte[] decrypted = rsa.doFinal(encrypted); System.out.println(String.format("Decrypted: %s",
new String(decrypted)));
Bijschrift: Listing 8
De keys vormen altijd een pair, maar het is nog vrij te kiezen welke van de twee public is en welke private. Beide methoden hebben zo hun toepassing. Door één key beschikbaar te stellen, kun je vertrouwelijkheid creëren. Iedereen mag een bericht versleutelen, maar alleen de ontvanger kan het weer leesbaar maken. De andere kant op kan iets wat met de private key versleuteld is, altijd met de public key leesbaar gemaakt worden. Zo kan de authenticiteit van een bericht vastgesteld worden.
Het gebruik van een keypair zorgt ervoor dat vertrouwelijke informatie (de private key) nooit verstuurd hoeft te worden. De key in jouw bankpas bevindt zich alleen in je bankpas en als je ooit een certificaat voor een website hebt aangevraagd, dan weet je dat je de gegenereerde private key nooit meestuurt met het signing request. De voornaamste beperking van asymmetrische cryptografie is dat het aardig resource-intensief kan zijn en daarom vaak te duur voor bulk- encryptie.
Elk praktisch crypto-systeem bevat daarom elementen van hashing, symmetrische encryptie en asymmetrische encryptie. Het samenvoegen van deze primitieven is de taak van de cryptograaf.
De Java Cryptography Architecture (JCA)
De API brengt de nodige abstractie aan en verschillende concepten met worden met dezelfde code-constructen behandeld. De overige delen van de API volgen hetzelfde patroon. Elke JVM bevat basis-implementaties van de verplichte algoritmen. Daarnaast is het mogelijk om een eigen Provider aan te leveren. Je kunt hiermee bijvoorbeeld algoritmen gebruiken die niet standaard beschikbaar zijn.
Security-gerelateerde activiteiten worden in Java meestal met de Java security API gedaan. Door het aanbieden van een alternatieve Provider lift alle code die hier gebruik van maakt mee op bijvoorbeeld optimalisaties voor jouw platform. De code kan ook gebruikmaken van een HSM (Hardware Security Module) die als Provider beschikbaar is.
Bouwen maar! Toch?
De Java API geeft je uitstekende tools in handen om een cryptosysteem te bouwen. Echter, het ontwerpen hiervan is écht specialistenwerk. Als het erop lijkt dat jouw situatie zó bijzonder is dat een standaard mechanisme (zoals bijvoorbeeld TLS) niet lijkt te passen, roep dan de hulp van een expert in.
Tot slot
In dit artikel hebben we enkel de hoofdlijnen gesproken van wat jij als Java ontwikkelaar moet weten over encryptie. Op https://github.com/angelos/javacrypto vind je de codevoorbeelden en een cheat sheet die je kunt gebruiken om nóg meer indruk te maken op je baas, collega’s en de huis-cryptograaf!