お疲れ様です! 
前回為止我們已經處理了「index」、「競態條件(唯一約束)」之類的內容,這次要來談的是 加密與雜湊。
使用 Rails 時經常會看到的 master.key、credentials.yml.enc、Devise 的密碼、cookies.signed。在這些背後,RSA、AES、bcrypt、SHA 都在運作。
不過,你是不是也曾經覺得「這些不就是全部都叫做『加密』嗎?」 
其實這 4 個東西,會分成 2 種不同類別。如果搞錯這裡,可能就會造成像是 「把密碼用 SHA 儲存」 這種會真的出事的事故。
最重要的就是這裡。這 4 個東西會依照 能不能還原 來分成兩類。
| 類別 | 能還原嗎? | 目的 | 代表 |
|---|---|---|---|
| 加密(encryption) | 可以(用金鑰解密) | 隱藏秘密,之後再讀取 | RSA / AES |
| 雜湊(hashing) | 不可以(單向) | 驗證一致性、偵測竄改 | SHA / bcrypt |
加密: "秘密" ──加密──> "x8#k2..." ──解密(鍵)──> "秘密" ✅ 可以來回
雜湊: "秘密" ──雜湊──> "a3f9..." ──✕ 無法還原 ❌ 單向
雜湊不是「加密」。因為它不能解密。把這兩者混為一談,是最常見的誤解。
為什麼雜湊不能還原?因為 輸入是無限的,但輸出是固定長度,資訊會被壓縮並丟棄一部分,所以根本不存在「還原」這個操作。
加密與解密使用「同一把金鑰」 的方式,所以稱為對稱。
require 'openssl'
cipher = OpenSSL::Cipher.new('aes-256-gcm')
cipher.encrypt
key = cipher.random_key # ← 這把金鑰會同時用於加密與解密
iv = cipher.random_iv
encrypted = cipher.update("秘密的資料") + cipher.final
使用 「公開金鑰」與「私密金鑰」 的一對金鑰。用公開金鑰加密,只有對應的私密金鑰才能解密。所以稱為非對稱。
require 'openssl'
rsa = OpenSSL::PKey::RSA.new(2048)
encrypted = rsa.public_key.public_encrypt("秘密") # 用公開金鑰加密(任何人都能做)
decrypted = rsa.private_decrypt(encrypted) # 只有持有私密金鑰的人才能解密
這就是用來解決 AES 弱點——金鑰配送問題——的方法。
公開金鑰 = 「鎖頭」、私密金鑰 = 「鑰匙」的概念:
・鎖頭(公開金鑰)可以放心發給任何人(只能上鎖)
・能打開的只有持有私密金鑰的人
→ 即使事前沒有共享秘密,也能安全地傳送加密資料 🙆
所以實務上會 搭配使用。
① 先用非對稱金鑰(RSA)安全地傳送「AES 的共通金鑰」
② 之後的大量資料,改用快速的對稱金鑰(AES)加密
這種「只有金鑰交換用 RSA,內容用 AES」的做法,就是後面會提到的 HTTPS 的本質 👇
單向 轉換。同樣的輸入一定會得到同樣的輸出,但不能從輸出回推輸入。
require 'digest'
Digest::SHA256.hexdigest("hello") # => "2cf24dba5fb0a30e..."
Digest::SHA256.hexdigest("hello!") # => "ce06092fb948d9ff..."(只差 1 個字元,結果就完全不同)
→ 工作就是確認「資料在傳輸過程中有沒有被改過」。
這裡是 最容易出事的地方。
「SHA 也是單向、不能還原,那不就可以拿來存密碼嗎?」——因為 SHA 太快了,所以不適合
假設攻擊者拿到了密碼雜湊
↓ SHA 超級快(1 秒可以算幾十億次)
↓ 把常見密碼全部拿去雜湊後逐一比對(暴力破解)
→ 短的、簡單的密碼一下子就被破解了 💥
bcrypt 是專門為密碼儲存設計的,而且是 故意做得很慢 的。
require 'bcrypt'
hashed = BCrypt::Password.create("my_password")
# => "$2a$12$C6UzMDM.H6dfI/f/IKcEeO..."(包含 salt,且刻意較慢)
BCrypt::Password.new(hashed) == "my_password" # => true
BCrypt::Password.new(hashed) == "wrong" # => false
bcrypt 優秀的原因有兩個:
$2a$12$ 裡面的 12 是計算成本。數字越高,每次運算越重,暴力破解效率就會被刻意降低。硬體變快時,也可以提高成本因子來跟上沒有 salt: password123 → "abc..."(大家都一樣 → 可被事前計算破解)
有 salt: password123 + salt_A → "xyz..."
password123 + salt_B → "def..."(同一密碼也會得到不同雜湊 🙆)
密碼請使用 「不是很快的 SHA」,而是「故意很慢且有 salt 的 bcrypt」。理由是它能抵抗暴力破解與彩虹表。
以上這些內容,其實就是 Rails 平常在背後默默做的事。
| Rails 功能 | 使用的技術 | 類別 |
|---|---|---|
credentials.yml.enc / master.key |
AES-128-GCM | 對稱金鑰・加密 |
Devise / has_secure_password |
bcrypt | 雜湊(密碼) |
cookies.signed / session |
HMAC-SHA | 雜湊(竄改檢查) |
cookies.encrypted / Active Record Encryption |
AES-256-GCM | 對稱金鑰・加密 |
master.key 與 credentials(= AES)config/credentials.yml.enc 是一種可以把 API 金鑰等秘密 加密後提交到 Git 的機制。內容是用 AES 加密 的,而負責解密的金鑰就是 master.key。
credentials.yml.enc(用 AES 加密後的內容,可提交到 Git)
↑ 解密/加密
master.key(對稱金鑰。不提交;正式環境則透過環境變數 RAILS_MASTER_KEY 傳入)
master.key 是 用於加密與解密的「同一把金鑰」=對稱金鑰(AES)。所以只要這把外洩,所有內容都會被讀出來 → 因此一定要加入 .gitignore。
Devise 的 encrypted_password(= 其實是雜湊!)Devise 的密碼會用 bcrypt 雜湊後,存進 encrypted_password 欄位。
# config/initializers/devise.rb
config.stretches = Rails.env.test? ? 1 : 12 # ← bcrypt 的成本因子(就是剛才的 "12")
欄位名稱雖然叫
encrypted_password,但 內容不是加密,而是雜湊,而且 不能解密。這是 Rails / Devise 常見的混淆點。
cookies.signed 與 cookies.encrypted(= SHA 與 AES)cookies.signed[:user_id] = 1 # 用 HMAC-SHA 做竄改檢查(內容可讀)
cookies.encrypted[:token] = "abc" # 用 AES 加密(內容也會被隱藏)
只想防止被竄改就用 signed;如果連內容也要隱藏,就用 encrypted。這正好就是本文「雜湊 vs 加密」的實際應用 🙆
到目前為止出現的角色,都會在 HTTPS 通訊裡一起組合使用。理解這點之後,整件事就會突然變得很清楚。
連上 HTTPS 時:
① 伺服器憑證驗證 … RSA/ECDSA(非對稱)+ SHA(簽章)
② 共通金鑰傳遞(金鑰交換)… RSA/ECDHE(非對稱)安全共享 AES 的金鑰
③ 實際資料傳輸 … AES(對稱)快速加密
④ 竄改檢查 … SHA 系(HMAC)確認資料是否損壞
→ 這就是前面說的「混合式加密」。「為什麼會有多種方式」的答案就藏在 HTTPS 裡 👍
master.key=AES、Devise 的 encrypted_password=其實是 bcrypt 雜湊、cookies.signed=SHA,全部都對應到本文的內容如果拿不定主意,可以用這樣的方式思考:「之後還要讀回來的東西用加密(AES),再也不需要還原的密碼用雜湊(bcrypt)」。
密碼學的原則是 不要自己從頭實作,改用成熟的函式庫(OpenSSL / bcrypt)。理解原理,同時把實作交給可靠工具,才是最安全的做法 🙆