Skip to main content

.NET Cryptography

Introduction

Cryptography is an essential aspect of modern application security, enabling developers to protect sensitive data through encryption, secure data transfers, verify data integrity, and authenticate users. In .NET, Microsoft provides a robust set of cryptographic libraries through the System.Security.Cryptography namespace, making it accessible for developers to implement these security features.

This guide will walk you through the fundamentals of .NET cryptography, covering encryption, hashing, digital signatures, and secure random number generation. By the end, you'll have a solid understanding of how to implement cryptographic solutions in your .NET applications.

Cryptography Fundamentals

Before diving into code, let's understand some key cryptography concepts:

  • Encryption: The process of converting data into a format that cannot be understood without a decryption key
  • Hashing: One-way transformation of data into a fixed-size string of characters
  • Digital Signatures: A mathematical scheme to verify the authenticity of digital messages or documents
  • Salt: Random data added to a password before hashing to prevent rainbow table attacks
  • Initialization Vector (IV): Random block used with encryption algorithms to ensure that identical plaintext doesn't encrypt to identical ciphertext

Hashing in .NET

Hashing is commonly used for password storage and data integrity verification. Instead of storing actual passwords, applications store password hashes and compare hashes during authentication.

Common Hashing Algorithms in .NET

csharp
// Import required namespaces
using System;
using System.Security.Cryptography;
using System.Text;

// Computing a SHA256 hash
public static string ComputeSha256Hash(string input)
{
using (SHA256 sha256 = SHA256.Create())
{
// Convert the input string to a byte array
byte[] inputBytes = Encoding.UTF8.GetBytes(input);

// Compute the hash
byte[] hashBytes = sha256.ComputeHash(inputBytes);

// Convert the byte array to a hexadecimal string
StringBuilder sb = new StringBuilder();
for (int i = 0; i < hashBytes.Length; i++)
{
sb.Append(hashBytes[i].ToString("x2"));
}
return sb.ToString();
}
}

Example Usage

csharp
string password = "MySecurePassword123";
string hashedPassword = ComputeSha256Hash(password);

Console.WriteLine($"Original password: {password}");
Console.WriteLine($"Hashed password: {hashedPassword}");

// Output:
// Original password: MySecurePassword123
// Hashed password: 8b2c89b3f33c9c76448e3437d6b6f7baef42f468098089b9d0f97bf3907fd128

Secure Password Hashing with Salt

For secure password storage, always use a salt with your hashing function:

csharp
public static (string hashedPassword, byte[] salt) HashPasswordWithSalt(string password)
{
// Generate a random salt
byte[] salt = new byte[16];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(salt);
}

// Create the hashed password with salt
using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 10000, HashAlgorithmName.SHA256))
{
byte[] hash = pbkdf2.GetBytes(32);
string hashedBase64 = Convert.ToBase64String(hash);
return (hashedBase64, salt);
}
}

public static bool VerifyPassword(string password, string storedHash, byte[] storedSalt)
{
using (var pbkdf2 = new Rfc2898DeriveBytes(password, storedSalt, 10000, HashAlgorithmName.SHA256))
{
byte[] hash = pbkdf2.GetBytes(32);
string hashedBase64 = Convert.ToBase64String(hash);
return hashedBase64 == storedHash;
}
}

Symmetric Encryption in .NET

Symmetric encryption uses the same key for both encryption and decryption. It's fast and efficient for encrypting large amounts of data.

Using AES (Advanced Encryption Standard)

csharp
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

