# 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 November 3, 2026

## 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:

```csharp
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)));
```

```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.

```csharp
using System.Security.Cryptography;

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

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

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

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

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

```text
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:

```csharp
#: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"));
```

```text
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.

```csharp
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($"Encrypted: {secret.Length} bytes");

string back = Encoding.UTF8.GetString(aes.DecryptCbc(secret, iv));
Console.WriteLine($"Decrypted: {back}");
```

```text
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

- [Hashing](https://www.ask-codey.com/glossary/hashing)
- [Salt](https://www.ask-codey.com/glossary/salt)
- [Encryption](https://www.ask-codey.com/glossary/encryption)

## Cheat sheets

- [Hashing and Encryption](https://www.ask-codey.com/cheat-sheets/security-basics-hashing-and-encryption-cheat-sheet)

Source: https://www.ask-codey.com/lessons/security-basics-hashing-and-encryption
