Při práci na projektu pro našeho významného klienta se neobejdeme bez šifrování. V tomto článku bych se chtěl podělit o některé zkušenosti a především nastínit základy šifrování v prostředí platformy Java. Článek si neklade za cíl jít do příliš velké hloubky ani být vyčerpávajícím popisem, ale měl by sloužit jako úvod do šifrování v Javě.

Začnu troškou teorie. Šifry (přesněji řečeno kryptografické algoritmy) je možné rozdělit podle mnoha kritérií, nás však budou zajímat především dvě z těchto členění – šifry blokové a proudové a šifry symetrické a asymetrické.

Šifry blokové a proudové se mimo jiné liší tím, s jak velkou částí zdrojového textu pracují. Proudové šifry operují obvykle s jednotlivými bity nebo byty s tím, že transformace každého z nich je proměnlivá. Blokové šifry pracují se většími bloky textu, pro které je transformace neměnná.

Symetrické šifry používají pro šifrovaní i dešifrování stejný klíč, proto je při použití velmi důležité utajení tohoto klíče. Asymetrické šifry používají 2 různé klíče, mezi kterými je matematická vazba a v rozumném čase není možné odvodit jeden z druhého. Na použití těchto šifer je založená asymetrická kryptografie.

Rád bych na několika zdrojových kódech ukázal, jakým způsobem je možné v Javě pracovat se šiframi DES, TripleDES, AES a RSA. V případě DES a jejích variant a šifry AES se jedná o blokové symetrické kryptografické algoritmy. RSA je potom šifrou asymetrickou, která je však také bloková.

DES a TripleDES

Šifra DES vznikla již v 70. letech 20. století a byla po dlouhou dobu velmi rozšířenou v mnoha oblastech. Její hlavní nevýhodou je malá délka klíče, která činní pouhých 56 bitů. Z tohoto důvodu se začala používat varianta TripleDES. Tato šifra je s původní DES zpětně kompatibilní. V případě TripleDES je délka klíče, jak již název napovídá, 168 bitů, tedy trojnásobek původní délky klíče. Pro TripleDES lze však také použít tzv. double length key, který má délku 112 bitů.

Princip TripleDES je následující. Zdrojový text se nejprve zašifruje DES šifrou pomocí prvních 56 bitů TripleDES klíče. Výsledný text se poté dešifruje DES algoritmem pomocí druhých 56 bitů TripleDES klíče. Výsledek této operace je nakonec opět zašifrován pomocí posledních 56 bitů TripleDES klíče. Pokud chceme použít pouze double length key, je prvních a posledních 56 bitů klíče stejných. V případě DES je pak 168 bitů TripleDES klíče tvořeno 3 stejnými 56 bitovým DES klíči. TripleDES je tak logicky kompatibilní s původní DES. Algoritmus je tedy pro všechny varianty stejný, rozdíl je pouze v tom, jaký klíč použijeme. Vzhledem k tomu, že princip TripleDES spočívá v zašifrování, dešifrování a opětném zašifrování zdrojového textu, je tato šifra také nazývána DESede, tedy DES encrypt – decrypt – encrypt.

Než se dostanu k ukázce zdrojového kódu, je třeba zmínit ještě jednu vlastnost blokových šifer, i když by měla být zřejmá. Pomocí blokové šifry je možné šifrovat pouze fixní bloky dat o délce bloku (block size). Tato délka je specifická pro každou šifru. V případě DES je to 64 bitů. V závislosti na módu šifry je často nutné, aby vstupní text byl takové délky, která je násobkem délky bloku. Pokud tomu tak není, je třeba poslední blok vstupních dat doplnit o nějaké hodnoty. Tomuto procesu se říká padding a může být implementován různými způsoby.

Nyní již k samotným ukázkám zdrojového kódu.


SecretKeyFactory factory = SecretKeyFactory.getInstance("DES");
KeySpec keySpec = new DESKeySpec(new byte[] {1, 1, 1, 1, 1, 1, 1, 1});
SecretKey key = factory.generateSecret(keySpec);
Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
byte[] sourceData = "Lorem ipsum dolor sit amet.".getBytes("UTF-8");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encrypted = cipher.doFinal(sourceData);

cipher.init(Cipher.DECRYPT_MODE, key);
byte[] decrypted = cipher.doFinal(encrypted);
String decryptedData = new String(decrypted, "UTF-8");
System.out.println(decryptedData);

Předchozí příklad ilustruje použití šifry DES v Javě. Volané metody vyhazují větší množství výjimek, pro lepší přehlednost jsou však bloky try – catch vynechány. Jádrem JCE (Java Cryptographic Extension) je třída javax.crypto.Cipher. Samotné šifrování a dešifrování je starostí této třídy. Aby však bylo možné třídu Cipher použít, je nutné jí sdělit, jakou šifru, její mód, padding, klíč a případné další parametry budeme používat. Instanci Cipher dostaneme voláním statické metody getInstance s parametrem typu String, který udává šifru, mód a padding oddělené lomítkem. V tomto případě tedy používáme šifru DES, mód ECB (electronic codebook) a padding scheme PKCS5Padding. Existuje větší množství padding schemes. Padding můžeme řešit i sami, v takovém případě použijeme NoPadding. Časté a doporučované řešení pro padding je za posledním bytem, tvořícím zdrojový text, dosadit byte s hodnotou 0x80 a zbylé byty posledního bloku obsadit nulou.

