Encriptación y desencriptación AES de Java

imagen del autor
Por mkyong | Última actualización: 2 de junio de 2020
Visto: 64,491 | +1,553 pv/w

Encriptación AES

El Estándar de Encriptación Avanzada (AES, Rijndael) es un algoritmo de encriptación y desencriptación por bloques, el más utilizado en el mundo. El AES procesa bloques de 128 bits utilizando una clave secreta de 128, 192 o 256 bits.

Este artículo le muestra algunos ejemplos de cifrado y descifrado de AES en Java:

  • Cifrado de cadenas AES – (cifrar y descifrar una cadena).
  • Cifrado basado en contraseña AES – (La clave secreta derivará de una contraseña dada).
  • Cifrado de archivos AES. (basado en contraseña).
    • En este artículo, nos centramos en el cifrado AES de 256 bits con el Modo Contador de Galois (GCM).

GCM = CTR + Authentication.

Lectura adicional
Lea esto – NIST – Recomendación para el modo Galois/Contador (GCM)

No utilice el modo AES Electronic codebook (ECB)
El modo AES ECB, o AES/ECB/PKCS5Padding (en Java) no es semánticamente seguro – El texto cifrado ECB puede filtrar información sobre el texto plano. Aquí hay una discusión sobre ¿Por qué no debería usar el cifrado ECB?

Entradas de cifrado de Java y AES.

En el cifrado y descifrado AES, necesitamos las siguientes entradas:

Mejores prácticas de cifrado AES
No reutilizar el IV con la misma clave.

1.1 El IV (valor inicial o vector inicial), son bytes aleatorios, normalmente 12 bytes o 16 bytes. En Java, podemos utilizar SecureRandom para generar el IV aleatorio.

 // 16 bytes IV public static byte getRandomNonce() { byte nonce = new byte; new SecureRandom().nextBytes(nonce); return nonce; } // 12 bytes IV public static byte getRandomNonce() { byte nonce = new byte; new SecureRandom().nextBytes(nonce); return nonce; }

1.2 La clave secreta AES, bien AES-128 o AES-256. En Java, podemos utilizar KeyGenerator para generar la clave secreta AES.

 // 256 bits AES secret key public static SecretKey getAESKey() throws NoSuchAlgorithmException { KeyGenerator keyGen = KeyGenerator.getInstance("AES"); keyGen.init(256, SecureRandom.getInstanceStrong()); return keyGen.generateKey(); }

1.3 La clave secreta AES que se deriva de una contraseña dada. En Java, podemos utilizar el SecretKeyFactory y PBKDF2WithHmacSHA256 para generar una clave AES a partir de una contraseña dada.

 // AES key derived from a password public static SecretKey getAESKeyFromPassword(char password, byte salt) throws NoSuchAlgorithmException, InvalidKeySpecException { SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); // iterationCount = 65536 // keyLength = 256 KeySpec spec = new PBEKeySpec(password, salt, 65536, 256); SecretKey secret = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES"); return secret; }

Usamos salt para proteger los ataques de arcoíris, y también es un byte aleatorio, podemos usar el mismo 1.1 getRandomNonce para generarlo.

1.4 Agrupamos los métodos anteriores en una única clase util, para no repetir el mismo código una y otra vez.

