Skip to content

Técnicas de cifrado y firma en Apex ☁️

Esta es una traducción que desde EGA Futura ofrecemos como cortesía a toda la Ohana y comunidad de programadores , consultores , administradores y arquitectos de Salesforce para toda Iberoamérica .

El enlace a la publicación original, lo encontrarás al final de este artículo.

Las fallas criptográficas es la segunda categoría más importante de vulnerabilidades enumeradas en el Top 10 de OWASP para 2021. En esta publicación de blog, cubriremos técnicas para cifrar y codificar datos en Apex cuando esos datos deben transmitirse hacia o desde un sistema externo. Compartiremos ejemplos de código y explicaremos cuándo elegir cada técnica.

OWASP # 2: Fallos criptográficos

OWASP (Open Web Application Security Project) es una fundación sin fines de lucro que trabaja para mejorar la seguridad del software. Recientemente, OWASP publicó su lista Top 10 para 2021 . Una de las categorías que ha subido en la lista es Fallos criptográficos, que ahora se encuentra en la segunda posición (A02). Esto significa que hoy en día este problema es aún más crítico y frecuente que en 2017 (cuando la categoría se denominó “Exposición de datos sensibles”).

Esta categoría cubre las fallas relacionadas con la criptografía, incluida la falta de mecanismos criptográficos, que a menudo conduce a la exposición de datos confidenciales. Esto es especialmente importante para las integraciones, donde los datos se transmiten de un sistema a otro. Afortunadamente, Salesforce Platform y Apex tienen utilidades y algoritmos criptográficos para garantizar que los datos se transmitan de forma segura y sin compromisos.

¿Qué datos se deben cifrar?

La primera pregunta a responder aquí es: ¿qué tipo de datos se deben cifrar y cómo? Las contraseñas, los números de tarjetas de crédito, los registros médicos, la información personal y los secretos comerciales son algunos ejemplos de datos confidenciales. Debemos cifrar dichos datos, no solo por nuestra propia seguridad, sino también (en algunos casos) para cumplir con regulaciones como el RGPD o el estándar de seguridad de datos PCI. Se recomienda cifrar los datos en reposo (en disco) y cuando se transmiten a través de la red (en tránsito), incluso si estamos usando protocolos seguros como HTTPS. En esta publicación de blog, nos centraremos en cifrar y aplicar hash a los datos en tránsito. Si desea saber más sobre el cifrado en reposo, eche un vistazo a nuestra función de cifrado de plataforma Shield.

Conceptos clave de seguridad de la información

Hay diferentes algoritmos disponibles para cifrar datos en tránsito, y cada uno puede hacer cumplir una o más de las siguientes características del proceso de comunicación:

  • Confidencialidad (secreto): asegura que un mensaje se transmite en un formato (normalmente encriptado), de modo que los usuarios no autorizados no puedan revelar lo que contiene el mensaje.
  • Integridad: asegura que el mensaje no ha sido alterado durante la transmisión (no manipulado). La confidencialidad y la integridad son independientes. Por ejemplo, un mensaje puede transmitirse en texto sin cifrar, sin ser secreto, sin dejar de preservar su integridad.
  • Autenticidad: asegura que el mensaje fue enviado por el remitente que lo reclama. Los algoritmos que garantizan la autenticidad a menudo también implican integridad.
  • No repudio: es un concepto más fuerte que la autenticidad. Agrega prueba legal que asegura que el remitente envió el mensaje.

La clase Apex Crypto contiene funciones predefinidas para ayudarlo a implementar algoritmos de cifrado seguros. Echemos un vistazo a algunos de ellos.

Cifrado AES

El Crypto.encrypt() permite cifrar datos antes de que se envíen a un receptor mediante el algoritmo AES . Esto asegura la confidencialidad . El algoritmo AES es un algoritmo de cifrado de bloques (opera con bloques de un tamaño fijo) que toma texto sin formato en bloques de 128 bits y los convierte en texto cifrado. El modo de funcionamiento de AES en Apex es el modo CBC . El texto cifrado sigue la sintaxis de relleno PKCS7. De manera equivalente, el Crypto.decrypt() permite descifrar los datos recibidos en texto cifrado.

