.NET Hashing
Introduction
Hashing is a fundamental concept in computer security that transforms any data (regardless of its size) into a fixed-length string of characters. This transformation is designed to be one-way, meaning it should be computationally infeasible to reverse the process and obtain the original data from the hash value.
In .NET applications, hashing serves several critical purposes:
- Password storage: Storing password hashes instead of plaintext passwords
- Data integrity: Verifying that data hasn't been tampered with
- Data identification: Quickly identifying duplicate data
- Digital signatures: Supporting authentication mechanisms
In this tutorial, we'll explore how to implement various hashing algorithms in .NET, understand their differences, and learn best practices for secure application development.
Hashing Basics in .NET
The .NET Framework provides robust support for hashing through the System.Security.Cryptography
namespace. This namespace contains several classes that implement different hashing algorithms.
Common Hashing Algorithms in .NET
- MD5: Fast but cryptographically broken (128-bit hash)
- SHA-1: Faster than SHA-256 but considered cryptographically weak (160-bit hash)
- SHA-256: Good balance between security and performance (256-bit hash)
- SHA-384/SHA-512: Stronger but slower (384/512-bit hash)
- HMAC: Hash-based Message Authentication Code (combines a hash with a secret key)
- BCrypt/PBKDF2: Password-specific algorithms with salt and work factor
Implementing Basic Hashing in .NET
Let's start by implementing a simple hash function using SHA-256:
using System;
using System.Security.Cryptography;
using System.Text;
public class BasicHashing
{
public static string ComputeSha256Hash(string rawData)
{
// Create a SHA256
using (SHA256 sha256Hash = SHA256.Create())
{
// ComputeHash - returns byte array
byte[] bytes = sha256Hash.ComputeHash(Encoding.UTF8.GetBytes(rawData));
// Convert byte array to a string
StringBuilder builder = new StringBuilder();
for (int i = 0; i < bytes.Length; i++)
{
builder.Append(bytes[i].ToString("x2"));
}
return builder.ToString();
}
}
public static void Main()
{
string input = "Hello, World!";
string hashedResult = ComputeSha256Hash(input);
Console.WriteLine($"Input: {input}");
Console.WriteLine($"SHA-256 Hash: {hashedResult}");
}
}
Output:
Input: Hello, World!
SHA-256 Hash: dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f
This example demonstrates a basic implementation of SHA-256 hashing. Notice that:
- We use
SHA256.Create()
to get an instance of the SHA-256 algorithm - The
ComputeHash
method returns a byte array which we convert to a hexadecimal string - The same input will always produce the same hash value (deterministic property)
Using Different Hashing Algorithms
.NET provides multiple hashing algorithms for different needs. Here's how to use several common ones:
using System;
using System.Security.Cryptography;
using System.Text;
public class MultipleHashingAlgorithms
{
public static string ComputeHash(HashAlgorithm hashAlgorithm, string input)
{
// Convert the input string to a byte array and compute the hash
byte[] data = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(input));
// Create a new StringBuilder to collect the bytes
// and create a string
StringBuilder builder = new StringBuilder();
// Loop through each byte of the hashed data
// and format each one as a hexadecimal string
for (int i = 0; i < data.Length; i++)
{
builder.Append(data[i].ToString("x2"));
}
// Return the hexadecimal string
return builder.ToString();
}
public static void Main()
{
string input = "Hello, World!";
// MD5 - Not recommended for security purposes
using (MD5 md5 = MD5.Create())
{
Console.WriteLine($"MD5: {ComputeHash(md5, input)}");
}
// SHA1 - Not recommended for security purposes
using (SHA1 sha1 = SHA1.Create())
{
Console.WriteLine($"SHA1: {ComputeHash(sha1, input)}");
}
// SHA256 - Recommended for most purposes
using (SHA256 sha256 = SHA256.Create())
{
Console.WriteLine($"SHA256: {ComputeHash(sha256, input)}");
}
// SHA384 - Higher security
using (SHA384 sha384 = SHA384.Create())
{
Console.WriteLine($"SHA384: {ComputeHash(sha384, input)}");
}
// SHA512 - Highest security, but slower
using (SHA512 sha512 = SHA512.Create())
{
Console.WriteLine($"SHA512: {ComputeHash(sha512, input)}");
}
}
}
Output:
MD5: 65a8e27d8879283831b664bd8b7f0ad4
SHA1: 0a0a9f2a6772942557ab5355d76af442f8f65e01
SHA256: dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f
SHA384: 5485cc9b3365b4305dfb4e8337e0a598a574f8242bf17289e0dd6c20a3cd44a089de16ab4ab308f63e44b1170eb5f515
SHA512: 374d794a95cdcfd8b35993185fef9ba368f160d8daf432d08ba9f1ed1e5abe6cc69291e0fa2fe0006a52570ef18c19def4e617c33ce52ef0a6e5fbe318cb0387
Notice that:
- Different algorithms produce different length outputs
- Longer hash outputs generally provide more security against collisions
Password Hashing in .NET
For password hashing, you should never use basic hash functions like SHA-256 alone. Instead, use specialized password hashing functions that include:
- Salt: Random data added to the password before hashing
- Work factor (iterations): Makes the hashing process slower to resist brute-force attacks
.NET provides the Rfc2898DeriveBytes
class (implements PBKDF2) for secure password hashing:
using System;
using System.Security.Cryptography;
using System.Text;
public class PasswordHashing
{
// Size of salt
private const int SaltSize = 16;
// Size of hash
private const int HashSize = 32;
// Iterations (work factor)
private const int Iterations = 10000;
public static string HashPassword(string password)
{
// Generate a random salt
byte[] salt = new byte[SaltSize];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(salt);
}
// Create hash with salt and iterations
byte[] hash = new Rfc2898DeriveBytes(password, salt, Iterations).GetBytes(HashSize);
// Combine salt and hash
byte[] hashBytes = new byte[SaltSize + HashSize];
Array.Copy(salt, 0, hashBytes, 0, SaltSize);
Array.Copy(hash, 0, hashBytes, SaltSize, HashSize);
// Convert to base64 for storage
return Convert.ToBase64String(hashBytes);
}
public static bool VerifyPassword(string password, string hashedPassword)
{
// Convert base64-encoded hash to byte array
byte[] hashBytes = Convert.FromBase64String(hashedPassword);
// Extract salt (first SaltSize bytes)
byte[] salt = new byte[SaltSize];
Array.Copy(hashBytes, 0, salt, 0, SaltSize);
// Compute hash of the current password
byte[] hash = new Rfc2898DeriveBytes(password, salt, Iterations).GetBytes(HashSize);
// Check if computed hash matches the stored hash
for (int i = 0; i < HashSize; i++)
{
if (hashBytes[i + SaltSize] != hash[i])
{
return false;
}
}
return true;
}
public static void Main()
{
string password = "MySecurePassword123!";
// Hash the password
string hashedPassword = HashPassword(password);
Console.WriteLine($"Hashed Password: {hashedPassword}");
// Verify the password
bool isPasswordCorrect = VerifyPassword(password, hashedPassword);
Console.WriteLine($"Password verification result: {isPasswordCorrect}");
// Try with wrong password
bool isWrongPasswordCorrect = VerifyPassword("WrongPassword", hashedPassword);
Console.WriteLine($"Wrong password verification result: {isWrongPasswordCorrect}");
}
}
Output:
Hashed Password: (base64 string - will be different each time due to random salt)
Password verification result: True
Wrong password verification result: False
In this example:
- We generate a random salt for each password
- We use PBKDF2 (via
Rfc2898DeriveBytes
) with 10,000 iterations - We store the salt with the hash to allow verification later
- We can verify a password without ever storing the original password
HMAC - Keyed Hashing for Message Authentication
Hash-based Message Authentication Code (HMAC) combines hashing with a secret key to verify both the data integrity and authentication.
using System;
using System.Security.Cryptography;
using System.Text;
public class HmacExample
{
public static string ComputeHmacSha256(string message, string key)
{
byte[] keyBytes = Encoding.UTF8.GetBytes(key);
byte[] messageBytes = Encoding.UTF8.GetBytes(message);
using (HMACSHA256 hmac = new HMACSHA256(keyBytes))
{
byte[] hashBytes = hmac.ComputeHash(messageBytes);
return BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
}
}
public static void Main()
{
string message = "This is a message that needs authentication";
string key = "MySecretKey12345";
string hmacResult = ComputeHmacSha256(message, key);
Console.WriteLine($"Message: {message}");
Console.WriteLine($"Key: {key}");
Console.WriteLine($"HMAC-SHA256: {hmacResult}");
// Verify HMAC with the correct key
string verificationHmac = ComputeHmacSha256(message, key);
bool isValid = hmacResult.Equals(verificationHmac);
Console.WriteLine($"HMAC verification: {isValid}");
// Try with incorrect key
string incorrectVerificationHmac = ComputeHmacSha256(message, "WrongKey");
bool isInvalidKeyValid = hmacResult.Equals(incorrectVerificationHmac);
Console.WriteLine($"HMAC verification with wrong key: {isInvalidKeyValid}");
}
}
Output:
Message: This is a message that needs authentication
Key: MySecretKey12345
HMAC-SHA256: 7f77889af6a29e681741327539e2b697d7633363774212a05336efa5319a9797
HMAC verification: True
HMAC verification with wrong key: False
HMAC is useful for:
- API authentication tokens
- Message integrity verification
- Cookie data authentication
Real-World Application: File Integrity Verification
Let's create a practical example that checks the integrity of a file using hashing:
using System;
using System.IO;
using System.Security.Cryptography;
public class FileIntegrityChecker
{
public static string CalculateFileHash(string filePath)
{
using (FileStream stream = File.OpenRead(filePath))
{
using (SHA256 sha = SHA256.Create())
{
byte[] hash = sha.ComputeHash(stream);
return BitConverter.ToString(hash).Replace("-", "").ToLower();
}
}
}
public static void SaveHashToFile(string filePath, string hash)
{
File.WriteAllText(filePath + ".hash", hash);
}
public static bool VerifyFileIntegrity(string filePath)
{
string storedHashFilePath = filePath + ".hash";
if (!File.Exists(storedHashFilePath))
{
Console.WriteLine("Hash file not found. Creating new hash file.");
string newHash = CalculateFileHash(filePath);
SaveHashToFile(filePath, newHash);
return true;
}
string storedHash = File.ReadAllText(storedHashFilePath);
string currentHash = CalculateFileHash(filePath);
bool match = storedHash.Equals(currentHash, StringComparison.OrdinalIgnoreCase);
if (match)
{
Console.WriteLine("File integrity verified: File has not been modified.");
}
else
{
Console.WriteLine("File integrity check FAILED: File has been modified!");
}
return match;
}
public static void Main()
{
// Example usage:
// 1. Create a test file
string testFilePath = "test_file.txt";
File.WriteAllText(testFilePath, "This is a test file content.");
// 2. Generate and save its hash
string originalHash = CalculateFileHash(testFilePath);
SaveHashToFile(testFilePath, originalHash);
Console.WriteLine($"Original file hash: {originalHash}");
// 3. Verify integrity (should pass)
VerifyFileIntegrity(testFilePath);
// 4. Modify the file
File.AppendAllText(testFilePath, "\nThis file has been modified!");
// 5. Verify integrity again (should fail)
VerifyFileIntegrity(testFilePath);
}
}
Output:
Original file hash: 2c8b08da5ce60398e1f19af0e5dccc744df274b826abe585eaba68c525434806
File integrity verified: File has not been modified.
File integrity check FAILED: File has been modified!
This example demonstrates how hashing can be used to verify if a file has been modified, which is useful for:
- Software downloads (to check for corruption)
- Configuration files (to detect tampering)
- System file integrity monitoring
Best Practices for Hashing in .NET
- Never use MD5 or SHA-1 for security purposes - They are considered cryptographically broken.
- Use SHA-256 or higher for general hashing needs - SHA-256 offers a good balance between security and performance.
- For password hashing:
- Always use a salt
- Use specialized password hashing functions (PBKDF2, BCrypt, or Argon2)
- Use a high iteration count/work factor
- Ensure the hash algorithm is configurable in your code, so you can easily upgrade if weaknesses are found.
- Use constant-time comparison when comparing hash values to prevent timing attacks.
- Keep secrets secure - When using HMAC, the key must remain secret.
- Use secure random number generation for salt creation (use
RandomNumberGenerator
class, notRandom
).
Summary
In this tutorial, we've explored:
- Basic concepts of hashing in .NET
- Implementing various hashing algorithms (SHA-256, SHA-512, etc.)
- Secure password hashing with salt and iterations
- Using HMAC for authenticated hashing
- Practical application of hashing for file integrity verification
Hashing is a critical tool in your security toolkit, but it must be implemented correctly to provide security benefits. By following the best practices outlined in this tutorial, you can use hashing effectively in your .NET applications.
Further Exercises
- Create a simple file download system that verifies downloaded files against provided hash values.
- Implement a password reset system using secure password hashing techniques.
- Create a tool that can generate and verify checksums for a directory of files.
- Implement a simple API authentication system using HMAC.
- Experiment with different hashing algorithms and compare their performance using
System.Diagnostics.Stopwatch
.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)