public static class AesEncryptionHelper
{
public static (byte[] encryptedData, byte[] key, byte[] iv) Encrypt(string plainText)
{
byte[] encrypted;
byte[] key;
byte[] iv;

using (Aes aesAlg = Aes.Create())
{
// Save the key and IV for decryption
key = aesAlg.Key;
iv = aesAlg.IV;

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

// Create MemoryStream to store the encrypted data
using (MemoryStream msEncrypt = new MemoryStream())
{
using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
{
using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
{
// Write the data to be encrypted
swEncrypt.Write(plainText);
}
encrypted = msEncrypt.ToArray();
}
}
}

return (encrypted, key, iv);
}

public static string Decrypt(byte[] cipherText, byte[] key, byte[] iv)
{
string plaintext = null;

using (Aes aesAlg = Aes.Create())
{
// Set the key and IV
aesAlg.Key = key;
aesAlg.IV = iv;

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

// Create stream for decryption
using (MemoryStream msDecrypt = new MemoryStream(cipherText))
{
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
{
using (StreamReader srDecrypt = new StreamReader(csDecrypt))
{
// Read the decrypted data
plaintext = srDecrypt.ReadToEnd();
}
}
}
}

return plaintext;
}
}

Example Usage

csharp
string originalData = "Sensitive information that needs encryption";

// Encrypt the data
var (encryptedData, key, iv) = AesEncryptionHelper.Encrypt(originalData);

Console.WriteLine($"Original text: {originalData}");
Console.WriteLine($"Encrypted (Base64): {Convert.ToBase64String(encryptedData)}");

// Decrypt the data
string decryptedData = AesEncryptionHelper.Decrypt(encryptedData, key, iv);
Console.WriteLine($"Decrypted text: {decryptedData}");

// Output:
// Original text: Sensitive information that needs encryption
// Encrypted (Base64): a2xqODJsMzIvN2xrM2psaz0= (Note: actual output will be different)
// Decrypted text: Sensitive information that needs encryption

Asymmetric Encryption in .NET

Asymmetric encryption uses a pair of keys (public and private) for encryption and decryption operations. The public key can be freely distributed, while the private key must remain secure.

Using RSA

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

public static class RsaEncryptionHelper
{
public static (string publicKey, string privateKey) GenerateKeyPair()
{
using (RSA rsa = RSA.Create(2048)) // 2048-bit key
{
// Export the public and private keys in XML format
string publicKey = rsa.ToXmlString(false);
string privateKey = rsa.ToXmlString(true);

return (publicKey, privateKey);
}
}

public static byte[] EncryptWithPublicKey(string data, string publicKey)
{
using (RSA rsa = RSA.Create())
{
rsa.FromXmlString(publicKey);
byte[] dataBytes = Encoding.UTF8.GetBytes(data);
return rsa.Encrypt(dataBytes, RSAEncryptionPadding.OaepSHA256);
}
}

public static string DecryptWithPrivateKey(byte[] encryptedData, string privateKey)
{
using (RSA rsa = RSA.Create())
{
rsa.FromXmlString(privateKey);
byte[] decryptedBytes = rsa.Decrypt(encryptedData, RSAEncryptionPadding.OaepSHA256);
return Encoding.UTF8.GetString(decryptedBytes);
}
}
}

Example Usage

csharp
// Generate RSA key pair
var (publicKey, privateKey) = RsaEncryptionHelper.GenerateKeyPair();

string sensitiveData = "This is confidential information";

// Encrypt with the public key
byte[] encryptedData = RsaEncryptionHelper.EncryptWithPublicKey(sensitiveData, publicKey);
Console.WriteLine($"Original data: {sensitiveData}");
Console.WriteLine($"Encrypted data (Base64): {Convert.ToBase64String(encryptedData)}");

// Decrypt with the private key
string decryptedData = RsaEncryptionHelper.DecryptWithPrivateKey(encryptedData, privateKey);
Console.WriteLine($"Decrypted data: {decryptedData}");

Digital Signatures

Digital signatures are used to verify the authenticity and integrity of data. They ensure that data was created by a known sender and hasn't been altered.

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