Puede elegir entre AES128, AES192 y AES256 para el algoritmo. Las clases de cifrado a menudo ofrecen múltiples versiones de algoritmos y, cuando sea posible, debe elegir la opción de bit más alto común para Salesforce y el sistema de terceros. Otro factor a considerar es el tiempo de cálculo: cuanto más complejo sea el algoritmo, más tiempo llevará cifrar y descifrar.

AES usa una clave simétrica de 128, 192 o 256 bits (siendo esta última la opción más segura). No es necesario que la longitud de clave elegida coincida con la versión del algoritmo elegido. Hay un Crypto.generateAESKey() que puede utilizar para generar la clave.

Una clave simétrica es un secreto compartido que tanto el remitente como el receptor tienen para cifrar y descifrar el mensaje. Compare esto con las claves asimétricas, donde el remitente encripta usando la clave pública del receptor y el receptor descifra usando su propia clave privada. Existe una situación particular, las firmas digitales, en la que el remitente usa su clave privada para cifrar (consulte la sección sobre esto a continuación).

Las claves simétricas deben compartirse de forma segura. La recomendación es compartirlos sin conexión o enviarlos encriptados, generalmente usando PKI . Además, recuerde que las claves deben almacenarse de forma segura en Salesforce. En los paquetes administrados, utilice un registro de metadatos personalizado protegido o una configuración personalizada protegida para ese propósito.

El algoritmo AES también necesita un vector de inicialización (IV). El tamaño del IV se basa en el modo de algoritmo. Para CBC, son 128 bits. Este es un número aleatorio que se utiliza para garantizar que el mismo valor cifrado varias veces no siempre dé como resultado el mismo valor cifrado.

El siguiente diagrama muestra dónde se usa el IV en el modo CBC. Observe cómo la salida sería la misma para una entrada y una clave determinadas si no hubiera un IV.


Para descifrar, no solo necesita la clave simétrica, sino también la IV. Puede generar un IV personalizado para cifrar los datos y enviarlo junto con el texto cifrado para descifrarlo en el receptor. Tenga en cuenta que el IV debe ser diferente en cada operación.

En la mayoría de los casos, es más fácil utilizar el Crypto.encryptWithManagedIV() que genera un vector de inicialización aleatorio para usted y lo transmite en los primeros 128 bits (16 bytes) del Blob cifrado. En ese caso, descifra los datos con Crypto.decryptWithManagedIV() . Le recomendamos encarecidamente que siga este enfoque.

Aquí tiene un código Apex de muestra que muestra cómo usar estos métodos:

 / ** * @description Cifra los datos usando el algoritmo AES, que necesita una clave simétrica para ser compartida con el receptor. * En este caso, el vector de inicialización lo gestiona Salesforce. * @param dataToEncrypt Blob que contiene los datos para cifrar * @return Blob * @ejemplo * Blob dataToEncrypt = Blob.valueOf ('Datos de prueba'); * Blob encryptedData = EncryptionRecipes.encryptAES256WithManagedIVRecipe (dataToEncrypt); * System.debug (EncodingUtil.base64Encode (encryptedData)); ** /
@AuraEnabled
cifrado de blobs estático público AES256WithManagedIVRecipe (blob dataToEncrypt) { // Llamar a Crypto.encryptWithManagedIV especificando el algoritmo AES seleccionado devolver Crypto.encryptWithManagedIV ( AESAlgorithm.AES256.name (), AES_KEY, dataToEncrypt );
}
 / ** * @description Cifra los datos usando el algoritmo AES, que necesita una clave simétrica para ser compartida con el receptor. * En este caso, el vector de inicialización serán los primeros 128 bits (16 bytes) de los datos recibidos. * @param dataToDecrypt Blob que contiene los datos que se van a descifrar * @return Blob * @ejemplo * Blob decryptedData = EncryptionRecipes.decryptAES256WithManagedIVRecipe (encryptedData); * System.debug (decryptedData.toString ()); ** /
@AuraEnabled
descifrado de Blob estático público AES256WithManagedIVRecipe (Blob dataToDecrypt) { // Llame a Crypto.decryptWithManagedIV especificando el algoritmo AES seleccionado devolver Crypto.decryptWithManagedIV ( AESAlgorithm.AES256.name (), AES_KEY, dataToDecrypt );
}

Tenga en cuenta que en todos los ejemplos, hemos codificado Blobs (variables que contienen datos binarios) en Base64 (codificación de binario a texto), de modo que pueda ver una Cadena al probarlos, pero los datos también se pueden transmitir en formato Blob.