CryptoUtils.java
package com.mkyong.crypto.utils;import javax.crypto.KeyGenerator;import javax.crypto.SecretKey;import javax.crypto.SecretKeyFactory;import javax.crypto.spec.PBEKeySpec;import javax.crypto.spec.SecretKeySpec;import java.security.NoSuchAlgorithmException;import java.security.SecureRandom;import java.security.spec.InvalidKeySpecException;import java.security.spec.KeySpec;import java.util.ArrayList;import java.util.List;public class CryptoUtils { public static byte getRandomNonce(int numBytes) { byte nonce = new byte; new SecureRandom().nextBytes(nonce); return nonce; } // AES secret key public static SecretKey getAESKey(int keysize) throws NoSuchAlgorithmException { KeyGenerator keyGen = KeyGenerator.getInstance("AES"); keyGen.init(keysize, SecureRandom.getInstanceStrong()); return keyGen.generateKey(); } // Password derived AES 256 bits secret key public static SecretKey getAESKeyFromPassword(char password, byte salt) throws NoSuchAlgorithmException, InvalidKeySpecException { SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); // iterationCount = 65536 // keyLength = 256 KeySpec spec = new PBEKeySpec(password, salt, 65536, 256); SecretKey secret = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES"); return secret; } // hex representation public static String hex(byte bytes) { StringBuilder result = new StringBuilder(); for (byte b : bytes) { result.append(String.format("%02x", b)); } return result.toString(); } // print hex with block size split public static String hexWithBlockSize(byte bytes, int blockSize) { String hex = hex(bytes); // one hex = 2 chars blockSize = blockSize * 2; // better idea how to print this? List<String> result = new ArrayList<>(); int index = 0; while (index < hex.length()) { result.add(hex.substring(index, Math.min(index + blockSize, hex.length()))); index += blockSize; } return result.toString(); }}

Encriptación y desencriptación AES.

El AES-GSM es el cifrado autenticado más utilizado. Este ejemplo cifrará y descifrará una cadena utilizando AES de 256 bits en modo contador de Galois (GCM).

Las entradas de AES-GCM:

  • Clave secreta de AES (256 bits)
  • IV – 96 bits (12 bytes)
  • Longitud (en bits) de la etiqueta de autentificación – 128 bits (16 bytes)
  • 2.1 En Java, utilizamos AES/GCM/NoPadding para representar el algoritmo AES-GCM. Para la salida encriptada, anteponemos el IV de 16 bytes al texto encriptado (texto cifrado), porque necesitamos el mismo IV para el descifrado.

    ¿Esto está bien si el IV es conocido públicamente?
    Está bien que el IV sea conocido públicamente, el único secreto es la clave, manténgala privada y confidencial.

    Este ejemplo utilizará AES para cifrar un texto plano Hello World AES-GCM y posteriormente descifrarlo de nuevo al texto plano original.

    EncryptorAesGcm.java
package com.mkyong.crypto.encryptor;import com.mkyong.crypto.utils.CryptoUtils;import javax.crypto.Cipher;import javax.crypto.SecretKey;import javax.crypto.spec.GCMParameterSpec;import java.nio.ByteBuffer;import java.nio.charset.Charset;import java.nio.charset.StandardCharsets;/** * AES-GCM inputs - 12 bytes IV, need the same IV and secret keys for encryption and decryption. * <p> * The output consist of iv, encrypted content, and auth tag in the following format: * output = byte {i i i c c c c c c ...} * <p> * i = IV bytes * c = content bytes (encrypted content, auth tag) */public class EncryptorAesGcm { private static final String ENCRYPT_ALGO = "AES/GCM/NoPadding"; private static final int TAG_LENGTH_BIT = 128; private static final int IV_LENGTH_BYTE = 12; private static final int AES_KEY_BIT = 256; private static final Charset UTF_8 = StandardCharsets.UTF_8; // AES-GCM needs GCMParameterSpec public static byte encrypt(byte pText, SecretKey secret, byte iv) throws Exception { Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO); cipher.init(Cipher.ENCRYPT_MODE, secret, new GCMParameterSpec(TAG_LENGTH_BIT, iv)); byte encryptedText = cipher.doFinal(pText); return encryptedText; } // prefix IV length + IV bytes to cipher text public static byte encryptWithPrefixIV(byte pText, SecretKey secret, byte iv) throws Exception { byte cipherText = encrypt(pText, secret, iv); byte cipherTextWithIv = ByteBuffer.allocate(iv.length + cipherText.length) .put(iv) .put(cipherText) .array(); return cipherTextWithIv; } public static String decrypt(byte cText, SecretKey secret, byte iv) throws Exception { Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO); cipher.init(Cipher.DECRYPT_MODE, secret, new GCMParameterSpec(TAG_LENGTH_BIT, iv)); byte plainText = cipher.doFinal(cText); return new String(plainText, UTF_8); } public static String decryptWithPrefixIV(byte cText, SecretKey secret) throws Exception { ByteBuffer bb = ByteBuffer.wrap(cText); byte iv = new byte; bb.get(iv); //bb.get(iv, 0, iv.length); byte cipherText = new byte; bb.get(cipherText); String plainText = decrypt(cipherText, secret, iv); return plainText; } public static void main(String args) throws Exception { String OUTPUT_FORMAT = "%-30s:%s"; String pText = "Hello World AES-GCM, Welcome to Cryptography!"; // encrypt and decrypt need the same key. // get AES 256 bits (32 bytes) key SecretKey secretKey = CryptoUtils.getAESKey(AES_KEY_BIT); // encrypt and decrypt need the same IV. // AES-GCM needs IV 96-bit (12 bytes) byte iv = CryptoUtils.getRandomNonce(IV_LENGTH_BYTE); byte encryptedText = EncryptorAesGcm.encryptWithPrefixIV(pText.getBytes(UTF_8), secretKey, iv); System.out.println("\n------ AES GCM Encryption ------"); System.out.println(String.format(OUTPUT_FORMAT, "Input (plain text)", pText)); System.out.println(String.format(OUTPUT_FORMAT, "Key (hex)", CryptoUtils.hex(secretKey.getEncoded()))); System.out.println(String.format(OUTPUT_FORMAT, "IV (hex)", CryptoUtils.hex(iv))); System.out.println(String.format(OUTPUT_FORMAT, "Encrypted (hex) ", CryptoUtils.hex(encryptedText))); System.out.println(String.format(OUTPUT_FORMAT, "Encrypted (hex) (block = 16)", CryptoUtils.hexWithBlockSize(encryptedText, 16))); System.out.println("\n------ AES GCM Decryption ------"); System.out.println(String.format(OUTPUT_FORMAT, "Input (hex)", CryptoUtils.hex(encryptedText))); System.out.println(String.format(OUTPUT_FORMAT, "Input (hex) (block = 16)", CryptoUtils.hexWithBlockSize(encryptedText, 16))); System.out.println(String.format(OUTPUT_FORMAT, "Key (hex)", CryptoUtils.hex(secretKey.getEncoded()))); String decryptedText = EncryptorAesGcm.decryptWithPrefixIV(encryptedText, secretKey); System.out.println(String.format(OUTPUT_FORMAT, "Decrypted (plain text)", decryptedText)); }}

Salida

Texto plano : Hello World AES-GCM

Terminal
------ AES GCM Encryption ------Input (plain text) :Hello World AES-GCMKey (hex) :603d87185bf855532f14a77a91ec7b025c004bf664e9f5c6e95613ee9577f436IV (hex) :bdb271ce5235996a0709e09cEncrypted (hex) :bdb271ce5235996a0709e09c2d03eefe319e9329768724755c56291aecaef88cd1e6bdf72b8c7b54d75a94e66b0cd3Encrypted (hex) (block = 16) :------ AES GCM Decryption ------Input (hex) :bdb271ce5235996a0709e09c2d03eefe319e9329768724755c56291aecaef88cd1e6bdf72b8c7b54d75a94e66b0cd3Input (hex) (block = 16) :Key (hex) :603d87185bf855532f14a77a91ec7b025c004bf664e9f5c6e95613ee9577f436Decrypted (plain text) :Hello World AES-GCM

Texto plano : Hello World AES-GCM, Welcome to Cryptography!

Terminal
------ AES GCM Encryption ------Input (plain text) :Hello World AES-GCM, Welcome to Cryptography!Key (hex) :ddc24663d104e1c2f81f11aef98156503dafdc435f81e3ac3d705015ebab095cIV (hex) :b05d6aedf023f73b9e1e2d11Encrypted (hex) :b05d6aedf023f73b9e1e2d11f6f5137d971aea8c5cdd5b045e0960eb4408e0ee4635cccc2dfeec2c13a89bd400f659be82dc2329e9c36e3b032f38bd42296a8495ac840b0625c097d9Encrypted (hex) (block = 16) :------ AES GCM Decryption ------Input (hex) :b05d6aedf023f73b9e1e2d11f6f5137d971aea8c5cdd5b045e0960eb4408e0ee4635cccc2dfeec2c13a89bd400f659be82dc2329e9c36e3b032f38bd42296a8495ac840b0625c097d9Input (hex) (block = 16) :Key (hex) :ddc24663d104e1c2f81f11aef98156503dafdc435f81e3ac3d705015ebab095cDecrypted (plain text) :Hello World AES-GCM, Welcome to Cryptography!

Encriptación y desencriptación basada en contraseña AES.

Para el cifrado basado en contraseñas, podemos utilizar la especificación de criptografía basada en contraseñas (PKCS), definida en el RFC 8018, para generar una clave a partir de una contraseña dada.

Para las entradas PKCS:

  • Contraseña, se proporciona esto.
  • Sal – Al menos 64 bits (8 bytes) aleatorios.
  • Cuento de iteraciones – Se recomienda un recuento mínimo de iteraciones de 1.000.
  • ¿Qué es la sal y el recuento de iteraciones?

    • El salt produce un amplio conjunto de claves para una contraseña dada. Por ejemplo, si la sal es de 128 bits, habrá hasta 2^128 claves para cada contraseña. Por lo tanto, aumenta la dificultad de los ataques rainbow. Además, la tabla rainbow que los atacantes construyen para la contraseña de un usuario se vuelve inútil para otro usuario.
    • El iteration count aumenta el coste de producir claves a partir de una contraseña, por lo que aumenta la dificultad y ralentiza la velocidad de los ataques.

    3.1 Para la salida cifrada, anteponemos el 12 bytes IV y el password salt al texto cifrado, porque necesitamos el mismo IV y la sal de la contraseña (para la clave secreta) para el descifrado. Además, utilizamos Base64 codificador para codificar el texto encriptado en una representación de cadena, de modo que podemos enviar el texto encriptado o el texto cifrado en formato de cadena (era matriz de bytes).

    ¿Esto está bien si la sal de la contraseña es conocida públicamente?
    Es lo mismo que el IV, y está bien que la sal de la contraseña sea conocida públicamente, el único secreto es la clave, manteniéndola privada y confidencial.

    EncryptorAesGcmPassword.java
package com.mkyong.crypto.encryptor;import com.mkyong.crypto.utils.CryptoUtils;import javax.crypto.Cipher;import javax.crypto.SecretKey;import javax.crypto.spec.GCMParameterSpec;import java.nio.ByteBuffer;import java.nio.charset.Charset;import java.nio.charset.StandardCharsets;import java.util.Base64;/** * AES-GCM inputs - 12 bytes IV, need the same IV and secret keys for encryption and decryption. * <p> * The output consist of iv, password's salt, encrypted content and auth tag in the following format: * output = byte {i i i s s s c c c c c c ...} * <p> * i = IV bytes * s = Salt bytes * c = content bytes (encrypted content) */public class EncryptorAesGcmPassword { private static final String ENCRYPT_ALGO = "AES/GCM/NoPadding"; private static final int TAG_LENGTH_BIT = 128; // must be one of {128, 120, 112, 104, 96} private static final int IV_LENGTH_BYTE = 12; private static final int SALT_LENGTH_BYTE = 16; private static final Charset UTF_8 = StandardCharsets.UTF_8; // return a base64 encoded AES encrypted text public static String encrypt(byte pText, String password) throws Exception { // 16 bytes salt byte salt = CryptoUtils.getRandomNonce(SALT_LENGTH_BYTE); // GCM recommended 12 bytes iv? byte iv = CryptoUtils.getRandomNonce(IV_LENGTH_BYTE); // secret key from password SecretKey aesKeyFromPassword = CryptoUtils.getAESKeyFromPassword(password.toCharArray(), salt); Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO); // ASE-GCM needs GCMParameterSpec cipher.init(Cipher.ENCRYPT_MODE, aesKeyFromPassword, new GCMParameterSpec(TAG_LENGTH_BIT, iv)); byte cipherText = cipher.doFinal(pText); // prefix IV and Salt to cipher text byte cipherTextWithIvSalt = ByteBuffer.allocate(iv.length + salt.length + cipherText.length) .put(iv) .put(salt) .put(cipherText) .array(); // string representation, base64, send this string to other for decryption. return Base64.getEncoder().encodeToString(cipherTextWithIvSalt); } // we need the same password, salt and iv to decrypt it private static String decrypt(String cText, String password) throws Exception { byte decode = Base64.getDecoder().decode(cText.getBytes(UTF_8)); // get back the iv and salt from the cipher text ByteBuffer bb = ByteBuffer.wrap(decode); byte iv = new byte; bb.get(iv); byte salt = new byte; bb.get(salt); byte cipherText = new byte; bb.get(cipherText); // get back the aes key from the same password and salt SecretKey aesKeyFromPassword = CryptoUtils.getAESKeyFromPassword(password.toCharArray(), salt); Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO); cipher.init(Cipher.DECRYPT_MODE, aesKeyFromPassword, new GCMParameterSpec(TAG_LENGTH_BIT, iv)); byte plainText = cipher.doFinal(cipherText); return new String(plainText, UTF_8); } public static void main(String args) throws Exception { String OUTPUT_FORMAT = "%-30s:%s"; String PASSWORD = "this is a password"; String pText = "AES-GSM Password-Bases encryption!"; String encryptedTextBase64 = EncryptorAesGcmPassword.encrypt(pText.getBytes(UTF_8), PASSWORD); System.out.println("\n------ AES GCM Password-based Encryption ------"); System.out.println(String.format(OUTPUT_FORMAT, "Input (plain text)", pText)); System.out.println(String.format(OUTPUT_FORMAT, "Encrypted (base64) ", encryptedTextBase64)); System.out.println("\n------ AES GCM Password-based Decryption ------"); System.out.println(String.format(OUTPUT_FORMAT, "Input (base64)", encryptedTextBase64)); String decryptedText = EncryptorAesGcmPassword.decrypt(encryptedTextBase64, PASSWORD); System.out.println(String.format(OUTPUT_FORMAT, "Decrypted (plain text)", decryptedText)); }}

Salida

Terminal
------ AES GCM Password-based Encryption ------Input (plain text) :AES-GSM Password-Bases encryption!Encrypted (base64) :KmrvjnMusJTQo/hB7T5BvlQpvi3bVbdjpZP51NT7I/enrIfSQuDfSK6iXgdPzvUP2IE54mwrKiyHqMkG8224lRZ9tXHcclmdh98I8b3B------ AES GCM Password-based Decryption ------Input (base64) :KmrvjnMusJTQo/hB7T5BvlQpvi3bVbdjpZP51NT7I/enrIfSQuDfSK6iXgdPzvUP2IE54mwrKiyHqMkG8224lRZ9tXHcclmdh98I8b3BDecrypted (plain text) :AES-GSM Password-Bases encryption!

3.2 Si la contraseña no coincide, Java lanza AEADBadTagException: Tag mismatch!

 // change the password to something else String decryptedText = EncryptorAesGcmPassword.decrypt(encryptedTextBase64, "other password"); System.out.println(String.format(OUTPUT_FORMAT, "Decrypted (plain text)", decryptedText));

Salida

Terminal
Exception in thread "main" javax.crypto.AEADBadTagException: Tag mismatch! at java.base/com.sun.crypto.provider.GaloisCounterMode.decryptFinal(GaloisCounterMode.java:623) at java.base/com.sun.crypto.provider.CipherCore.finalNoPadding(CipherCore.java:1118) at java.base/com.sun.crypto.provider.CipherCore.fillOutputBuffer(CipherCore.java:1055) at java.base/com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:855) at java.base/com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:446) at java.base/javax.crypto.Cipher.doFinal(Cipher.java:2207) at com.mkyong.crypto.encryptor.EncryptorAesGcmPassword.decrypt(EncryptorAesGcmPassword.java:88) at com.mkyong.crypto.encryptor.EncryptorAesGcmPassword.main(EncryptorAesGcmPassword.java:109)

Encriptación y desencriptación de archivos AES.

Este ejemplo es una encriptación de archivos con contraseña AES. Las ideas son las mismas, pero necesitamos algunas clases IO para trabajar con los recursos o archivos.

Aquí tenemos un archivo de texto, en la carpeta resources.

readme.txt
This is line 1.This is line 2.This is line 3.This is line 4.This is line 5.This is line 9.This is line 10.

4.1 Este ejemplo es similar a 3.1 EncryptorAesGcmPassword.java, con algunos cambios menores como devolver un byte en lugar de una cadena codificada en base64.

 public static byte encrypt(byte pText, String password) throws Exception { //... // prefix IV and Salt to cipher text byte cipherTextWithIvSalt = ByteBuffer.allocate(iv.length + salt.length + cipherText.length) .put(iv) .put(salt) .put(cipherText) .array(); // it works, even if we save the based64 encoded string into a file. // return Base64.getEncoder().encodeToString(cipherTextWithIvSalt); // we save the byte into a file. return cipherTextWithIvSalt; }

Añadir encryptFile y decryptFile para trabajar con el archivo.

 public static void encryptFile(String fromFile, String toFile, String password) throws Exception { // read a normal txt file byte fileContent = Files.readAllBytes(Paths.get(ClassLoader.getSystemResource(fromFile).toURI())); // encrypt with a password byte encryptedText = EncryptorAesGcmPasswordFile.encrypt(fileContent, password); // save a file Path path = Paths.get(toFile); Files.write(path, encryptedText); } public static byte decryptFile(String fromEncryptedFile, String password) throws Exception { // read a file byte fileContent = Files.readAllBytes(Paths.get(fromEncryptedFile)); return EncryptorAesGcmPasswordFile.decrypt(fileContent, password); }

4.2 Leer el archivo anterior readme.txt del classpath, cifrarlo, y los datos cifrados a un nuevo archivo c:\test\readme.encrypted.txt.

 String password = "password123"; String fromFile = "readme.txt"; // from resources folder String toFile = "c:\\test\\readme.encrypted.txt"; // encrypt file EncryptorAesGcmPasswordFile.encryptFile(fromFile, toFile, password);

Salida

Encriptación de archivos AES

4.3 Leer el archivo encriptado, desencriptarlo e imprimir la salida.

 String password = "password123"; String toFile = "c:\\test\\readme.encrypted.txt"; // decrypt file byte decryptedText = EncryptorAesGcmPasswordFile.decryptFile(toFile, password); String pText = new String(decryptedText, UTF_8); System.out.println(pText);

Salida

Terminal
This is line 1.This is line 2.This is line 3.This is line 4.This is line 5.This is line 9.This is line 10.

P.S El cifrado de imágenes AES es el mismo concepto.

Descarga el código fuente

$ git clone https://github.com/mkyong/core-java

$ cd java-crypto

Hazme saber si el artículo necesita mejoras. Gracias.

  • Wikipedia – Cipher JavaDoc
  • Wikipedia – Cipher Block Chaining (CBC)
  • Wikipedia – Galois/Counter Mode (GCM)
  • Oracle – KeyGenerator Algorithms JavaDoc
  • Java – ¿Cómo generar 12 bytes aleatorios?
  • ¿Por qué no debo utilizar el cifrado BCE?
  • Módulo Crypto de Spring Security
  • Wikipedia – PBKDF2
  • RFC 8018 – PKCS
  • Java – Cómo unir y dividir una matriz de bytes
  • Estándar de seguridad de Java. Nombres de Algoritmos
  • NIST – Recomendación para el Modo Galois/Contador (GCM)
  • imagen del autor

    mkyong

    Fundador de Mkyong.com, amante de Java y de las cosas de código abierto. Síguelo en Twitter. Si te gustan mis tutoriales, considera hacer una donación a estas organizaciones benéficas.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *