Cifrado y descifrado RSA con 4js Genero y Java

Published on

in

, , , ,

Espero que esta crónica sea de su interés y que les ahorre dolores de cabeza, horas de lectura y pruebas.

If you want to read this in English, click here.

Todo comienza con un requerimiento: cifrar la respuesta de un web service hecho en 4js Genero y cuyo producto será leído, y por consiguiente descifrado, en un programa hecho en Java, utilizando llaves RSA.

Contrario a lo que inicialmente pensé, por estadística y experiencia, la parte más sencilla fue con Genero. Aquí tuve que hacer convivir tres tecnologías:

  • openssl para generar las llaves públicas y privadas de tipo RSA
  • Genero para usar dichas llaves con el fin de cifrar, y descrifrar -para pruebas- lo que se cifró con la llave pública.
  • Java para retomar las cadenas cifradas y usar la llave privada para descifrarla.

Vamos paso por paso con cada una.

Crear las llaves RSA

RSA es un algoritmo de cifrado que utiliza un par de llaves, una pública y una privada. Para efectos de resolver el requerimiento, la llave pública se usará para cifrar cadenas de texto y la privada para descifrarla.

Genero, a diferencia de Java, no cuenta con elementos en su lenguaje para crear las llaves, de tal suerte que decidí crearlas usando openssl, herramienta con la que sí cuento en el ambiente de desarrollo y que existe también en Linux, donde desplegaré.

Inicialmente generé el par de llaves RSA en formato PEM, siguiendo los pasos listados en esta guía:

https://rietta.com/blog/openssl-generating-rsa-key-from-command/

Pero después tuve que regenerarlas porque el programa Java con el que pretendía probar el descifrado necesita la llave en formato PKCS8, de tal suerte que entonces seguí esta guía:

https://kb.vander.host/security/how-to-generate-rsa-public-and-private-key-pair-in-pkcs8-format/

Cifrar y descifrar con Genero

Como comenté anteriormente, Genero (sorprendiéndome nuevamente, porque en general es algo engorroso y limitado) puede trabajar con llaves RSA en formato PEM o PKCS8, sin necesidad de indicar en sus funciones que se trata de uno u otro formato.

Las funciones de cifrado están en la biblioteca XML. No es necesario instanciar clase alguna, la biblioteca tiene una clase estática Encryption que expone los métodos:

RSAEncrypt(key, source_string) RETURNING encoded_string

RSADecrypt(key, encoded_string) RETURNING decoded_string

key es de tipo STRING y contiene la ruta específica o relativa donde se encuentra la llave RSA.

source_string es de tipo STRING y contiene la cadena que se desea cifrar.

encoded_string es de tipo STRING y contiene la cadena cifrada que se desea cifar.

Evidentemente la salida de ambos métodos es de tipo STRING y se trata de la cadena cifrada y descifrada correspondientemente.

Este es un programa de demostración, la cadena que deseo cifrar es «3200.00» que se deja en código fijo así como la ubicación de las llaves pública y privadas. Nota que la llave pública se usa para cifrar y la privada para descifrar tal cual se explicó en la sección anterior:

IMPORT XML

MAIN
DEFINE  v_archivo_rsa          STRING
       ,v_archivo_rsa_privado  STRING
       ,v_cadena_base          STRING
       ,v_cadena_encriptada    STRING
       ,v_cadena_reversada     STRING

   DISPLAY "Invocando cifrado de cadena con RSA"
   LET v_archivo_rsa = "publickey.crt"
   LET v_archivo_rsa_privado = "pkcs8.key"
   LET v_cadena_base = "3200.00"

   LET v_cadena_encriptada = xml.Encryption.RSAEncrypt(v_archivo_rsa, v_cadena_base)

   DISPLAY "Cadena cifrada: ", v_cadena_encriptada

   DISPLAY "Invocando descifrado de cadena con RSA"

   LET v_cadena_reversada = xml.Encryption.RSADecrypt(v_archivo_rsa_privado, v_cadena_encriptada)

   DISPLAY "Cadena descifrada: ", v_cadena_reversada

