加密與雜湊
雜湊(Hashing)vs 加密(Encryption)
💡 比喻
- 雜湊 = 碎紙機:把文件丟進去,出來的碎紙無法還原成原文件。 用途:確認兩份文件是否相同(碎出來的樣子一樣就是同一份)
- 加密 = 保險箱:把文件鎖進去,有鑰匙就能拿出來。 用途:保護文件不被別人看到,需要時可以解鎖取回
差異比較
特性 雜湊(Hash) 加密(Encryption)
──────────────────────────────────────────────────────
可逆性 不可逆(單向) 可逆(可解密)
用途 驗證完整性、密碼存儲 保護機密資料
輸出長度 固定長度 依輸入長度變化
鑰匙 不需要 需要金鑰
雜湊演算法
MD5 和 SHA-256
using System.Security.Cryptography;
using System.Text;
// MD5 雜湊(128 位元,已不安全)
var md5Input = "Hello, World!";
// 計算 MD5 雜湊值
var md5Bytes = MD5.HashData(Encoding.UTF8.GetBytes(md5Input));
// 轉換成十六進位字串
var md5Hash = Convert.ToHexString(md5Bytes).ToLower();
// 輸出:65a8e27d8879283831b664bd8b7f0ad4
Console.WriteLine($"MD5: {md5Hash}");
// SHA-256 雜湊(256 位元,目前安全)
var sha256Input = "Hello, World!";
// 計算 SHA-256 雜湊值
var sha256Bytes = SHA256.HashData(Encoding.UTF8.GetBytes(sha256Input));
// 轉換成十六進位字串
var sha256Hash = Convert.ToHexString(sha256Bytes).ToLower();
// 輸出:dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f
Console.WriteLine($"SHA-256: {sha256Hash}");
雜湊的特性:
├── 相同輸入 → 永遠相同輸出
├── 微小變化 → 完全不同輸出(雪崩效應)
├── 無法從輸出反推輸入
└── 不同輸入可能相同輸出(碰撞),但機率極低
MD5 vs SHA-256:
├── MD5:速度快,但已被發現碰撞攻擊,不安全
└── SHA-256:目前安全,用於數位簽章、區塊鏈等
密碼雜湊:bcrypt 和 Argon2
為什麼不能用 SHA-256 存密碼?
SHA-256 太快了!駭客可以用 GPU 每秒嘗試數十億次。
bcrypt/Argon2 的設計:
├── 故意很慢(可調整難度)
├── 內建 Salt(防止彩虹表攻擊)
└── 每次雜湊結果都不同(因為 Salt 不同)
bcrypt 範例
// 安裝套件:dotnet add package BCrypt.Net-Next
using BCrypt.Net;
// 雜湊密碼(註冊時)
var password = "mypassword123";
// 自動產生 Salt 並雜湊,workFactor=12 控制難度
var hashedPassword = BCrypt.Net.BCrypt.HashPassword(password, workFactor: 12);
// 輸出類似:$2a$12$LJ3m4ys1Y2bCxYz...(每次都不同!)
Console.WriteLine(hashedPassword);
// 驗證密碼(登入時)
var inputPassword = "mypassword123";
// 比對使用者輸入的密碼和資料庫中的雜湊值
var isValid = BCrypt.Net.BCrypt.Verify(inputPassword, hashedPassword);
// 輸出:True
Console.WriteLine($"密碼正確:{isValid}");
// 錯誤密碼
var wrongPassword = "wrongpassword";
// 錯誤密碼會回傳 false
var isWrong = BCrypt.Net.BCrypt.Verify(wrongPassword, hashedPassword);
// 輸出:False
Console.WriteLine($"密碼正確:{isWrong}");
Salt(鹽)
什麼是 Salt?
Salt 是一串隨機字串,加在密碼前面再雜湊。
沒有 Salt 的問題:
├── 相同密碼 → 相同雜湊值
├── 駭客可以用「彩虹表」(預先計算好的雜湊對照表)快速破解
└── 一旦知道一個人的密碼,所有用同密碼的人都被破解
有 Salt:
├── "password123" + "abc" → 雜湊 A
├── "password123" + "xyz" → 雜湊 B(完全不同!)
└── 每個使用者有不同的 Salt,彩虹表無效
// 手動加 Salt 的概念(bcrypt 自動處理)
var password2 = "mypassword";
// 產生隨機 Salt
var salt = new byte[16];
RandomNumberGenerator.Fill(salt);
// 把 Salt 加在密碼前面
var saltedPassword = Convert.ToBase64String(salt) + password2;
// 再做雜湊
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(saltedPassword));
// 注意:實務上請用 bcrypt 或 Argon2,它們自動處理 Salt
對稱加密:AES
對稱加密:加密和解密用同一把鑰匙
比喻:你和朋友各有一把相同的鑰匙,都能開同一個保險箱
優點:速度快
缺點:如何安全地把鑰匙給對方?
using System.Security.Cryptography;
// AES 對稱加密範例
public static class AesHelper
{
// 加密
public static byte[] Encrypt(string plainText, byte[] key, byte[] iv)
{
// 建立 AES 加密器
using var aes = Aes.Create();
aes.Key = key; // 設定金鑰(256 位元)
aes.IV = iv; // 設定初始向量(128 位元)
// 建立加密轉換器
using var encryptor = aes.CreateEncryptor();
// 把明文轉成 byte 陣列
var plainBytes = Encoding.UTF8.GetBytes(plainText);
// 執行加密,回傳加密後的 byte 陣列
return encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
}
// 解密
public static string Decrypt(byte[] cipherBytes, byte[] key, byte[] iv)
{
// 建立 AES 解密器
using var aes = Aes.Create();
aes.Key = key; // 必須用相同的金鑰
aes.IV = iv; // 必須用相同的初始向量
// 建立解密轉換器
using var decryptor = aes.CreateDecryptor();
// 執行解密
var plainBytes = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);
// 把 byte 陣列轉回字串
return Encoding.UTF8.GetString(plainBytes);
}
}
// 使用範例
var key = new byte[32]; // 256 位元金鑰
var iv = new byte[16]; // 128 位元初始向量
// 產生隨機金鑰和 IV
RandomNumberGenerator.Fill(key);
RandomNumberGenerator.Fill(iv);
// 加密
var encrypted = AesHelper.Encrypt("機密資料", key, iv);
// 解密(用相同的 key 和 iv)
var decrypted = AesHelper.Decrypt(encrypted, key, iv);
// 輸出:機密資料
Console.WriteLine(decrypted);
非對稱加密:RSA
非對稱加密:有兩把鑰匙——公鑰和私鑰
比喻:
├── 公鑰 = 郵筒的投信口(任何人都能投信進去)
└── 私鑰 = 郵筒的鑰匙(只有你能打開取信)
用公鑰加密 → 只有私鑰能解密
用私鑰簽章 → 公鑰可以驗證簽章
using System.Security.Cryptography;
// RSA 非對稱加密範例
using var rsa = RSA.Create(2048); // 產生 2048 位元的金鑰對
// 匯出公鑰和私鑰
var publicKey = rsa.ExportRSAPublicKey(); // 可以公開給任何人
var privateKey = rsa.ExportRSAPrivateKey(); // 必須保密!
// 用公鑰加密(任何人都能加密)
var plainText = "機密訊息";
var plainBytes = Encoding.UTF8.GetBytes(plainText);
// 使用 OAEP 填充模式(比 PKCS1 更安全)
var encrypted2 = rsa.Encrypt(plainBytes, RSAEncryptionPadding.OaepSHA256);
// 用私鑰解密(只有持有私鑰的人能解密)
var decrypted2 = rsa.Decrypt(encrypted2, RSAEncryptionPadding.OaepSHA256);
// 把 byte 陣列轉回字串
Console.WriteLine(Encoding.UTF8.GetString(decrypted2));
HTTPS 與 TLS 憑證
HTTPS 結合了對稱和非對稱加密:
1. 非對稱加密(RSA)→ 安全地交換對稱金鑰
2. 對稱加密(AES) → 之後的通訊都用快速的對稱加密
為什麼不全程用 RSA?
因為 RSA 太慢了!只用來交換 AES 的金鑰。
TLS 憑證的作用:
├── 證明伺服器的身份(CA 機構簽發)
├── 包含伺服器的公鑰
└── 瀏覽器用公鑰加密「對稱金鑰」傳給伺服器
🤔 我這樣寫為什麼會錯?
❌ 錯誤 1:用 MD5 或 SHA-256 存密碼
// ❌ 錯誤:用 SHA-256 雜湊密碼
var password3 = "user_password";
// SHA-256 太快了,駭客可以暴力破解
var hash3 = SHA256.HashData(Encoding.UTF8.GetBytes(password3));
// 存入資料庫...(危險!)
// ✅ 正確:用 bcrypt(故意很慢,防暴力破解)
var safeHash = BCrypt.Net.BCrypt.HashPassword(password3, workFactor: 12);
// bcrypt 自帶 Salt,每次結果不同
// 存入資料庫...(安全!)
❌ 錯誤 2:把金鑰寫死在程式碼中
// ❌ 錯誤:金鑰寫死在原始碼中
var secretKey = "my-super-secret-key-12345"; // 推上 Git 就洩漏了!
// ✅ 正確:從環境變數或 Secret Manager 讀取
// 開發環境用 User Secrets
// dotnet user-secrets set "Jwt:Key" "your-secret-key"
var secretKey2 = builder.Configuration["Jwt:Key"];
// 正式環境用環境變數或 Azure Key Vault
// 環境變數:export Jwt__Key="your-secret-key"
var secretKey3 = Environment.GetEnvironmentVariable("JWT_KEY");
❌ 錯誤 3:自己發明加密演算法
❌ 錯誤:「我把每個字元的 ASCII 碼加 3,這樣就加密了!」
這不是加密,這是凱薩密碼,2000 年前就被破解了。
✅ 正確:使用經過驗證的加密演算法
├── 對稱加密 → AES-256
├── 非對稱加密 → RSA-2048 或 ECDSA
├── 密碼雜湊 → bcrypt 或 Argon2
└── 雜湊 → SHA-256 或 SHA-3
永遠不要自己發明加密演算法!
💡 重點整理
| 概念 | 說明 |
|---|---|
| Hash(雜湊) | 單向不可逆,用於驗證和密碼存儲 |
| Encryption(加密) | 可逆,用於保護機密資料 |
| bcrypt/Argon2 | 專門設計來存密碼的雜湊演算法(故意很慢) |
| Salt | 隨機字串加在密碼前面,防止彩虹表攻擊 |
| AES | 對稱加密(同一把鑰匙加解密) |
| RSA | 非對稱加密(公鑰加密、私鑰解密) |
| TLS | HTTPS 底層,結合對稱和非對稱加密 |