Java: Sifrujeme s RSA

Vecery v tomto tydnu jsem stravil stridave pojidanim sushi, ctenim Pratchetta a programovanim te libustky, kterou jsem popisoval ve svem predeslem postu. Uprimne receno, take jsem si puvodne nemyslel, ze mi to zabere tolik casu, ale tak to holt vetsinou byva. Zakladnim kamenem urazu se stalo zakodovani a rozkodovani dane trid pomoci RSA sifry.

Prvni co jsem udelal bylo vygenerovani privatniho klice tedy

keytool -genkey -alias AlCapone -keyalg RSA -keysize 1024 -dname "CN=Al Capone,O=Mafia,C=IT" -keypass prohibition -storepass coffin

a nasledne vygenerovani odpovidajiciho X509 certifikatu s public klicem

keytool -export -storepass coffin -alias AlCapone -file alcapone.cer

So far, so good.

Jelikoz se Sunim jdk neni standardne dodavan JCE RSA provider (az snad od 1.5, ale to jsem nekontroloval), je potreba sahnout po knihovne treti strany. Jako prvni jsem zkusil knihovnu Cryptix, ktera slibovala vyborny vykon. Bohuzel, s ni jsem narazil protoze zhlediska asymetrickeho sifrovani podporuje pouze model “zasifruj public klicem a rozsifruj private”. A ja to potreboval presne naopak.

Tak mi prisel pod ruku BouncyCastle. Rychly testik, “filemon” zakodovan a dekodovan v poradku a jde se na vec. Jako prvni jsem se pro zakodovani souboru snazil pouzit javax.crypto.CipherOutputStream, ktery by bere v konstruktoru inicializany java.security.Cipher objekt a provadi kryptovani pred zapisem. Zel bohu, po zapsani par byte mi programek vracel “too much data for RSA”. Rozhodl jsem se tedy provest kompletni zasifrovani pred zapisem a pak teprve zapsat. Sekvence Cipher.update(byte[] data) a nakonec Cipher.doFinal() opet ustila v “too much data for RSA”. Proto jsem nakonec skoncil s nasledujicim kodem, ktery postupne po blocich sifruje vstup a svyuzitim DataOutputStreamu ho uklada

import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.Key;
import java.security.KeyStore;
import javax.crypto.Cipher;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

public class RSAEncryptor {

	public static final int DATA_BLOCK = 0x01;
	public static final int FINAL_BLOCK = 0x02;
	private static final String RSA_CIPHER = "RSA/None/OAEPPadding"; 

	public static void main(String[] args) throws Exception {
		String storePass = "coffin";
		String keyPass = "prohibition";
		String alias = "AlCapone";
		String keystorePath = "C:/tmp/.keystore";
		String filePath = "C:/tmp/Checker.class";
		String outputPath = "C:/tmp/EncChecker.class";
		new RSAEncryptor().doIt(new FileInputStream(new File(filePath)),
				new FileOutputStream(new File(outputPath)),
				new FileInputStream(keystorePath), alias, storePass, keyPass);
	}

	private void doIt(InputStream input, OutputStream output,
			InputStream keystoreInput, String alias, String storePass,
			String keyPass) throws Exception {
		
		java.security.Security.addProvider(new BouncyCastleProvider());
		KeyStore keystore = KeyStore.getInstance("JKS", "SUN");
		keystore.load(keystoreInput, storePass.toCharArray());
		Key privKey = keystore.getKey(alias, keyPass.toCharArray());

		DataOutputStream os = new DataOutputStream(output);
		
		byte[] chunk = new byte[64];
		int length = 0;
		byte[] enc;
		Cipher cipher = Cipher.getInstance(RSA_CIPHER);
		cipher.init(Cipher.ENCRYPT_MODE, privKey);		
		while ((length = input.read(chunk)) != -1) {
			enc = cipher.doFinal(chunk, 0, length);
			os.writeInt(enc.length);
			os.write(enc);
		}
	}
}

Zastavil bych se jeste u jednoho problemku, ktery muze potrapit asice “padding”. Padding je v podstate specifikace toho, jak u blokovych sifer nalozit s poslednim blokem bytu (sifra pracuje s pevnym poctem byte v danem behu a tak nam na konci vetsinou zbyde neuplny blok). Specifikace jsou ruzne, napriklad PKCS#5 naplni pred sifrovanim zbyvajici bytes cislem, urcujicim pocet zbyvajicich bytes. Pokud neni specifikovan pri inicializaci sifry, mel by se pouzit nejaka defaultni hodnota. Mne se s BouncyCastlem stalo, ze mi po desifrovani nekolik koncovych bytes chybelo! Ukazuje to u me na to, ze se padding mechanismus musel nejakym zpusobem pri sifrovani a desifrovani lisit, ale uz jsem nemel cas to zkoumat. Proto tedy, abyste se vyhnuli takovymto problemum, doporucuji vzdycky “vystlavaci” algoritmus pri inicializaci sifry specifikovat (v tomto pripade “OAEPPadding”).