Pro práci s klíči jsou v Javě stěžejní rozhraní java.security.Key a java.security.spec.KeySpec. Implementace rozhraní Key představuje samotný klíč. Předtím, než můžeme šifrovat, je nunté inicializovat objekt Cipher voláním metody init. Prvním parametrem určujeme, zda se bude šifrovat nebo dešifrovat. Druhý parametr je pak zvolený klíč, tedy objekt typu Key. Tato metoda může mít i další parametry, k tomu se ale dostanu dále.

Než je možné klíč použít, je potřeba ho nějakým způsobem vytvořit. K tomu slouží konkrétní implementace rozhraní KeySpec. V našem příkladu vytváříme DES klíč, proto voláme konstruktor třídy DESKeySpec, který má jako parametr pole bytů představující klíč. Jeho délka je 8, nikoliv 7 bytů, jak by se dalo pro 56 bitů dlouhý DES klíč předpokládat. Poslední bit každého bytu je totiž paritní, proto je ve výsledku využito pouze 7 bytů. Implementace KeySpec představuje jakási metada, informace nutné pro vytvoření klíče (key material). Její pomocí můžeme klíče vytvářet, ale zároveň o klíči zjistit různé informace, např. modulus v případě veřejného klíče, o tom však dále.

Klíč samotný vytvoříme pomocí třídy SecretKeyFactory, jejíž instanci vytváříme pro konkrétní šifru. Na objektu SecretKeyFactory poté zavoláme metodu generateKey, jejímž parametrem je připravený objekt KeySpec. Klíč je také možné si nechat vygenerovat.


SecretKey key = KeyGenerator.getInstance("DES").generateKey();

Nyní jsme již připraveni na šifrování. Zdrojový text převedeme na pole bytů, které předáme metodě doFinal třídy Cipher. Výsledkem je pole bytů zašifrované zvolenou šifrou. Pokud si příklad zkompilujete a spustíte, výsledný vypsaný řetězec bude odpovídat zdrojovému textu „Lorem ipsum dolor sit amet“.


SecretKeyFactory factory = SecretKeyFactory.getInstance("DESede");
KeySpec keySpec = new DESedeKeySpec(new byte[] {1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3});
SecretKey key = factory.generateSecret(keySpec);
Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");
AlgorithmParameterSpec algorithmParameterSpec = new IvParameterSpec(new byte[] {4, 4, 4, 4, 4, 4, 4, 4});
byte[] sourceData = "Lorem ipsum dolor sit amet.".getBytes("UTF-8");
cipher.init(Cipher.ENCRYPT_MODE, key, algorithmParameterSpec);
byte[] encrypted = cipher.doFinal(sourceData);

cipher.init(Cipher.DECRYPT_MODE, key, algorithmParameterSpec);
byte[] decrypted = cipher.doFinal(encrypted);
String decryptedData = new String(decrypted, "UTF-8");
System.out.println(decryptedData);


Ve druhém příkladu je použita šifra DESede, proto místo DESKeySpec používáme DESedeKeySpec. Parametrem konstruktoru v tomto případě musí být pole bytů délky 24 bytů, protože délka klíče je 168 bitů (192 bitů – 24 paritních bitů). Náš klíč je triple length key, protože se jednotlivé části klíče liší.

Druhý příklad se od prvního liší ještě v jedné věci. Mód tentokrát není ECB, ale CBC (cipher-block chaining). Při použití CBC je každý blok před šifrováním modifikován operací XOR vůči masce tvořené předcházejícím blokem. První blok je modifikován tzv. inicializačním vektorem. Tento vektor musíme předat metodě init třídy Cipher. Vytvoříme object IvParameterSpec a v parametru konstruktoru mu předáme pole bytů představující inicializační vektor. Délka musí být v tomto případě 8 bytů, protože délka bloku DES šifry je 64 bitů. Inicializační vektor použitý při šifrování je nutné znát i pro dešifrování.

AES

Šifra AES byla přijata jako standard na počátku 21. století a jejím účelem je nahradit již nevyhovující DES. Délka klíče je v případě AES volitelná a může činit 128, 192, nebo 256 bitů. Délka bloku je 128 bitů.


