DEV Community

Isaac Ojeda
Isaac Ojeda

Posted on • Updated on

.NET: Modelo Criptográfico, lo que necesitas saber.

Introducción

La criptografía suele ser un tema complicado y sinceramente no debes de ser un experto para poder manejarla, por eso quisiera compartirles lo que últimamente he tenido que implementar y por lo tanto me ha llevado a investigar cosas muy interesantes sobre criptografía y .NET.

.NET provee implementaciones de muchos algoritmos criptográficos y su modelo es extendible. El sistema de criptografía en .NET implementa un patrón basado en la herencia que permite extender la funcionalidad sin mucha dificultad:

  • Las clases padres de todos los algoritmos según su tipo de algoritmo sonSymmetricAlgorithm, AsymmetricAlgorithm, o HashAlgorithm. A este nivel, son clases abstractas.
  • También existen clases de cada algoritmo (que heredan de la clase anterior según su tipo); por ejemplo, Aes, RSA, o ECDiffieHellman. A este nivel sigue siendo abstracto, pero cada clase tiene su factory para crear una instancia de su implementación default (y recomendada).
  • Y al final tendremos distintas implementaciones de cada algoritmo; por ejemplo, AesManaged, RC2CryptoServiceProvider, o ECDiffieHellmanCng.

Este patrón de herencia de clases permite al framework agregar un nuevo algoritmo o una implementación distinta de un algoritmo existente. Por ejemplo, para crear un algoritmo con llave pública, se debería de heredar de la clase AsymmetricAlgorithm. Para crear una nueva implementación de un algoritmo en específico, deberíamos crear una clase no abstracta de dicho algoritmo (como Aes o RSA).

Nota 💡: Al mencionar que este diseño permite extender funcionalidad por medio de la herencia de clases y crear diferentes implementaciones, no significa necesariamente que nosotros lo vamos a hacer. De hecho, NO DEBEMOS crear nuestras propias implementaciones, más bien es el diseño correcto del framework para poder evolucionar a otros algoritmos cuando es necesario

¿Cómo están implementados estos algoritmos en .NET?

Como ejemplo de las distintas implementaciones disponibles de un algoritmo criptográfico y tomando como referencia uno simétrico. La clase base de todos los algoritmos simétricos es SymmetricAlgorithm, el cual es usado como base para algoritmos como Aes, TripleDES, y entre otros que por lo general ya no son recomendados.

Aes es heredado por AesCryptoServiceProvider, AesCng, y AesManaged, que son las diferentes implementaciones de un algoritmo que podemos encontrar en .NET en Windows

  • Las clases con la nomenclatura *CryptoServiceProvider, como en el ejemplo AesCryptoServiceProvider, son wrappers de la implementación CAPI (Windows Cryptographic API) y solo está disponible en Windows.
    • Hoy en día estas implementaciones ya están marcadas como obsoletas en .NET 6.
  • Clases con *Cng, como ECDiffieHellmanCng, son también wrappers de la implementación del sistema operativo, pero ahora utilizando la implementación de Windows Cryptography Next Generation. Por lo tanto, solo disponible también en Windows.
  • Clases *Managed, como AesManaged, son escritas totalmente en código manejado. Estas implementaciones no son nativas del sistema operativo y por lo tanto no están certificadas por la FIPS y también pueden ser más lentas que las versiones Csp y Cng.

Para revisar la compatibilidad de distintos algoritmos en Windows, Linux o macOS visita Cross-platform cryptography in .NET Core and .NET 5

Entonces ¿Qué implementación debo usar?

Respuesta corta: Que .NET lo decida con {Algoritmo}.Create().

Respuesta larga:

Podría ser sencillo decidir, pero todo depende que sistema operativo vas a usar, que tipo de aplicación vas a realizar y quién la va a usar.

La Federal Information Processing Standards (FIPS) certifica los algoritmos usados en los sistemas operativos (Linux, macOS y Windows que incluye los Csp y Cng) y esta certificación es requerida por el gobierno federal de Estados Unidos y seguro por muchas corporaciones o naciones.

