Este post está dedicado al uso de la arquitectura estándar de criptografía de Java (JCA). El objetivo del post es mostrar diferentes casos de uso en los que se aplican, desde las APIs de JCA, diversas utilidades relacionadas con soluciones criptográficas estándar, como son:
- Encriptacion y desencriptación usando diferentes algoritmos.
- Uso de almacenes de claves.
- Generación de claves.
- Encriptación y desencriptación de ficheros.
- Algoritmos de ofuscación de datos (digest).
- Uso de proveedores de algoritmos.
Los ejemplos que se presentan a continuación son parte de un proyecto completo desarrollado como un ejercicio práctico de aplicación de los diferentes mecanismos antes mencionados. El proyecto completo se puede descargar desde el siguiente enlace.
1. Encriptación y desencriptación de texto plano.
El texto a encriptar o desencriptar, una vez encriptado o desencriptado, se almacena como un array de bytes.
1 2 3 4 |
/* Plain text */ String encText = ""; /* Encrypted bytes */ byte[] encrypted = null; |
Esta línea nos permite obtener la representación del algoritmo de encriptación o desencriptación a usar. Veremos más adelante como se implementa.
1 2 |
/* Get cipher */ Cipher cipher = getCipher(algorithm, password, cipherMode); |
Dependiendo del modo establecido, se encripta o desencripta.
- Al encriptar, se obtienen los bytes del texto a encriptar. Se generan los bytes de salida del texto encriptado. Después, para presentarlo o almacenarlo como texto, se codifican, por ejemplo en BASE64.
- Al desencriptar, primero se decodifica el BASE64 para obtener el texto encriptado como array de bytes, estos se desencriptan y se traducen como texto desencriptado final.
1 2 3 4 5 6 7 8 9 10 |
/* Encrypt or decrypt */ if (cipherMode == Cipher.ENCRYPT_MODE) { byte[] data = text.getBytes("UTF8"); encrypted = cipher.doFinal(data); encText = new BASE64Encoder().encode(encrypted); } else if (cipherMode == Cipher.DECRYPT_MODE) { byte[] data = new BASE64Decoder().decodeBuffer(text); encrypted = cipher.doFinal(data); encText = new String(encrypted); } |
Para obtener la representación del algoritmo de encriptación/desencriptación a usar (lo que haría la funcion ‘getCipher‘):
Se instancia el algoritmo con el nombre del mismo tal y como figura en el proveedor seleccionado (‘Blowfish’, ‘DES’, ‘AES’, ‘DESSede’, ‘RSA’, etc.).
1 2 |
/* Algorithm */ Cipher cipher = Cipher.getInstance(algorithm); |
Se genera una clave secreta con el password a utilizar (si el algoritmo es simétrico, es decir, que solo usa una clave) y se inicializa el mismo.
1 2 3 4 |
String pwdStr = new String(password); byte[] keyBytes = pwdStr.getBytes(); SecretKeySpec secretKey = new SecretKeySpec(keyBytes, algorithm); cipher.init(cipherMode, secretKey); |
Si se usa un algoritmo asimétrico (es decir, que usa un par de claves, públic/privada para encriptar/desencriptar) como por ejemplo el RSA, entonces tenemos lo siguiente.
Generamos el par de claves (más adelante veremos como leerlo de un almacen de claves).
1 2 3 4 5 6 7 8 |
/* Generate public and private keys */ if (MyKeyPair.pubKey == null || MyKeyPair.privKey == null) { KeyPairGenerator keyGen = KeyPairGenerator.getInstance(algorithm); keyGen.initialize(1024); // Key size KeyPair kp = keyGen.genKeyPair(); MyKeyPair.pubKey = kp.getPublic(); MyKeyPair.privKey = kp.getPrivate(); } |
Elegimos la clave a usar, bien para encriptar o bien para desencriptar. En este caso usamos la clave pública para encriptar y la privada para desencriptar.
1 2 3 4 5 6 7 8 |
/* Encrypt */ if (cipherMode == Cipher.ENCRYPT_MODE) { cipher.init(cipherMode, MyKeyPair.pubKey); } /* Decrypt */ else if (cipherMode == Cipher.DECRYPT_MODE) { cipher.init(cipherMode, MyKeyPair.privKey); } |
2. Uso del almacén de claves.
El almacen de claves es un mecanismo clásico de Java para almacenar pares de claves y generar certificados de autenticación. Estas claves pueden ser usadas para encriptar y desencriptar como se ha visto anteriormente. La ventaja del almacen de claves es que éstas estarían protegidas en el mismo de forma segura (bajo password) y el almacen de claves puede ser facilmente transportado o compartido en caso de ser necesario. Además varios mecanismos de seguridad en Java usan los almacenes de claves para establecer canales de comunicación seguros, como puede ser en el caso de conexiones SSL.
Para poder acceder al almacen de claves desde la API de JCA basta con realizar lo siguiente. Si se usa un tipo de almacen de claves distinto al creado por defecto (PKCS, etc.) entonces se puede especificar uno diferente:
1 2 3 4 5 6 |
/* Load default type keystore */ KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); /* Load the keystore */ FileInputStream fis = new FileInputStream(keystoreFile); ks.load(fis, passwd); |
Para obtener la clave privada y o la pública de uno de los pares de claves almacenados se invocan las siguientes funciones. Se indica el nombre del par de claves usado al crear el mismo, es decir, el alias que se uso al crearlo.
1 2 3 4 5 |
/* Get the private key */ PrivateKey privKey = (PrivateKey) ks.getKey("mykeystore", passwd); /* Get the public key */ PublicKey pubKey = ks.getCertificate("mykeystore").getPublicKey(); |
3. Generación de claves.
La API de JCA también permite generar claves de forma dinámica. Anteriormente se ha mostrado como generar un par de claves de forma dinmámica para usarlo con algoritmos asimétricos. También se pueden generar claves individuales para los algoritmos simétricos de la siguiente manera:
1 2 3 4 |
KeyGenerator kgen = KeyGenerator.getInstance(algorithm); SecretKey skey = kgen.generateKey(); byte[] raw = skey.getEncoded(); return new SecretKeySpec(raw, algorithm); |
Para aquellos algoritmos que usan “Password Base Encription” o PBE, la cosa sería como sigue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/** Salt for PBE generation. */ private static final byte[] salt = {(byte) 0xc7, (byte) 0x83, (byte) 0x21, (byte) 0x9c, (byte) 0x6e, (byte) 0xc8, (byte) 0xfe, (byte) 0x90 }; /** Iteration count for PBE generation. */ private static final int count = 12; Map<String, Object> pbeMap = new HashMap<String, Object>(); /* Create PBE parameter set */ PBEParameterSpec pbeParamSpec = new PBEParameterSpec(salt, count); pbeMap.put("params", pbeParamSpec); /* Create secrete key */ PBEKeySpec pbeKeySpec = new PBEKeySpec(password); SecretKeyFactory keyFac = SecretKeyFactory.getInstance(algorithm); SecretKey pbeKey = keyFac.generateSecret(pbeKeySpec); pbeMap.put("key", pbeKey); cipher.init(cipherMode, (SecretKey) pbeMap.get("key"), (PBEParameterSpec) pbeMap.get("params")); |
4. Encriptación y desencriptación de ficheros.
La encriptación y desencriptación de ficheros trata el procesamiento del contenido de los mismos. JCA ofrece unas APIs que facilitan la labor adicional de procesar este contenido y almacenar el nuevo contenido generado, bien encriptado o desencriptado.
Los pasos comunes a la encriptación y desencriptación de texto plano sería la inicialización del algoritmo de cifrado, tal y como se ha visto anteriormente.
1 |
Cipher cipher = getCipher(algorithm, password, cipherMode); |
Se usa un buffer para ir leyendo del fichero de entrada de a pocos:
1 |
byte[] buffer = new byte[BUFFER_SIZE]; |
Se instancia la clase que se encargará de encriptar o desencriptar, dependiendo del modo en el que se haya inicializado el algoritmo y al mismo tiempo escribirá la salida en el fichero indicado:
1 2 |
/* Output stream for encrypted file */ CipherOutputStream cos = new CipherOutputStream(new FileOutputStream(dest), cipher); |
Por otro lado, se lee del fichero de entrada en el buffer, y se pasa el buffer al objeto anterior para que se encargue de encriptarlo o desencriptarlo y escribirlo al fichero de salida al mismo tiempo:
1 2 3 4 5 6 7 8 9 10 11 |
/* Read from original file */ FileInputStream fis = new FileInputStream(f); int bytesRead = -1; while ((bytesRead = fis.read(buffer)) != -1) { /* Write encrypted to destination file */ cos.write(buffer, 0, bytesRead); cos.flush(); } fis.close(); cos.close(); |
¡Y ya estaría hecho! Tan solo hay que tener en cuenta que los algoritmos de encriptación asimétricos, los que usan pares de claves, no son aptos para estos menesteres, ya que los bloques de datos que pueden encriptar y desencriptar están limitados por el tamaño de la clave. Por ello lo normal es usar para encriptar ficheros algoritmos simétricos de una sola clave, que no tienen esta limitación, y por ejemplo usar los algoritmos asimétricos, más robustos, para encriptar la clave usada en la encriptación del fichero.
5. Algoritmos de ofuscación de datos (digest).
Los algoritmos de “digestión” de mensajes de texto son mecanismos de ofuscación de información que también pueden ser usados como validadores de integridad de los datos o el texto que se ofusca, ya que el resultado sobre estos debe ser siempre el mismo. Así la salida que se obtiene ha de ser igual, o de lo contrario esto indicará una diferencia en los datos de origen. Entre estos algoritmos se encuentran los “Message Digest” o las funciones “Hash“, por ejemplo. Los primeros son fáciles de usar desde la API de JCA tal que asi:
1 2 3 4 |
/* Digest password */ MessageDigest md = MessageDigest.getInstance(algorithm); md.update(password.getBytes()); byte byteData[] = md.digest(); |
Los datos de salida se pueden representar como texto en alguna codificación concreta como puede ser BASE64 o hexadecimal.
6. Uso de proveedores de algoritmos.
Se pueden visualizar facilmente con que proveedores de algoritmos cuenta nuestro JDK, asi como los algoritmos implementados por el mismo. También es posible incorporar a nuestra aplicación diferentes proveedores de algoritmos de encriptación, que además si usan la misma API de JCA, se integrarán perfectamente con el resto de los existentes.
Para obtener la lista de los nombres de los proveedores existentes se realiza la siguiente operación:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
List<String> providerNames = new ArrayList<String>(); /* Providers */ Provider[] providers = Security.getProviders(); for (Provider prov : providers) { String provName = prov.getName(); providerNames.add(provName); } /* Order list */ Collections.sort(providerNames); return providerNames; |
La lista de los algorimos de cifrado, dado un proveedor se obtendría tal que así:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
List<String> mds = new ArrayList<String>(); Provider[] providers = Security.getProviders(); for (Provider prov : providers) { /* Check provider name if requested */ String provName = prov.getName(); if (providerName != null && !provName.equals(providerName)) { /* Next provider */ continue; } Set<Object> keys = prov.keySet(); for (Object key : keys) { String keyStr = (String) key; if (keyStr.startsWith("Cipher.")) { String name = keyStr.substring("Cipher.".length()); if (!mds.contains(name)) { mds.add(name); } } } } /* Order list */ Collections.sort(mds); return mds; |
La de los algoritmos de “digestión”, de forma similar pero cambiando la etiqueta de lo que se busca:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
List<String> mds = new ArrayList<String>(); Provider[] providers = Security.getProviders(); for (Provider prov : providers) { /* Check provider name if requested */ String provName = prov.getName(); if (providerName != null && !provName.equals(providerName)) { /* Next provider */ continue; } Set<Object> keys = prov.keySet(); for (Object key : keys) { String keyStr = (String) key; if (keyStr.startsWith("MessageDigest.")) { mds.add(keyStr.substring("MessageDigest.".length())); } } } /* Sort it out */ Collections.sort(mds); return mds; |
Por último, para agregar nuevos proveedores a nuestra aplicación, basta con invocar la siguiente API:
1 2 3 |
Security.addProvider((Provider) Class.forName(provClass).newInstance()); // provClass=org.bouncycastle.jce.provider.BouncyCastleProvider |
Pasando la instancia del proveedor (o en este caso el nombre de la clase del proveedor), una vez las librerias del mismos se encuentran en el classpath a disposición del aplicativo, sería suficiente. BouncyCastle es uno de los proveedores clásicos de algoritmos para JCA más famosos.
Se puede descargar el proyecto completo (Jcrypto) para Eclipse desde este enlace. La libreria de BC usada esta aquí.
NOTA
Se puede obtener la versión usada (u otra) de la librería de Bouncy Castle mediante una dependencia de Maven (está disponible en el repositorio de mvnrepository). La dependencia usada en este caso es:
1 2 3 4 5 |
<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.54</version> </dependency> |
Además, se ha subido el código, pom.xml y el proyecto de Eclipse entero al repositorio enlazado de Github, para mejor gestión y mayor facilidad a la hora de compartir. Se aconseja usar esta fuente como recurso para obtener el proyecto y los ejemplos descritos, los cuales se han revisado y se han incluido ligeras mejoras.