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 son
SymmetricAlgorithm
,AsymmetricAlgorithm
, oHashAlgorithm
. 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
, oECDiffieHellman
. 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
, oECDiffieHellmanCng
.
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 ejemploAesCryptoServiceProvider
, 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
, comoECDiffieHellmanCng
, 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
, comoAesManaged
, 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:
- Privacidad de datos:
- Integridad de datos
- Firmas digitales
- Intercambio de claves
- Generación de números aleatorios (para claves privadas)
- Generador de claves privadas a partir de una contraseña
Nota 💡: Si revisas la documentación de las clases mencionadas arriba, verás que sus clases padre son
SymmetricAlgorithm
,AsymmetricAlgorithm
oHashAlgorithm
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();
}
}
¿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 elCryptoStream
, 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;
}
}
¿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
yDecrypt
: 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);
}
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}");
}
}
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>
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
- .NET cryptography model | Microsoft Docs
- Cross-platform cryptography in .NET Core and .NET 5 | Microsoft Docs
- c# - Difference between SHA256CryptoServiceProvider and SHA256Managed - Stack Overflow
- .NET Core 2.0 Cryptography uses Apple Security Framework on macOS · Issue #21 · dotnet/announcements (github.com)
- c# - System.Security.Cryptography.Csp on Ubuntu 16.04 - Stack Overflow
- runtime/src/libraries/System.Security.Cryptography.Cng/src/System/Security/Cryptography at main · dotnet/runtime (github.com)