En el desarrollo de aplicaciones seguras, es frecuente necesitar proteger la confidencialidad de los datos. A continuación, se detalla la construcción de una clase de utilidad en Java para realizar operaciones de cifrado y descifrado utilizando el algoritmo AES, configurado con el modo ECB y el esquema de relleno PKCS5Padding.
Configuración Criptgoráfica
- Algoritmo: AES (Advanced Encryption Standard)
- Modo de operación: ECB (Electronic Codebook)
- Esquema de relleno: PKCS5Padding
Clase de Utilidad Criptográfica
Para evitar dependencias externas innecesarias, esta implementación utiliza las API nativas de Java (como java.util.Base64 y java.nio.charset.StandardCharsets). La clase está diseñada como una utilidad sin estado, donde la clave secreta se pasa como parámetro en cada operación.
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class AesEcbCryptoUtil {
private static final String ALGORITHM = "AES";
private static final String TRANSFORMATION = "AES/ECB/PKCS5Padding";
/**
* Cifra el texto plano y devuelve el resultado codificado en Base64.
*/
public static String cipherToBase64(String plainText, String secretKey) throws Exception {
byte[] encryptedBytes = cipherToBytes(plainText, secretKey);
return Base64.getEncoder().encodeToString(encryptedBytes);
}
/**
* Cifra el texto plano y devuelve el array de bytes crudo.
*/
public static byte[] cipherToBytes(String plainText, String secretKey) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), ALGORITHM);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
return cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
}
/**
* Descifra un texto cifrado que está codificado en Base64.
*/
public static String decipherFromBase64(String base64CipherText, String secretKey) throws Exception {
byte[] decodedBytes = Base64.getDecoder().decode(base64CipherText);
return decipherFromBytes(decodedBytes, secretKey);
}
/**
* Descifra un array de bytes crudo y devuelve el texto plano original.
*/
public static String decipherFromBytes(byte[] cipherBytes, String secretKey) throws Exception {
if (secretKey == null || secretKey.isEmpty()) {
throw new IllegalArgumentException("La clave secreta no puede ser nula o vacía");
}
SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), ALGORITHM);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[] originalBytes = cipher.doFinal(cipherBytes);
return new String(originalBytes, StandardCharsets.UTF_8);
}
}
Formatos de Salida: Base64 vs Bytes Crudos
La utilidad proporciona dos enfoques para manejar los datos cifrados:
- Codificación Base64: Convierte los bytes cifrados en una cadena de texto segura para su transmisión o almacenamiento en formatos que solo aceptan texto (como JSON o XML).
- Array de Bytes: Mantiene los datos en su forma binaria original, lo cual es ideal para escribir directamente en archivos, bases de datos BLOB o flujos de red.
Casos de Prueba
A continuación, se presentan pruebas unitarias que validan ambos enfoques. Es crucial utilizar una clave de longitud válida para AES (16, 24 o 32 bytes).
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class AesEcbCryptoUtilTest {
// Clave de 16 bytes para AES-128
private final String secretKey = "1234567890123456";
@Test
public void testBase64EncryptionAndDecryption() throws Exception {
String originalText = "Mensaje secreto de prueba";
String encryptedBase64 = AesEcbCryptoUtil.cipherToBase64(originalText, secretKey);
String decryptedText = AesEcbCryptoUtil.decipherFromBase64(encryptedBase64, secretKey);
assertEquals(originalText, decryptedText);
}
@Test
public void testByteArrayEncryptionAndDecryption() throws Exception {
String originalText = "Otro mensaje secreto";
byte[] encryptedBytes = AesEcbCryptoUtil.cipherToBytes(originalText, secretKey);
String decryptedText = AesEcbCryptoUtil.decipherFromBytes(encryptedBytes, secretKey);
assertEquals(originalText, decryptedText);
}
}
El Peligro de la Conversión Directa entre Bytes y Strings
Un error común al manejar datos cifrados es intentar convertir el array de bytes resultante directamente a un String utilizando codificaciones estándar como UTF-8, para luego intentar revertirlo. Esto provocará un fallo en el descifrado.
// Ejemplo de lo que NO se debe hacer:
byte[] encryptedBytes = AesEcbCryptoUtil.cipherToBytes("Texto", secretKey);
String wrongString = new String(encryptedBytes, StandardCharsets.UTF_8);
byte[] corruptedBytes = wrongString.getBytes(StandardCharsets.UTF_8);
// Esto lanzará una excepción de relleno incorrecto (BadPaddingException)
AesEcbCryptoUtil.decipherFromBytes(corruptedBytes, secretKey);
¿Por qué ocurre esto?
Las secuencias de bytes generadas por algoritmos de cifrado son esencialmente aleatorias y rara vez forman secuencias de caracteres UTF-8 válidas. Al forzar la conversión a String, el codificador de caracteres reemplaza los bytes inválidos con caracteres de reemplazo (como U+FFFD), destruyendo la información original de forma irreversible. Por este motivo, siempre se debe utilizar Base64 o codificación Hexadecimal cuando sea necesario representar datos binarios como texto.
Descifrado de Archivos Binarios Remotos
En escenarios del mundo real, a menudo es necesario descargar un archivo binario cifrado desde un servidor y descifrarlo en memoria. El siguiente ejemplo demuestra cómo leer un flujo de red y procesarlo directamente como bytes.
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
public class RemoteFileDecryptor {
public String decryptRemoteFile(String fileUrl, String secretKey) throws Exception {
URL url = new URL(fileUrl);
try (InputStream inputStream = url.openStream()) {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int nRead;
byte[] data = new byte[4096];
while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, nRead);
}
byte[] fileBytes = buffer.toByteArray();
return AesEcbCryptoUtil.decipherFromBytes(fileBytes, secretKey);
}
}
}