資安筆記 - AES GCM 加解密
| | | 4 | |
講到對稱式加解密(加解密使用同一把金鑰),DES、TripleDES 已被 NIST(美國國家標準暨技術研究院)禁用多年,AES 是人類當前的王牌,即便量子電腦也打不穿(延伸閱讀:【打破砂鍋】量子電腦已成功破解 AES 加密?真的假的?),加上新一代處理器有支援硬體加速,是加密演算法的優先選擇。
依金鑰長度不同,AES 有 AES-128、AES-192、AES-256 三種規格。除了金鑰長度,AES 演算法還可選擇工作模式 (ECB/CBC/GCM) 跟填充模式 (ECB 與 CBC 需將明文資料補足到 128 bits 固定區塊大小,填充方法有 PKCS#5、PKCS#7)。ECG/CBC/GCM 比較如下:
| 特性 | ECB (Electronic Codebook) | CBC (Cipher Block Chaining) | GCM (Galois/Counter Mode) |
|---|---|---|---|
| 安全性 | 低 (會洩露模式) | 中 | 高 (具認證功能) |
| 效率 | 高 (可並行) | 低 (循序加密) | 極高 (可並行) |
| 需填充 | 是 | 是 | 否 |
| 防竄改功能 | 無 | 無 | 有 (AEAD) |
| 典型案例 | 已棄用 | 舊版 TLS、舊系統 | 現代網站 (HTTPS)、雲端服務 |
從今天的角度,應優先選擇 GCM,但 .NET 古蹟系統幾乎都是用 CBC,如之前 Bouncy Castle DES/AES 加解密所提,.NET Framework 時代流行的 AES 寫法會用密碼字串計算雜湊生成 Key 及 IV,而 .NET AES 函式的預設工作模式卻還是 CBC。
隨著時代演進,以前這麼做沒問題,不代表現在 OK,從資安角度「昨是今非」已是司空見慣。
2019/08 發佈的 NIST SP 800-52r2 標準,對 AES 的使用建議為:
- 相比於傳統 CBC,NIST 強烈建議使用 AES-GCM 和 CCM 模式以防止填充攻擊,且需要獨立的 HMAC
- 對稱加密必須要有 AEAD(Authenticated Encryption with Associated Data)同時提供「保密性」與「完整性」
- GCM 的安全性高度依賴 Nonce 的唯一性,計數器與 Nonce 不可重複或耗盡
近年來以上也慢慢成為企業資安合規的標準,原本 AES CBC 寫法,有可能在源碼檢測中被判定成弱點。為了提供未來 AES CBC 改 GCM 時參考,寫篇筆記好了。
(什麼?你說將來反正都是丟給 AI 改,不用學?是沒錯啦,這篇留給學點傳統技藝以備不時之需的同學看。)
先看傳統用密碼生成 Key 及 IV 並使用 CBC 模式的寫法 (以 .NET 為例):
using System.Security.Cryptography;
using System.Text;
public static class AesCbcNoRndIVTest
{
// CBC 之 IV 長度等於 AES 區塊大小:128 bits = 16 bytes
private const int IvSize = 16;
public static string Encrypt(string plain, string keyString)
{
using var aes = Aes.Create();
// 使用 keyString 生成固定的 AES key (32 bytes / AES-256) 和 IV (16 bytes)
// 結果不需包含 IV 較短,但同 Key 同明文的每次加密結果相同,較不安全
aes.Key = DeriveKey(keyString);
aes.IV = DeriveIv(keyString);
// 預設用 CBC / PKCS7,不用特別設定
using var encryptor = aes.CreateEncryptor();
byte[] plaintext = Encoding.UTF8.GetBytes(plain);
byte[] ciphertext = encryptor.TransformFinalBlock(plaintext, 0, plaintext.Length);
return Convert.ToBase64String(ciphertext);
}
public static string Decrypt(string encrypted, string keyString)
{
byte[] ciphertext = Convert.FromBase64String(encrypted);
using var aes = Aes.Create();
aes.Key = DeriveKey(keyString);
aes.IV = DeriveIv(keyString);
// 預設用 CBC / PKCS7,不用特別設定
// key、IV 或 padding 不正確時,解密失敗並拋出例外
using var decryptor = aes.CreateDecryptor();
byte[] plaintext = decryptor.TransformFinalBlock(ciphertext, 0, ciphertext.Length);
return Encoding.UTF8.GetString(plaintext);
}
// 用 SHA-256 從字串產生 32 bytes key,要更安全可用 PBKDF2/Argon2 搭配 salt
private static byte[] DeriveKey(string keyString) =>
SHA256.HashData(Encoding.UTF8.GetBytes(keyString));
// 不安全示範:從 keyString 產生固定 IV,取 SHA-256 結果前 16 bytes (正式用途建議用隨機 IV)
private static byte[] DeriveIv(string keyString) =>
SHA256.HashData(Encoding.UTF8.GetBytes($"iv:{keyString}"))[..IvSize];
}
以下是 AES GCM 做法,使用隨機 Nonce,加密時一併產生 Tag 提供完整性驗證,以符合現代資安要求。Nonce 與 Tag 要附加在加密結果中,故加密後的字串會比較長。
using System.Security.Cryptography;
using System.Text;
public static class AesGcmTest
{
// AES-256 金鑰長度:32 bytes
private const int KeySize = 32;
// 12 bytes nonce,讓同一把 Key 同內容每次加密結果不同
private const int NonceSize = 12;
// 認證標籤長度,用來驗證密文是否被竄改
private const int TagSize = 16;
public static string Encrypt(string plain, string keyString)
{
// 將使用者輸入的字串轉成固定長度 AES key
byte[] key = DeriveKey(keyString);
// nonce 不需要保密,但必須每次加密隨機產生新值
byte[] nonce = RandomNumberGenerator.GetBytes(NonceSize);
byte[] plaintext = Encoding.UTF8.GetBytes(plain);
byte[] ciphertext = new byte[plaintext.Length];
byte[] tag = new byte[TagSize];
// GCM 會同時加密並產生 tag,提供機密性與完整性驗證
using var aes = new AesGcm(key, TagSize);
aes.Encrypt(nonce, plaintext, ciphertext, tag);
// 將解密需要的 nonce、tag 和 ciphertext 一起輸出
return Convert.ToBase64String([.. nonce, .. tag, .. ciphertext]);
}
public static string Decrypt(string encrypted, string keyString)
{
byte[] data = Convert.FromBase64String(encrypted);
byte[] key = DeriveKey(keyString);
// 依照加密時的組合順序取回 nonce、tag、ciphertext
byte[] nonce = data[..NonceSize];
byte[] tag = data[NonceSize..(NonceSize + TagSize)];
byte[] ciphertext = data[(NonceSize + TagSize)..];
byte[] plaintext = new byte[ciphertext.Length];
// 若 key、nonce、tag 或密文不正確,Decrypt 會驗證失敗並拋出例外
using var aes = new AesGcm(key, TagSize);
aes.Decrypt(nonce, ciphertext, tag, plaintext);
return Encoding.UTF8.GetString(plaintext);
}
// 用 SHA-256 從字串產生 32 bytes 金鑰;若要更安全可用 PBKDF2/Argon2 搭配 Salt 產生
private static byte[] DeriveKey(string keyString) =>
SHA256.HashData(Encoding.UTF8.GetBytes(keyString));
}
測試程式及結果如下:
string plain = args.Length > 0 ? args[0] : "Hello, World!";
const string key = "my-demo-key";
Show("AES CBC", AesCbcNoRndIVTest.Encrypt, AesCbcNoRndIVTest.Decrypt);
Show("AES GCM", AesGcmTest.Encrypt, AesGcmTest.Decrypt);
void Show(string name, Func<string, string, string> encrypt, Func<string, string, string> decrypt)
{
string encrypted = encrypt(plain, key);
string decrypted = decrypt(encrypted, key);
Console.WriteLine($"[{name}]");
Console.WriteLine($"Plain : {plain} ({plain.Length})");
Console.WriteLine($"Encrypted : {encrypted} ({encrypted.Length})");
Console.WriteLine($"Decrypted : {decrypted}");
Console.WriteLine();
}

最後,來個隨堂測驗,為什麼 CBC 加密字串長度是 24 跟 44,GCM 結果為什麼是 56 跟 60?答對的話就表示真的有搞懂二者區別。
【解答】
- 明文
Hello World!13 字元- CBC 填補到 16 字元,IV 由 key 字串產生不需出現在加密結果,Base64 等於 Math.ceil(bytes / 3) * 4,16 bytes / 3 = 5.3 取 6, 6 * 4 = 24
- GCM 明文 13 字元(bytes) 免填補 + 12 bytes nonce + 16 bytes tag = 41 bytes / 3 = 13.7 取 14, 14 * 4 = 56
- 明文
0123456789ABCDEFG17 字元- CBC 填補到 32 字元,32 bytes / 3 = 10.7 取 11, 11 * 4 = 44
- GCM 明文 17 字元(bytes) + 12 bytes nonce + 16 bytes tag = 45 bytes / 3 = 15, 15 * 4 = 60
Explains why modern AES encryption should prefer GCM over legacy CBC, covering NIST guidance, AEAD integrity protection, nonce requirements, and .NET implementation examples comparing CBC and GCM output sizes.
Comments
# by cs8425
"AES 有多種金鑰長度,長度愈長,運算較耗時但抗破解能力愈強。"後面到"可知 SHA1 (128 bits)、SHA224、SHA256、SHA384、SHA512 都是常用的 AES 加密規格。" 感覺這段放的有點奇怪...? SHA系列是湊雜(hash) AES是對稱加密(symmetric encryption) hash跟對稱加密兩者會湊在一起用的時候通常是AEAD 但AES-GCM的auth tag沒有用SHA系列, 用的是GHASH (雖然但是, 自己用一樣的原理搞一套AEAD也不是不行) 另外吐槽下 .net提供的hash/加密函式都好難用啊... 之前有需求想用AES-GCM、chacha20-poly1305、ed25519 繞了好一大圈才搞定Orz
# by Jeffrey
to cs8425,噗,我應該是寫到大腦 Buffer Overflow 了,已修改,謝謝指正。 .NET 內建的加解密很陽春,只滿足基本應用,複雜一點的需求我都用 BouncyCastle,功能最齊全。
# by JeffPeng
請問一下 我系統原本是CBC 但如果要升級成GCM 每次加密的結果都不一樣 變成本來有拿加密的資料去當條件或是join之類 就無法使用了 但又不想新增一個欄位去存hash要改太多 是不是無解
# by Jeffrey
to JeffPeng,哈! GCM 加入 Nonce 該每次加密結果不同,目前之一就是怕有人用加密值做比對進行攻擊,剛好跟你的需求抵觸。