*CryptoServiceProvider utiliza Windows Cryptography API y *Cng utiliza Cryptography Next Generation. Este último siendo el más reciente, solo estando disponible en Windows Server 2008+ (de verdad ¿quién usaría un servidor más viejo que eso? 😅)

Por conclusión, podemos decir que si la versión de .NET lo permite, la versión del sistema operativo lo permite, deberíamos de usar Cng siempre. Pero, desde .NET 6, la documentación recomienda siempre usar el factory Create() de cada algoritmo base (como los ejemplos que veremos más abajo) y por default será un algoritmo implementado con Cng si es Windows, si es otro sistema operativo usará su propia implementación (por ejemplo Linux usa OpenSSL) pero de ser posible, certificada por la FIPS.

En la mayoría de los casos, no será necesario hacer referencia a una implementación concreta de un algoritmo como AesCng. Lo métodos y propiedades que típicamente se necesitan para AES (como ejemplo) se encuentran en la clase base Aes. Esta clase base crea una instancia de la implementación default (y recomendada) usando el factory Create().

De hecho, podríamos decir que por default cada SO usa:

  • Apple Security Framework en macOS
  • OpenSSL en Linux
  • CNG en Windows

Por lo tanto utilizar la clase base del algoritmo y su factory Create() es la opción que recomiendo usar si es posible, por que estoy seguro que habrá escenarios donde se tendrá que ser más específico en alguna implementación en particular (Ejem. Usar llaves privadas en formato .key con RSA).

Nota 💡: Si en .NET 6 usas cualquier CryptoServiceProvider, tendrás un warning de que es obsoleto. Si usas cualquier *Cng obtendrás otro warning diciendo que solo está disponible en Windows. Siempre la mejor opción será usar {Algoritmo}.Create().

Elige un Algoritmo según la tarea

Puedes seleccionar un algoritmo por varias razones: por ejemplo, para integridad de datos, para privacidad de datos o para generar llaves. Los algoritmos Hash y Simétricos tienen el propósito de proteger información y su integridad (protegerlas de que las puedan cambiar) o también por privacidad (protegerlas de que alguien las vea). Los algoritmos Hash principalmente son para la integridad de la información.

Aquí hay una lista de los algoritmos recomendados según la aplicación:

Nota 💡: Si revisas la documentación de las clases mencionadas arriba, verás que sus clases padre son SymmetricAlgorithm, AsymmetricAlgorithm o HashAlgorithm

Ejemplo de encriptación Simétrica

using System.Security.Cryptography;
using System.Text;

string original = "Here is some data to encrypt!";

using (var myAes = Aes.Create())
{
    var encrypted = EncryptBytes(Encoding.UTF8.GetBytes(original), myAes.Key, myAes.IV);
    var plainBytes = DecryptBytes(encrypted, myAes.Key, myAes.IV);
    var roundtrip = Encoding.UTF8.GetString(plainBytes);

    Console.WriteLine("Original:   {0}", original);
    Console.WriteLine("Encrypted:  {0}", Convert.ToBase64String(encrypted));
    Console.WriteLine("Round Trip: {0}", roundtrip);
    Console.WriteLine();
    Console.ReadLine();
}

byte[] EncryptBytes(byte[] plainBytes, byte[] Key, byte[] IV)
{
    byte[] encrypted;

    using (var aesAlg = Aes.Create())
    {
        aesAlg.Key = Key;
        aesAlg.IV = IV;

        ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);

        using MemoryStream msEncrypt = new();
        using CryptoStream csEncrypt = new(msEncrypt, encryptor, CryptoStreamMode.Write);

        csEncrypt.Write(plainBytes, 0, plainBytes.Length);
        csEncrypt.Close();
        csEncrypt.Flush();

        encrypted = msEncrypt.ToArray();
    }

    return encrypted;
}

byte[] DecryptBytes(byte[] encrypted, byte[] Key, byte[] IV)
{
    using (var aesAlg = Aes.Create())
    {
        aesAlg.Key = Key;
        aesAlg.IV = IV;

        ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);

        using MemoryStream msDecrypt = new(encrypted);
        using CryptoStream csDecrypt = new(msDecrypt, decryptor, CryptoStreamMode.Read);
        using MemoryStream msDestination = new();

        csDecrypt.CopyTo(msDestination);

        return msDestination.ToArray();
    }
}
Enter fullscreen mode Exit fullscreen mode