Key key = new SecretKeySpec(new byte[] {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
byte[] sourceData = "Lorem ipsum dolor sit amet.".getBytes("UTF-8");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encrypted = cipher.doFinal(sourceData);

cipher.init(Cipher.DECRYPT_MODE, key);
byte[] decrypted = cipher.doFinal(encrypted);
String decryptedData = new String(decrypted, "UTF-8");
System.out.println(decryptedData);

V případě šifry AES není možné použít SecretKeyFactory. Místo toho můžeme využít třídu SecretKeySpec a její konstruktor, jemuž druhým parametrem specifikujeme šifru. Protože třída SecretKeySpec je implementací rozhraní Key, je možné ji použít v metodě init třídy Cipher. V našem příkladu jsme vytvořili klíč dlouhý 128 bitů.

RSA

RSA je asymetrická bloková šifra. Protože asymetrické šifry jsou výpočetně výrazně náročnější, než šifry symetrické, užívají se obvykle pro šifrování malého objemu dat (typicky pouze 1 blok). Příkladem takových dat může být klíč symetrické šifry (session key), který se využívá k zabezpečení komunikace mezi dvěma stranami – např. protokol SSL/TSL. Délka klíče v případě RSA odpovídá délce modulu, viz dále. Pokud je zvolena délka klíče 1024 bitů, je délka bloku rovna také 1024 bitů, tedy 128 bytů. Musíme však odečíst 11 bytů, které jsou obětovány paritě. Pro 1024 bitů dlouhý klíč tedy dostáváme blok o délce 117 bytů.

Modulus je společný veřejnému i soukromému klíči a jeho délka odpovídá délce RSA klíče. Každý z klíčů má však svůj vlastní exponent. Proto, abychom mohli vytvořit veřejný a soukromý klíč, potřebujeme stanovit hodnoty těchto čísel. V dosavadních příkladech byly klíče vytvářeny přímo v kódu, s tím se však v praxi příliš nesetkáme. Klíč se obvykle nachází v nějakém keystoru nebo ho získáme jiným způsobem. Pro ilustraci však uvádím, jakým způsobem vytvořit pár RSA klíčů v kódu, pokud známe jejich modulus a oba exponenty.


BigInteger modulus = new BigInteger(1, new byte[] {/* modulus in bytes */});
KeySpec publicKeySpec = new RSAPublicKeySpec(modulus, new BigInteger(1, new byte[] {/* exponent in bytes */}));
KeySpec privateKeySpec = new RSAPrivateKeySpec(modulus, new BigInteger(1, new byte[] {/* exponent in bytes}));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);
PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);

Konstruktory tříd PublicKeySpec a PrivateKeySpec mají parametry modulus a exponent odpovídající tomu kterému klíči. Oba parametry jsou typu BigInteger. Na tomto místě je třeba pamatovat na to, že je nutné použít dvouparametrický konstruktor BigIntegeru, jehož první parametr odpovídá hodnotě funkce signum a udává znaménko čísla reprezentovaného tímto objektem. Hodnota prvního parametru musí být jedna, tedy kladné číslo. Pokud použijeme jednoparametrický konstruktor pouze s polem bytů, může se nám stát, že při pokusu zašifrovat zdrojový text dojde k vyhození výjimky javax.crypto.BadPaddingException: Message is larger than modulus. Tato výjimka bude vyhozena bez ohledu na délku dat, která se pokusíme zašifrovat. Problém tkví v tom, že bez udání znaménka nám pro určité pole bytů může vzniknout záporný BigInteger.

K samotnému vytvoření klíčů u asymetrických šifer nevyužíváme třídy SecretKeyFactory, ale KeyFactory.


byte[] sourceData = "Lorem ipsum dolor sit amet.".getBytes("UTF-8");
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encrypted = cipher.doFinal(sourceData);

cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] decrypted = cipher.doFinal(encrypted);
String decryptedData = new String(decrypted, "UTF-8");
System.out.println(decryptedData);

Šifrování a dešifrování již probíhá obdobně, jako v případě symetrických šifer. Pokud si chceme pár klíčů nechat vygenerovat, můžeme použít následující kód.


KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(1024);
KeyPair keyPair = generator.genKeyPair();
Key publicKey = keyPair.getPublic();
Key privateKey = keyPair.getPrivate();

V případě, že nás zajímá modulus a exponenty vygenerovaných klíčů, je možné z nich vytvořit instance KeySpec, z nichž tyto informace získáme.


KeyFactory keyFactory = KeyFactory.getInstance("RSA");
RSAPublicKeySpec publicKeySpec = keyFactory.getKeySpec(publicKey, RSAPublicKeySpec.class);
RSAPrivateKeySpec privateKeySpec = keyFactory.getKeySpec(privateKey, RSAPrivateKeySpec.class);
publicKeySpec.getModulus();
publicKeySpec.getPublicExponent();
privateKeySpec.getModulus();
privateKeySpec.getPrivateExponent();

Závěr

V článku jsem uvedl příklady použití pouze několika málo šifer, nicméně se jedná o šifry, na než může vývojář v praxi narazit pravděpodobně nejčastěji. Na příkladech jsem se navíc pokusil demonstrovat základní principy a třídy, které Java pro práci se šifrováním využívá. Po přečtení článku by tak čtenář neměl mít problém pustit se do vlastních experimentů s libovolnou šifrou v Javě.