TL;DR
Using PBKDF2 (Password-Based Key Derivation Function 2) on a key generated by RNGCryptoServiceProvider significantly improves security. RNGCryptoServiceProvider creates random keys, but they’re vulnerable to brute-force attacks if compromised. PBKDF2 adds salt and iteration counts, making cracking the key much harder even if an attacker gets hold of it.
Why Use PBKDF2 with RNGCryptoServiceProvider?
RNGCryptoServiceProvider is a good source of random numbers for generating keys. However, simply storing this raw key isn’t enough in most real-world scenarios. Here’s why:
- Brute-Force Attacks: If an attacker gets access to your stored key (e.g., through a database breach), they can try every possible combination until they find the correct one.
- Rainbow Tables: Precomputed tables of hashes can be used to quickly reverse engineer keys if they’re not properly protected.
PBKDF2 addresses these issues by:
- Salting: Adding a unique, random value (the salt) to the key before hashing makes rainbow table attacks ineffective. Each key has a different salt, so precomputed tables won’t work.
- Iteration Count: Repeating the hashing process many times (iteration count) dramatically increases the time it takes to crack the key. This makes brute-force attacks impractical.
Step-by-Step Guide
- Generate a Random Key with RNGCryptoServiceProvider:
using System; using System.Security.Cryptography; public class Example { public static byte[] GenerateRandomKey(int keyLength) { byte[] key = new byte[keyLength]; using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider()) { rng.GetBytes(key); } return key; } } - Generate a Random Salt: The salt should be at least 16 bytes long.
public static byte[] GenerateRandomSalt(int saltLength) { byte[] salt = new byte[saltLength]; using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider()) { rng.GetBytes(salt); } return salt; } - Derive the Key using PBKDF2: This is where the magic happens.
using System.Security.Cryptography; public static byte[] DeriveKey(byte[] password, byte[] salt, int iterationCount, int keyLength) { using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterationCount)) { return pbkdf2.GetBytes(keyLength); } }Important: Choose a high iteration count (e.g., 10,000 or higher). The higher the count, the more secure, but also the slower the process.
- Store the Salt and Derived Key: Store both the salt *and* the derived key in your database.
Do not store the original password!
- Verification (when a user logs in):
- Retrieve the stored salt for the user.
- Hash the entered password using PBKDF2 with the retrieved salt and iteration count.
- Compare the resulting hash to the stored derived key. If they match, the password is correct.
Example Code (Complete)
using System;
using System.Security.Cryptography;
public class Example {
public static byte[] GenerateRandomKey(int keyLength) {
byte[] key = new byte[keyLength];
using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider()) {
rng.GetBytes(key);
}
return key;
}
public static byte[] GenerateRandomSalt(int saltLength) {
byte[] salt = new byte[saltLength];
using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider()) {
rng.GetBytes(salt);
}
return salt;
}
public static byte[] DeriveKey(byte[] password, byte[] salt, int iterationCount, int keyLength) {
using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterationCount)) {
return pbkdf2.GetBytes(keyLength);
}
}
public static bool VerifyPassword(string password, byte[] storedSalt, byte[] storedDerivedKey, int iterationCount) {
byte[] derivedKey = DeriveKey(System.Text.Encoding.UTF8.GetBytes(password), storedSalt, iterationCount, storedDerivedKey.Length);
return CompareByteArrays(derivedKey, storedDerivedKey);
}
private static bool CompareByteArrays(byte[] arr1, byte[] arr2) {
if (arr1.Length != arr2.Length) return false;
for (int i = 0; i < arr1.Length; i++) {
if (arr1[i] != arr2[i]) return false;
}
return true;
}
public static void Main(string[] args) {
int keyLength = 32; // Example key length
int saltLength = 16;
int iterationCount = 10000;
byte[] password = System.Text.Encoding.UTF8.GetBytes("mySecretPassword");
byte[] salt = GenerateRandomSalt(saltLength);
byte[] derivedKey = DeriveKey(password, salt, iterationCount, keyLength);
// Store salt and derivedKey in your database.
bool isValid = VerifyPassword("mySecretPassword", salt, derivedKey, iterationCount);
Console.WriteLine("Password is valid: " + isValid);
}
}
Important Considerations
- Iteration Count: Increase the iteration count as computing power increases to maintain security.
- Salt Length: Use a salt length of at least 16 bytes (128 bits).
- Key Length: Choose an appropriate key length for your encryption algorithm (e.g., 32 bytes for AES-256).