Hashing and Encryption

Hashing is one-way, a fingerprint you can check but never undo, so it's for passwords. Encryption is two-way with a key, so it's for data you need back. A password hash needs a random salt and many rounds, which is what PBKDF2 and ASP.NET Core's PasswordHasher do for you.

Journeyman · C# · Web Development · Updated

After this lesson you can

  • Explain the difference between hashing and encryption, and when to use each
  • Store and check a password as a salted, slow hash

Key takeaways

  • Hashing is one-way: you check a password by hashing it again, but can never turn the hash back. Encryption is two-way, with a key.
  • Hash passwords, never encrypt them: if the key leaks with the database, every password can be read.
  • A password hash needs a random salt per user and many rounds, so equal passwords get different hashes and guessing billions becomes hopeless.
  • Use PasswordHasher: HashPassword gives the string to store, and VerifyHashedPassword checks a password against it. Never write your own.
  • An encryption key must stay secret: never in your code, and never in Git.

A sign-in form sends a password to your API. The API has to remember every user's password well enough to check it next time, without keeping the password itself. If the database ever leaks, and databases do leak, nobody should be able to read the passwords in it. Two tools from cryptography help, and they do different jobs: hashing and encryption.

Hashing: a fingerprint

A hash turns any amount of data into a short, fixed-size value, like a fingerprint. .NET's SHA256 makes a 32-byte one:

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

Console.WriteLine(Fingerprint("hello"));
Console.WriteLine(Fingerprint("hello"));
Console.WriteLine(Fingerprint("Hello"));

static string Fingerprint(string text) =>
    Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(text)));
2CF24DBA5FB0A30E26E83B2AC5B9E29E1B161E5C1FA7425E73043362938B9824
2CF24DBA5FB0A30E26E83B2AC5B9E29E1B161E5C1FA7425E73043362938B9824
185F8DB32271FE25F561A6FC938B2E264306EC304EDA518007D1764826381969
  • The same input always gives the same hash, so you can check a password by hashing it again and comparing.
  • A tiny change, one capital letter, gives a completely different hash.
  • It's one-way: there's no way to turn the hash back into hello. A fingerprint tells you whose finger it is, but you can't grow a finger from it.

So an API stores the hash of each password. At sign-in, it hashes the password it's given, and compares the two hashes.

Not just any hash

A plain SHA-256 of a password isn't safe enough, for two reasons.

  1. Same password, same hash. Two users with the password summer2026 get identical hashes. An attacker with a leaked database sees that at once, and can look up hashes of common passwords in ready-made lists.
  2. It's fast. A computer can work out billions of SHA-256 hashes a second, so it can try every likely password against a leaked hash.

The fixes:

  • A salt. A random value, different for every user, mixed in before hashing. Two users with the same password now get different hashes, and ready-made lists are useless. The salt isn't secret: it's stored beside the hash.
  • Many rounds. A password hashing function, such as PBKDF2, hashes over and over, hundreds of thousands of times. Checking one password still takes a blink, but trying billions becomes hopeless.
using System.Security.Cryptography;

byte[] salt = RandomNumberGenerator.GetBytes(16);
byte[] hash = Rfc2898DeriveBytes.Pbkdf2("summer2026", salt, 600_000, HashAlgorithmName.SHA256, 32);