Aunque hemos escrito ambas piezas de código en Apex, el caso de uso más común será enviar los datos a un sistema externo o viceversa. Normalmente, otros lenguajes de programación tienen bibliotecas similares para implementar exactamente el mismo comportamiento. Por ejemplo, eche un vistazo a este C # o estas soluciones de JavaScript. Tenga en cuenta que al cifrar con Crypto.encryptWithManagedIV() , el sistema externo tendrá que obtener el IV de los primeros 128 bits (16 bytes) del texto cifrado recibido para el descifrado.

Hash digest

El Crypto.generateDigest() genera un resumen de hash unidireccional utilizando MD5, SHA1, SHA256 o SHA512 (el último es el más seguro). El proceso de resumen de hash aplica un algoritmo a la entrada, lo que da como resultado una cadena final de una longitud fija. Las funciones hash son unidireccionales por diseño. No es factible revertir un hash criptográfico. Si bien no puede revertir un hash, los algoritmos están diseñados para ser compatibles, lo que le permite realizar un hash de la misma información en varios sistemas. Cada sistema, utilizando el mismo algoritmo, genera un hash idéntico. Entonces, el receptor se asegurará de que el mensaje no haya sido manipulado, la integridad del mensaje se ha preservado. Tenga en cuenta que generar y verificar un resumen de hash no garantiza la confidencialidad. Si necesita que el mensaje sea confidencial, deberá utilizar adicionalmente un algoritmo de cifrado. Además, un hash por sí solo no garantiza la autenticidad. Cubriremos HMAC y firmas digitales, que lo proporcionan, más adelante en la publicación.

Aquí tienes un código Apex que muestra cómo usar estos métodos para un emisor y para un receptor del mensaje:

 / ** * @description Genera un resumen de hash unidireccional que se puede verificar en el destino para garantizar la integridad. * @param dataToHmac Blob que contiene algunos datos para los cuales generar un hash * @return Blob * @ejemplo * Blob dataToHash = Blob.valueOf ('Datos de prueba'); * Blob hash = EncryptionRecipes.generateSHA512HashRecipe (); * System.debug (EncodingUtil.base64Encode (hash)); ** /