¿Qué está pasando aquí?

Aquí están sucediendo varias cosas pero afortunadamente es igual en casi todos los distintos algoritmos que existen.

Como lo he repetido muchas veces, lo importante aquí es usar el Factory Create() de la clase base del algoritmo, en este caso Aes.

Aes.Create() nos crea la instancia del algoritmo con la implementación según el SO y certificado por la FIPS. Por defecto nos crea una llave aleatoria y un vector de inicialización (Key & IV). Lo más habitual es que estos los generemos nosotros y los guardemos en algún lugar seguro (para evidentemente, usarlos cuando se necesiten)

Lo demás:

  • ICryptoTransform. Este es el encriptador y desencriptador, es el que transforma los bytes de un Stream a bytes encriptados.
  • MemoryStream. Es el Stream origen donde se leen los bytes por encriptar o ya encriptados
  • CryptoStream. Este Stream el enlace entre el Decryptor y el Stream que se quiere encriptar / desencriptar. Al leer o escribir bytes que pasan por el CryptoStream, estos son encriptados / desencriptados.

Siendo sinceros, se ve intimidante tanto Stream, pero esto es casi un boilerplate, solo varía en casos específicos.

Cambiar de algoritmo es muy fácil, si queremos usar TripleDES, solo cambiamos la clase del algoritmo y listo.

Ejemplo de encriptación Asimétrica

using System.Security.Cryptography;
using System.Text;

string original = "Here is some data to encrypt!";

using (var rsa = RSA.Create())
{
    var encrypted = EncryptBytes(Encoding.UTF8.GetBytes(original), rsa.ExportParameters(false));
    var plainBytes = DecryptBytes(encrypted, rsa.ExportParameters(true));
    var roundtrip = Encoding.UTF8.GetString(plainBytes);

    Console.WriteLine("Original:   {0}", original);
    Console.WriteLine("Encrypted:  {0}", Convert.ToBase64String(encrypted));
    Console.WriteLine("Round Trip: {0}", roundtrip);
    Console.WriteLine();
    Console.ReadLine();
}

byte[] EncryptBytes(byte[] DataToEncrypt, RSAParameters RSAKeyInfo)
{
    try
    {
        byte[] encryptedData;
        using (var rsa = RSA.Create())
        {

            rsa.ImportParameters(RSAKeyInfo);

            encryptedData = rsa.Encrypt(DataToEncrypt, RSAEncryptionPadding.OaepSHA512);
        }
        return encryptedData;
    }

    catch (CryptographicException e)
    {
        Console.WriteLine(e.Message);

        return null;
    }
}

byte[] DecryptBytes(byte[] DataToDecrypt, RSAParameters RSAKeyInfo)
{
    try
    {
        byte[] decryptedData;

        using (var rsa = RSA.Create())
        {

            rsa.ImportParameters(RSAKeyInfo);

            decryptedData = rsa.Decrypt(DataToDecrypt, RSAEncryptionPadding.OaepSHA512);
        }
        return decryptedData;
    }

    catch (CryptographicException e)
    {
        Console.WriteLine(e.ToString());

        return null;
    }
}
Enter fullscreen mode Exit fullscreen mode

¿Qué está pasando aquí? x2

Este se ve más sencillo, pero es el más probable que se complique en el mundo real.

Las llaves privadas/públicas RSA tienen una infinidad de formatos, y esto a veces se vuelve un problema mayor. Sinceramente, es donde más batallo yo (ver más RSA Key Formats).

Resumen:

  • RSA.Create(): Como lo hemos estado mencionando, este se encarga de inicializar la mejor implementación actual y según el SO. También te crea las llaves necesarias (privadas y públicas) y sería bueno guardarlas de alguna forma (o importarlas según el RSA Key Format 🤮)
  • Encrypt y Decrypt: estos hacen la magia, ya es un proceso más simplificado. RSAEncryptionPadding especifica modo Padding del algoritmo y los parámetros para emplear la encriptación y desencriptación (la verdad, soy sincero, no entiendo esta parte 😃)