Console.WriteLine(quot;Salt: {Convert.ToBase64String(salt)}");
Console.WriteLine(quot;Hash: {Convert.ToBase64String(hash)}");

byte[] attempt = Rfc2898DeriveBytes.Pbkdf2("summer2026", salt, 600_000, HashAlgorithmName.SHA256, 32);
Console.WriteLine(quot;Right password: {CryptographicOperations.FixedTimeEquals(hash, attempt)}");

byte[] wrong = Rfc2898DeriveBytes.Pbkdf2("summer2025", salt, 600_000, HashAlgorithmName.SHA256, 32);
Console.WriteLine(quot;Wrong password: {CryptographicOperations.FixedTimeEquals(hash, wrong)}");

It prints something like this. Run it twice, and the salt and the hash are different each time:

Salt: q7mB0kq8xC8jTqK1dE0Wfw==
Hash: Wq5o0Zt8m7dE0p2b1x3Y9Q7cF4hJk6Lm8Nn0Pp2Rr4s=
Right password: True
Wrong password: False
  • RandomNumberGenerator.GetBytes(16) makes 16 random bytes for the salt. It's the random number generator for security; Random isn't random enough.
  • Rfc2898DeriveBytes.Pbkdf2 hashes the password with the salt, 600,000 times, with SHA-256, into 32 bytes.
  • CryptographicOperations.FixedTimeEquals compares two hashes in the same time whether they differ in the first byte or the last, so an attacker can't learn anything from how long a check takes.
  • Convert.ToBase64String turns the bytes into text you can store in a database column.

In your app: PasswordHasher

You don't need to write that yourself. ASP.NET Core's PasswordHasher, which you used in Cookie Authentication and [Authorize], does exactly this: a random salt, PBKDF2 with 100,000 rounds, and a fixed-time check. It packs the salt, the settings and the hash into one string for one database column:

#:sdk Microsoft.NET.Sdk.Web
using Microsoft.AspNetCore.Identity;

var hasher = new PasswordHasher<string>();

string stored = hasher.HashPassword("ada", "summer2026");
Console.WriteLine(stored.Length);

Console.WriteLine(hasher.VerifyHashedPassword("ada", stored, "summer2026"));
Console.WriteLine(hasher.VerifyHashedPassword("ada", stored, "summer2025"));
84
Success
Failed

PasswordHasher is part of ASP.NET Core, so in an API project it's simply there. The first line, #:sdk Microsoft.NET.Sdk.Web, gives this one-file program the same: try it with dotnet run hasher.cs.

HashPassword gives the string to store; VerifyHashedPassword checks a password against it. (The first argument is the user, which PasswordHasher doesn't use; a string is fine here.) Use it, and never write your own password hashing for a real app.

Encryption: a lockbox

Encryption scrambles data with a key, and the same key unscrambles it. It's two-way: a lockbox, not a fingerprint. Use it for data you need to read again, such as a saved credit card token or a message.

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

using Aes aes = Aes.Create();
byte[] iv = RandomNumberGenerator.GetBytes(16);

byte[] secret = aes.EncryptCbc(Encoding.UTF8.GetBytes("Meet at noon"), iv);
Console.WriteLine(quot;Encrypted: {secret.Length} bytes");

string back = Encoding.UTF8.GetString(aes.DecryptCbc(secret, iv));
Console.WriteLine(quot;Decrypted: {back}");
Encrypted: 16 bytes
Decrypted: Meet at noon
  • Aes.Create() makes an AES encryptor with a new random key. Anyone with the key can decrypt, so the key must be kept secret: never in your code, and never in Git. You'll keep a key in user secrets, a settings file outside the project, in Securing a Web API with JWT Bearer.
  • The IV (initialization vector) is a random starting value, so the same message encrypted twice looks different. Like a salt, it isn't secret.

You also use encryption every day without writing any: HTTPS encrypts everything between the browser and your API, so a password can't be read on its way.

Which one?

Hashing Encryption
Direction One-way: can't be undone Two-way: decrypt with the key
Needs a key No (a password hash uses a salt) Yes, kept secret
Use it for Passwords: you only ever need to check them Data you need to read again
In .NET PasswordHasher, Rfc2898DeriveBytes.Pbkdf2, SHA256 Aes, and HTTPS for data on the move

Never encrypt passwords: if the key leaks with the database, every password can be read. Hash them, so even you can't.

Check yourself

2 questions to try on the lesson page.

So what

Hash what you only need to check, with a salt and many rounds: that's every password, and PasswordHasher does it for you. Encrypt what you need to read again, and keep the key out of your code.

Key terms

Cheat sheets