@AuraEnabled
public static Blob generateSHA512HashRecipe (Blob dataToHash) { // Llamar a Crypto.generateDigest especificando el algoritmo seleccionado return Crypto.generateDigest (HashAlgorithm.SHA512.name (), dataToHash);
}
 / ** * @description Vuelve a calcular el resumen de hash y lo compara con el recibido, lanzando una excepción si no son iguales. * @param hash Blob que contiene el hash recibido * @param dataToCheck Blob que contiene los datos para verificar el hash * @return void * @ejemplo * tratar { * EncryptionRecipes.checkSHA512HashRecipe (hash, corruptedData); *} captura (Excepción e) { * // Debería registrar la excepción * System.debug (e.getMessage ()); *} ** /
@AuraEnabled
verificación de vacío estática públicaSHA512HashRecipe (Blob hash, Blob dataToCheck) { Blob recomputedHash = Crypto.generateDigest ( HashAlgorithm.SHA512.name (), dataToCheck ); // recomputedHash y hash deben ser idénticos! if (! areEqualConstantTime (hash, recomputedHash)) { lanzar una nueva CryptographicException ('¡Hash incorrecto!'); }
}

Tenga en cuenta que areEqualConstantTime() es un método que compara dos blobs en tiempo constante para evitar efectos de ataque de tiempo.

 / ** * Las comparaciones que involucran criptografía deben realizarse en tiempo constante * uso de funciones especializadas para evitar efectos de ataque de tiempo. * https://en.wikipedia.org/wiki/Timing_attack * @param primera primera cadena para comparar * @param segundo segundo String para comparar * @return Las cadenas booleanas son iguales * / public static boolean areEqualConstantTime (String primero, String segundo) { Resultado booleano = verdadero; if (first.length ()! = second.length ()) { resultado = falso; } Integer max = first.length ()> second.length () ? second.length () : primera.longitud (); para (Entero i = 0; i <max; i ++) { if (first.substring (i, i + 1)! = second.substring (i, i + 1)) { resultado = falso; } } devolver resultado; }

HMAC

El método Crypto.generateMac() (ver referencia ) genera un código de autenticación de mensajes basado en hash (HMAC). Con HMAC, se usa una clave secreta (simétrica) para derivar dos claves que se usan en el proceso de hash. De esta manera, no solo se garantiza la integridad , sino también la autenticidad, ya que podemos asegurarnos de que la MAC del mensaje se generó utilizando la clave secreta. Los algoritmos HMAC admitidos son HMACMD5, HMACSHA1, HMACSHA256 y HMACSHA512 (el último es el más seguro).

La única restricción para la clave es que es inferior a 4 KB. Se recomienda utilizar diferentes claves para el cifrado del mensaje y para el HMAC.

Aquí tiene un código Apex para generar un HMAC en el remitente y verificar su validez en el receptor.

 / ** * @description Genera HMAC unidireccional (utilizando una clave simétrica) que se puede verificar en el destino para garantizar la integridad y autenticidad. * @param dataToHmac Blob que contiene algunos datos para los cuales generar un HMAC * @return Blob * @ejemplo * Blob dataToHmac = Blob.valueOf ('Datos de prueba'); * Blob hmac = EncryptionRecipes.generateHMACSHA512Recipe (); * System.debug (EncodingUtil.base64Encode (hmac)); ** /
@AuraEnabled
Blob estático público generateHMACSHA512Recipe (Blob dataToHmac) { // Llamar a Crypto.generateMac especificando el algoritmo seleccionado devolver Crypto.generateMac ( HMACAlgorithm.HMACSHA512.name (), dataToHmac, HMAC_KEY );
}
 / ** * @description Vuelve a calcular HMAC usando la clave simétrica y la compara con la recibida, lanzando una excepción si no son iguales. * @param hmac Blob que contiene el hmac recibido * @param dataToCheck Blob que contiene los datos para verificar el hmac * @return void * @ejemplo * tratar { * EncryptionRecipes.checkHMACSHA512Recipe (hmac, corruptedData); *} captura (Excepción e) { * // Debería registrar la excepción * System.debug (e.getMessage ()); *} ** /
@AuraEnabled
verificación de vacío estático públicoHMACSHA512Recipe (Blob hmac, Blob dataToCheck) { Booleano correcto = Crypto.verifyHMAC ( HMACAlgorithm.HMACSHA512.name (), dataToCheck, HMAC_KEY, hmac ); if (! correct) { lanzar una nueva CryptographicException ('¡HMAC incorrecto!'); }
}

Firma digital

Aunque las claves simétricas deben compartirse de forma segura, ya que se comparten, siempre existe la posibilidad de que alguien más las haya utilizado para hacerse pasar por el remitente. Por eso utilizamos firmas digitales para garantizar el no repudio. Las firmas digitales utilizan una clave asimétrica en la que la parte privada de la clave nunca se comparte. Entonces, cuando un mensaje está firmado digitalmente, se considera legalmente probado que el remitente envió el mensaje.

Con el Crypto.sign() , puede calcular una firma única para el mensaje utilizando el algoritmo de firma especificado y la clave privada proporcionada; en este caso, la parte del remitente de una clave asimétrica. Los algoritmos válidos son RSA, RSA-SHA1, RSA-SHA256, RSA-SHA384, RSA-SHA512, ECDSA-SHA256, ECDSA-SHA384 y ECDSA-SHA512. Hay varios factores a tener en cuenta al comparar los algoritmos RSA y ECDSHA, así que evalúelos en profundidad antes de elegir. En este caso, se aplica la integridad , autenticidad y no repudio del mensaje.

La clave asimétrica debe estar en la sintaxis PKCS # 8 de RSA. Compruebe cómo generar un par de claves con openssl .

Aquí tienes algunos ejemplos de código para comprender mejor cómo usar los métodos:

 / ** * @description Genera una firma digital unidireccional (cifrada con una clave asimétrica) que se puede verificar en el destino para garantizar la integridad, autenticidad y no repudio. * @param dataToSign Blob que contiene algunos datos para firmar * @return Blob * @ejemplo * Blob dataToSign = Blob.valueOf ('Datos de prueba'); * Firma de blob = EncryptionRecipes.generateRSASHA512DigitalSignatureRecipe (); * System.debug (EncodingUtil.base64Encode (firma)); ** /
@AuraEnabled
Blob estático público generateRSASHA512DigitalSignatureRecipe ( Blob dataToSign
) { // Llamar a Crypto.sign especificando el algoritmo seleccionado devolver Crypto.sign ( DigitalSignatureAlgorithm.RSA_SHA512.name (). Replace ('_', '-'), dataToSign, DIGITAL_SIGNATURE_PRIVATE_KEY );
}
 / ** * @description Vuelve a calcular la firma digital y la compara con la recibida, lanzando una excepción si no son iguales. * @param signature Blob que contiene la firma recibida * @param dataToCheck Blob que contiene los datos para verificar la firma * @return void * @ejemplo * tratar { * EncryptionRecipes.checkRSASHA512DigitalSignatureRecipe (firma, corruptedData); *} captura (Excepción e) { * // Debería registrar la excepción * System.debug (e.getMessage ()); *} ** /
@AuraEnabled
control de vacío estático públicoRSASHA512DigitalSignatureRecipe ( Firma de blob, Blob dataToCheck
) { Booleano correcto = Crypto.verify ( DigitalSignatureAlgorithm.RSA_SHA512.name (). Replace ('_', '-'), dataToCheck, firma, DIGITAL_SIGNATURE_PUBLIC_KEY ); if (! correct) { lanzar una nueva CryptographicException ('¡Firma incorrecta!'); }
}

Alternativamente, puede firmar un mensaje usando Crypto.signWithCertificate() , que toma el nombre de un certificado X509 que contiene una clave privada, y luego llama a Crypto.sign() internamente. Utilice Crypto.signXML() para firmar un documento XML.

Firmar un documento digitalmente es el enfoque más sólido, pero tenga en cuenta que cuanto mayor sea la complejidad, más tardará el algoritmo en ejecutarse. El tiempo de ejecución será un factor importante a tener en cuenta en la selección del algoritmo.

Combinando algoritmos de cifrado y firma

La combinación de algoritmos de cifrado y firma proporcionará cualquier combinación de características de confidencialidad, integridad y no repudio que necesite. Por ejemplo, mire este ejemplo en el que combinamos el cifrado con un algoritmo de firma digital:

 public class EncryptedAndSignedData { public Blob encryptedData; firma pública de Blob;
}
 / ** * @description Cifra el mensaje con AES y luego genera Firma Digital (cifrada con una clave asimétrica) que se puede verificar en destino. * Esto asegura la confidencialidad, integridad, autenticidad y no repudio. * @param dataToEncryptAndSign Blob que contiene algunos datos para cifrar y firmar * @return Blob * @ejemplo * Blob dataToEncryptAndSign = Blob.valueOf ('Datos de prueba'); * Envoltorio EncryptedAndSignedData = EncryptionRecipes.encryptAES256AndGenerateRSASHA512DigitalSignRecipe (); * System.debug (EncodingUtil.base64Encode (wrapper.encryptedData)); * System.debug (EncodingUtil.base64Encode (wrapper.signature)); ** /
@AuraEnabled
public static EncryptedAndSignedData encryptAES256AndGenerateRSASHA512DigitalSignRecipe ( Blob dataToEncryptAndSign
) { // Llamar a Crypto.encrypt especificando el algoritmo seleccionado Blob encryptedData = Crypto.encryptWithManagedIV ( AESAlgorithm.AES256.name (), AES_KEY, dataToEncryptAndSign ); // Llamar a Crypto.sign especificando el algoritmo seleccionado Firma de blob = Crypto.sign ( DigitalSignatureAlgorithm.RSA_SHA512.name (). Replace ('_', '-'), encryptedData, DIGITAL_SIGNATURE_PRIVATE_KEY ); Envoltorio EncryptedAndSignedData = nuevo EncryptedAndSignedData (); wrapper.encryptedData = encryptedData; wrapper.signature = firma; envoltorio de devolución;
}
 / ** * @description Descifra el mensaje y verifica su Firma Digital. * @param signature Blob que contiene la firma recibida * @param dataToDecryptAndCheck Blob que contiene los datos para verificar la firma * @return Blob datos descifrados * @ejemplo * tratar { * EncryptionRecipes.decryptAES256AndCheckRSASHA512DigitalSignRecipe (firma, corruptedData); *} captura (Excepción e) { * // Debería registrar la excepción * System.debug (e.getMessage ()); *} ** /
@AuraEnabled
desencriptación de blobs estáticos públicos AES256AndCheckRSASHA512DigitalSignRecipe ( Firma de blob, Blob dataToDecryptAndCheck
) { Booleano correcto = Crypto.verify ( DigitalSignatureAlgorithm.RSA_SHA512.name (). Replace ('_', '-'), dataToDecryptAndCheck, firma, DIGITAL_SIGNATURE_PUBLIC_KEY ); if (! correct) { lanzar una nueva CryptographicException ('¡Firma incorrecta!'); } devolver Crypto.decryptWithManagedIV ( AESAlgorithm.AES256.name (), AES_KEY, dataToDecryptAndCheck );
}

Resumen

En esta publicación de blog, hemos cubierto diferentes técnicas de cifrado y firma que puede utilizar para evitar fallas criptográficas al transmitir datos. Cada una de las técnicas asegura ciertos aspectos del proceso de comunicación que puedes encontrar resumidos en esta tabla:

Además, considere la complejidad del algoritmo frente al tiempo que tarda en ejecutarse al elegir el algoritmo adecuado para su caso de uso.

Hemos agregado todos los ejemplos de código destacados en esta publicación de blog a Apex Recipes . Además, obtenga más información sobre el cifrado en el módulo Trailhead: Use Encryption in Custom Applications .

Sobre el Autor

Alba Rivas trabaja como defensora principal de desarrolladores en Salesforce. Se centra en los componentes web Lightning y la estrategia de adopción de Lightning. Puedes seguirla en Twitter @AlbaSFDC .

Esta es una traducción realizada por EGA Futura, y este es el link a la publicación original: https://developer.salesforce.com/blogs/2021/12/encryption-and-signature-techniques-in-apex.html

Últimas novedades 
de EGA Futura
1954
Desde hace más de 25 años potenciamos a las Empresas de Iberoamérica
🎬 Video de EGA Futura » Conceptos de Seguridad (EGA Futura ERP / Salesforce)

🎬 Video de EGA Futura » Conceptos de Seguridad (EGA Futura ERP / Salesforce)

🎬 Video de Juan Manuel Garrido » Claves para tu Productividad diaria 🙌✅

🎬 Video de EGA Futura » Facturación Electrónica en Uruguay » Conceptos básicos con EGA Futura Windows

🎬 Video de EGA Futura » Facturación Electrónica en Uruguay » Configuración de EGA Futura Windows

🎬 Video de EGA Futura » Facturación Electrónica en Uruguay » Funcionamiento con EGA Futura Windows

🎬 Video de EGA Futura » Configuración de la Plataforma EGA Futura

🎬 Video de EGA Futura » Configuración de usuario en EGA Futura

🎬 Video de EGA Futura » Como automatizar la publicación en Redes Sociales?

🎬 Video de Juan Manuel Garrido » Cómo restaurar la configuración de fábrica de EGA Futura Windows sin perder la información

🎬 Video de Juan Manuel Garrido » Factura electrónica: Prueba de Factura Electronica previa a la activacion

🎬 Video de EGA Futura » Como se registran los Beneficios de cada Empleado en la base de datos de EGA Futura

🎬 Video de EGA Futura » EGA Futura Time Clock » Reloj de Control horario y asistencia

🎬 Video de EGA Futura » Como registrar Observaciones en un Empleado dentro de EGA Futura People?

🎬 Video de EGA Futura » Cómo registrar la Educación de cada Empleado en EGA Futura People?

🎬 Video de EGA Futura » Como hacer la Desvinculación de un Empleado? (Offboarding)

🎬 Video de EGA Futura » Como registrar Habilidades o Skills de empleados dentro de EGA Futura

🎬 Video de EGA Futura » Como hacer el Onboarding o Proceso de Incorporación de un Empleado?

🎬 Video de EGA Futura » Cómo administrar Turno de trabajo dentro de EGA Futura

🎬 Video de EGA Futura » Que es un Ticket interno dentro de la Plataforma EGA Futura

🎬 Video de EGA Futura » Que son los Entrenamientos de Empleado en EGA Futura people?

🎬 Video de EGA Futura » Qué son los Epics dentro de EGA Futura

🎬 Video de EGA Futura » Qué es EGA Futura People?

🎬 Video de EGA Futura » EGA Futura People » Asistencias

🎬 Video de EGA Futura » Soporte EGA Futura » Software de Gestión Windows vs Software de Gestión Nube 🤩

🎬 Video de EGA Futura » ツ Comparando un Objeto con un Fichero

Técnicas de cifrado y firma en Apex ☁️
Técnicas de cifrado y firma en Apex ☁️