public static class DigitalSignatureHelper
{
public static byte[] SignData(string data, RSAParameters privateKey)
{
byte[] dataBytes = Encoding.UTF8.GetBytes(data);

using (RSA rsa = RSA.Create())
{
rsa.ImportParameters(privateKey);
return rsa.SignData(dataBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
}

public static bool VerifySignature(string data, byte[] signature, RSAParameters publicKey)
{
byte[] dataBytes = Encoding.UTF8.GetBytes(data);

using (RSA rsa = RSA.Create())
{
rsa.ImportParameters(publicKey);
return rsa.VerifyData(dataBytes, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
}
}

Example Usage

csharp
// Generate an RSA key pair
using (RSA rsa = RSA.Create())
{
// Get the key parameters
RSAParameters privateKey = rsa.ExportParameters(true);
RSAParameters publicKey = rsa.ExportParameters(false);

// Original document to be signed
string document = "This is a legal document that I am signing digitally.";

// Sign the document
byte[] signature = DigitalSignatureHelper.SignData(document, privateKey);

Console.WriteLine($"Document: {document}");
Console.WriteLine($"Signature created: {Convert.ToBase64String(signature)}");

// Verify the signature (should be true)
bool isValid = DigitalSignatureHelper.VerifySignature(document, signature, publicKey);
Console.WriteLine($"Signature verification result: {isValid}");

// Try to verify with tampered data (should be false)
string tamperedDocument = document + " with some unauthorized changes";
bool isTamperedValid = DigitalSignatureHelper.VerifySignature(tamperedDocument, signature, publicKey);
Console.WriteLine($"Tampered document verification result: {isTamperedValid}");
}

Secure Random Number Generation

For cryptographic operations, you need truly random numbers that cannot be predicted. .NET provides the RandomNumberGenerator class for this purpose.

csharp
using System;
using System.Security.Cryptography;

public static class SecureRandomHelper
{
public static byte[] GenerateRandomBytes(int length)
{
byte[] randomBytes = new byte[length];
using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
{
rng.GetBytes(randomBytes);
}
return randomBytes;
}

public static int GenerateRandomNumber(int minValue, int maxValue)
{
if (minValue >= maxValue)
throw new ArgumentOutOfRangeException("minValue must be less than maxValue");

byte[] buffer = new byte[4];
using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
{
rng.GetBytes(buffer);
int result = BitConverter.ToInt32(buffer, 0);
return Math.Abs(result % (maxValue - minValue)) + minValue;
}
}
}

Example Usage

csharp
// Generate secure random bytes for a cryptographic key
byte[] randomKey = SecureRandomHelper.GenerateRandomBytes(32); // 256-bit key
Console.WriteLine($"Random key (Base64): {Convert.ToBase64String(randomKey)}");

// Generate a secure random number in a specific range
int randomPin = SecureRandomHelper.GenerateRandomNumber(1000, 9999);
Console.WriteLine($"Random PIN: {randomPin}");

Real-World Application: Secure Configuration Storage

Let's create a complete example of securely storing and retrieving configuration settings for an application:

csharp
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

public class SecureConfigManager
{
private readonly string _configFilePath;
private readonly string _keyFilePath;

public SecureConfigManager(string configFilePath, string keyFilePath)
{
_configFilePath = configFilePath;
_keyFilePath = keyFilePath;
}

public void SaveConfiguration<T>(T config)
{
// Serialize the configuration
string jsonConfig = JsonSerializer.Serialize(config);

// Generate or load encryption key
byte[] key;
byte[] iv;

if (File.Exists(_keyFilePath))
{
string[] keyData = File.ReadAllLines(_keyFilePath);
key = Convert.FromBase64String(keyData[0]);
iv = Convert.FromBase64String(keyData[1]);
}
else
{
using (Aes aes = Aes.Create())
{
key = aes.Key;
iv = aes.IV;
}

// Save the key and IV securely (in a real application, consider using DPAPI or Key Vault)
File.WriteAllLines(_keyFilePath, new[]
{
Convert.ToBase64String(key),
Convert.ToBase64String(iv)
});
}

// Encrypt the configuration
byte[] encryptedConfig;
using (Aes aes = Aes.Create())
{
aes.Key = key;
aes.IV = iv;

using (MemoryStream ms = new MemoryStream())
{
using (CryptoStream cs = new CryptoStream(ms, aes.CreateEncryptor(), CryptoStreamMode.Write))
{
byte[] jsonBytes = Encoding.UTF8.GetBytes(jsonConfig);
cs.Write(jsonBytes, 0, jsonBytes.Length);
}
encryptedConfig = ms.ToArray();
}
}

// Save the encrypted configuration
File.WriteAllText(_configFilePath, Convert.ToBase64String(encryptedConfig));
}

public T LoadConfiguration<T>()
{
if (!File.Exists(_configFilePath) || !File.Exists(_keyFilePath))
throw new FileNotFoundException("Configuration or key file not found");

// Read the key and IV
string[] keyData = File.ReadAllLines(_keyFilePath);
byte[] key = Convert.FromBase64String(keyData[0]);
byte[] iv = Convert.FromBase64String(keyData[1]);

// Read and decrypt the configuration
string encryptedBase64 = File.ReadAllText(_configFilePath);
byte[] encryptedConfig = Convert.FromBase64String(encryptedBase64);

string jsonConfig;
using (Aes aes = Aes.Create())
{
aes.Key = key;
aes.IV = iv;

using (MemoryStream ms = new MemoryStream(encryptedConfig))
{
using (CryptoStream cs = new CryptoStream(ms, aes.CreateDecryptor(), CryptoStreamMode.Read))
{
using (StreamReader sr = new StreamReader(cs))
{
jsonConfig = sr.ReadToEnd();
}
}
}
}

// Deserialize and return
return JsonSerializer.Deserialize<T>(jsonConfig);
}
}

// A sample configuration class
public class AppSettings
{
public string ConnectionString { get; set; }
public string ApiKey { get; set; }
public bool EnableLogging { get; set; }
}

Example Usage

csharp
// Create and save encrypted configuration
var settings = new AppSettings
{
ConnectionString = "Server=myserver;Database=mydb;User Id=sa;Password=p@ssw0rd!;",
ApiKey = "sk_test_abcdefghijklmnopqrstuvwxyz",
EnableLogging = true
};

var configManager = new SecureConfigManager("config.encrypted", "config.key");
configManager.SaveConfiguration(settings);
Console.WriteLine("Configuration saved securely.");

// Later, load the configuration
var loadedSettings = configManager.LoadConfiguration<AppSettings>();
Console.WriteLine($"Loaded connection string: {loadedSettings.ConnectionString}");
Console.WriteLine($"Loaded API key: {loadedSettings.ApiKey}");
Console.WriteLine($"Logging enabled: {loadedSettings.EnableLogging}");

Summary

In this guide, we've covered the basics of cryptography in .NET applications:

  1. Hashing: Secure password storage using SHA256 and salting
  2. Symmetric encryption: Data protection with AES
  3. Asymmetric encryption: Secure communication with RSA
  4. Digital signatures: Data integrity and authentication
  5. Secure random number generation: For cryptographic operations
  6. Real-world application: A secure configuration storage system

These cryptographic techniques form the foundation of security in .NET applications, enabling you to protect sensitive data, ensure data integrity, and authenticate users and systems securely.

Best Practices

  • Always use modern, standardized cryptographic algorithms (e.g., AES, SHA-256)
  • Never create your own cryptographic algorithms
  • Use appropriate key sizes (at least 128 bits for symmetric, 2048 bits for asymmetric)
  • Properly manage and protect encryption keys
  • Salt your hashes, especially for passwords
  • Use secure random number generation for cryptographic purposes
  • For passwords, use specialized algorithms like PBKDF2, Argon2, or Bcrypt
  • Keep your .NET Framework and cryptographic libraries updated

Additional Resources

Exercises

  1. Create a password-based file encryption system using AES
  2. Implement a secure token generator for a web API
  3. Build a hybrid encryption system that combines RSA and AES for large file encryption
  4. Create a digital signature verification system for document integrity
  5. Implement a secure key exchange protocol using Diffie-Hellman


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)