END MAIN 

Esta es la salida de dicho programa:

Invocando cifrado de cadena con RSA
Cadena cifrada: dGro1pkl2q0dvginhXw6qOnwdI5tTqtt+l+GQHi73tSmWzGXERBpGkLlJnZAnAPx0goFyNbtHJ0EgR5xLOB5WHCXuYaOpakbjw4wOdpIALBi5bqzA9Zvx7HC9bXQaRyBtENZqrqCiG46Qz22qofOoXpB1q5bKD0JTVSwBIfVtHaWjZGNSDLWqb8NT3J6mGur9C7DEcQVtwHlodt/GseH6QGGtYx0+1cg/w+OF3pks1TfVDHUXirdAcofdzAyGLeimRQAaccJogEoH7euvHsIoIF9A19v+K4MpkQVymBdR95m/X6zRb2yPaLnL8tRPaQRHb+itDT4E/9/ymq0XJVaIw==
Invocando descifrado de cadena con RSA
Cadena descifrada: 3200.00

Como se aprecia la cadena cifrada es una cadena mucho más larga que la cadena original y se encuentra expresada en Base64.

Hasta aquí, todo bien. Ahora vamos al talón de Aquiles… Java.

Descifrar la cadena en Java

Esta parte se me antojaba la más rápida y más sencilla, porque… es Java. Tú, querida lectora/querido lector, no me dejarás mentir, esta plataforma cuenta con toda suerte de herramientas, usar una llave RSA debería ser muy sencillo. Debería ser algo muy similar, instanciar alguna clase que tome la llave privada, se le entregue la cadena y la descifre, ¿no?

Antes de ejecutar la prueba con Java, decidí intentar probar con las llaves en algún servicio web. Esta es la página que encontré más fácil de usar:

https://www.devglan.com/online-tools/rsa-encryption-decryption

Curiosamente ese sitio en su back end utiliza Java, así que me agradó tomarlo como prueba de vuelo. ¿Cómo supe que usaba Java? Muy sencillo, al usarlo, causé un error y en lugar de obtener la cadena cifrada o descifrada que quería, recibí una excepción de Java.

Bueno, intenté cifrar la misma cadena usando la llave pública RSA. Para fines de demostración y práctica, compartiré ambas llaves. No las usen para algo productivo.

Esta es la llave pública:

Copié y pegué el texto en el recuadro para dicha llave en la sección de cifrado del sitio web:

Y como puedes apreciar, aquí empezaron mis problemas.

¿Por qué Genero sí pudo cifrarlo con la llave así y este sitio web (que usa Java en su backend) no?

Aquí es donde te ahorraré horas de búsqueda y lectura (si es que encontraste mi crónica primero). Hay que remover el encabezado y pie de página:

-----BEGIN PUBLIC KEY-----
-----END PUBLIC KEY-----

Entonces la llave «ajustada» ya en el sitio web debe verse así:

Y como se puede apreciar, en el apartado final, en lugar de tener una excepción, obtuve una cadena en Base64:

bQALzE/FVRXIafWrDh7Ollw8Vn3AxhQDj9kPnHN2tFZqhVgpfeR1Ue+Hs/4+SUipHT0VH9jjk4ipcN5OMpTtpHd0S52+D5haIn+BlnhzdwyTMvaBszwBfEztKhRWWH9yEake8ffCYZwl030FU8tCJuZQ9B3sALAHn4CuiPdb0YAkJMvY19EfjPIAQ+Zr4zsuc4dbFByO/XeeshWUodzNuWqstBPehq3nQG43ne8JRKfy6+mq0VAzU2ujhdlEzkIGQqPoflwV0R6zROtiYp98jStvD337cGd3SNSE8T2Jx6YFnTtEIiBUSVXVIvx5lQs55HVQecmc+yoFWjyUKNCz6w==

El paso siguiente era, naturalmente, descifrar esa cadena usando la llave privada, así que tomé la misma y la copié y pegué en el apartado correspondiente:

