Implementación de Cifrado y Descifrado AES/ECB/PKCS5Padding en Java

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:

  1. 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).
  2. 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);
        }
    }
}

Etiquetas: java AES ECB PKCS5Padding JCA

Publicado el 7-12 04:03