Ejemplo de Hashing.

using System.Security.Cryptography;
using System.Text;

string original = "Here is some data to hash!";
byte[] originalBytes = Encoding.UTF8.GetBytes(original);

var hash = ComputeHash(originalBytes);
var verify = VerifyHash(originalBytes, hash);

Console.WriteLine("Text to Hash:      {0}", original);
Console.WriteLine("Text hashed:       {0}", Convert.ToBase64String(hash));
Console.WriteLine("Text Verify Result {0}", verify);
Console.ReadLine();

byte[] ComputeHash(byte[] data)
{
    using var sha256 = SHA256.Create();
    return sha256.ComputeHash(data);
}

bool VerifyHash(byte[] original, byte[] hash)
{
    using var sha256 = SHA256.Create();
    var newHash = sha256.ComputeHash(original);
    return newHash.SequenceEqual(hash);
}
Enter fullscreen mode Exit fullscreen mode

Este la verdad está muy sencillo, simplemente los Hash’s no se pueden revertir. Es de utilidad para proteger información que no quieres que nadie vea (como contraseñas, aunque para eso hay algoritmos más fuertes que solo usar SHA256, un buen ejemplo es el PasswordHasher de ASP.NET Identity que utiliza PBKDF2 con HMAC-256)

No tengas miedo, IDataProtector es tu mejor amigo.

No necesariamente se tiene que profundizar en criptografía para proteger información, dale una estudiada a la Data Protection API de ASP.NET Core, la verdad es la forma default para proteger información en aplicaciones modernas en .NET.

Siempre deberías de usar IDataProtector (internamente usa todo esto, pero lo hace por ti), pero seguro existirán escenarios donde no será suficiente (Ejem. Si eres de México y necesitas implementar una facturación electrónica, no tendrás opción más que hacer todo a mano).

Lo genial es que IDataProtector se encarga del manejo de llaves, actualización de algoritmos, etc. Puedes personalizarlo y extenderlo como lo necesites.

También es mejor si utilizas ASP.NET Identity cuando se trate de información de usuarios, restablecimiento de contraseñas, generación de tokens, Two Factor Authentication (que internamente Identity Core usa la API de IDataProtector para generación de tokens).

Ejemplo con IDataProtector

using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.DependencyInjection;

var serviceCollection = new ServiceCollection();
serviceCollection.AddDataProtection();
var services = serviceCollection.BuildServiceProvider();

var instance = ActivatorUtilities.CreateInstance<MyClass>(services);
instance.RunSample();

class MyClass
{
    IDataProtector _protector;

    // the 'provider' parameter is provided by DI
    public MyClass(IDataProtectionProvider provider)
    {
        _protector = provider.CreateProtector("Contoso.MyClass.v1");
    }

    public void RunSample()
    {
        Console.Write("Enter input: ");
        string? input = Console.ReadLine();

        // protect the payload
        string protectedPayload = _protector.Protect(input!);
        Console.WriteLine($"Protect returned: {protectedPayload}");

        // unprotect the payload
        string unprotectedPayload = _protector.Unprotect(protectedPayload);
        Console.WriteLine($"Unprotect returned: {unprotectedPayload}");
    }
}
Enter fullscreen mode Exit fullscreen mode

Para poder correr este ejemplo, es necesario agregar las referencias del Shared Framework que vienen incluidas en el Runtime.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
    <ItemGroup>
        <FrameworkReference Include="Microsoft.AspNetCore.App" />
    </ItemGroup>
</Project>
Enter fullscreen mode Exit fullscreen mode

Conclusión

La protección de datos es un tema delicado, es importante saber como estamos implementando los algoritmos de encriptación y si estamos usando el correcto para la tarea correcta.

La implementación de cualquier algoritmo de encriptación debe de seguir siendo diseñado para que pueda cambiarse o actualizarse. El tiempo avanza y el computo también, en el momento en que un algoritmo pueda ser explotado, ese día se volverá obsoleto.

Puedes ver todos los ejemplos en mi Github.

¡Saludos!

Referencias

Discussion (0)