Otra vez el error que tuve con la llave pública. En Genero tampoco fue necesario quitar el encabezado y pie de página de la llave. Procedí a removerlos y probar de nuevo:

Perfecto. Hasta ahí todo bien. Habiendo validado que este sitio web sí podía descifrar la cadena, procedí con mi ejemplo en Java. Este es el código fuente:

import java.io.FileInputStream;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;

import javax.crypto.Cipher;

public class RSADecode {

  public static void main(String[] args) throws Exception {
      
    String privateKeyFile = "pkcs8.key";
        String ciphertext = "RQ4yuY7m1bem31cDcjgi8L/wJZrXoPkKI7fvsuLMFa71RcMWvf3yVzSCU7Ef32EoiqvtOVVJAngiMEq1LYK2CR2FKksKeetajeMB0nV2u87RxJU4nWlvHXsD8H+BXgostfkAqbueP/EI2G8vNeVcjv+7/pcjnzFGRQTtG1iBN/2gm8X/G9cjYUNsd/cxFf4C9pQxEvK0J5sFO13pdulD5hzK/eB61iilYDQ1cmCWZpYHB+rTPaRRXFMQviSwV+G88AAnBBcM8Lbo/GKexZANHCSvR0j8JUKHuVHLieUa6JWOZqGyfznaBEnkx3xhi0qGbSueJFOKum/K47eLq1wL8Q==";

        // Read the private key from the file
        byte[] encodedPrivateKey = readFile(privateKeyFile);

        // Decode the private key from PEM format
        encodedPrivateKey = Base64.getMimeDecoder().decode(encodedPrivateKey);

        // Create a key specification from the decoded private key
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encodedPrivateKey);

        // Create a key factory to generate a PrivateKey object
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PrivateKey privateKey = keyFactory.generatePrivate(keySpec);

        // Decode the ciphertext
        byte[] decodedCiphertext = Base64.getDecoder().decode(ciphertext);

        // Initialize the cipher with the private key and perform the decryption
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        byte[] plaintext = cipher.doFinal(decodedCiphertext);

        // Print the decrypted message
        System.out.println("Decrypted message: " + new String(plaintext, StandardCharsets.UTF_8));
    }

    private static byte[] readFile(String filename) throws Exception {
        try (FileInputStream fis = new FileInputStream(filename)) {
            byte[] data = new byte[fis.available()];
            fis.read(data);
            return data;
        }
    }

}

Al ejecutarlo, esto recibí:

El archivo que contiene la llave privada que puse como referencia, es el mismo que uso en Genero, por lo que cuenta con el encabezado y pie de página que indica que es una llave RSA. Procedí a generar una copia del mismo sin esas líneas, cambié la referencia en el programa y volví a probar:

Y listo, ahora sí todo bien.

Hay un tema más que te debo compartir. Las llaves RSA están expresadas en más de una línea, como aquí se aprecia:

A Java se lo puedes pasar así, con saltos de línea o como una sola cadena completa.

Oye… y después de ver esto, ¿no te llama la atención que las cadenas cifradas generadas en Genero y en el sitio web no son las mismas aunque la cadena base y la llave pública sí lo son?

Antes de documentarme, probé descifrando en Genero y en Java las cadenas generadas usando la misma llave pública, la cadena base «3200.00» y la misma llave privada. Todo funcionó correctamente.

¿Qué pasa?

Que RSA no es un algoritmo de HASH, por lo que no necesariamente tiene que dar una misma cadena cifrada si los insumos, cadena base y llave pública son los mismos en cada ejecución. Eso rompería la seguridad del algoritmo.

Entonces… ahora sí, todo bien. Espero que la crónica sea de utilidad.

If you want to read this in English, click here.

One response to “Cifrado y descifrado RSA con 4js Genero y Java”

  1. […] Si estás buscando esta información en Español, haz clic aquí. […]

Responder a RSA encryption and decryption with 4js Genero & JAva – Crónicas de ProgramaciónCancelar respuesta

Descubre más desde Crónicas de Programación

Suscríbete ahora para seguir leyendo y obtener acceso al archivo completo.

Seguir leyendo