お疲れ様です! :pencil:

前回為止我們已經處理了「index」、「競態條件(唯一約束)」之類的內容,這次要來談的是 加密與雜湊

使用 Rails 時經常會看到的 master.keycredentials.yml.enc、Devise 的密碼、cookies.signed。在這些背後,RSA、AES、bcrypt、SHA 都在運作。

不過,你是不是也曾經覺得「這些不就是全部都叫做『加密』嗎?」 :thinking:

其實這 4 個東西,會分成 2 種不同類別。如果搞錯這裡,可能就會造成像是 「把密碼用 SHA 儲存」 這種會真的出事的事故。


首先最重要的一點:「加密」與「雜湊」是不同的東西

最重要的就是這裡。這 4 個東西會依照 能不能還原 來分成兩類。

類別 能還原嗎? 目的 代表
加密(encryption) 可以(用金鑰解密) 隱藏秘密,之後再讀取 RSA / AES
雜湊(hashing) 不可以(單向) 驗證一致性、偵測竄改 SHA / bcrypt
加密:  "秘密" ──加密──> "x8#k2..." ──解密(鍵)──> "秘密"   ✅ 可以來回
雜湊:  "秘密" ──雜湊──> "a3f9..." ──✕ 無法還原          ❌ 單向

:point_up: 雜湊不是「加密」。因為它不能解密。把這兩者混為一談,是最常見的誤解。

為什麼雜湊不能還原?因為 輸入是無限的,但輸出是固定長度,資訊會被壓縮並丟棄一部分,所以根本不存在「還原」這個操作。


加密①:對稱金鑰 AES 🔑

加密與解密使用「同一把金鑰」 的方式,所以稱為對稱。

require 'openssl'

cipher = OpenSSL::Cipher.new('aes-256-gcm')
cipher.encrypt
key = cipher.random_key   # ← 這把金鑰會同時用於加密與解密
iv  = cipher.random_iv
encrypted = cipher.update("秘密的資料") + cipher.final
  • 🙆 速度快,適合大量資料的加密
  • 😤 但是 金鑰要怎麼傳給對方?(也就是金鑰配送問題)。因為雙方必須共用同一把金鑰,但要安全地把這把金鑰傳過去,本身就需要其他方法

加密②:非對稱金鑰 RSA 🔑🔓

使用 「公開金鑰」與「私密金鑰」 的一對金鑰。用公開金鑰加密,只有對應的私密金鑰才能解密。所以稱為非對稱。

require 'openssl'

rsa = OpenSSL::PKey::RSA.new(2048)
encrypted = rsa.public_key.public_encrypt("秘密")  # 用公開金鑰加密(任何人都能做)
decrypted = rsa.private_decrypt(encrypted)          # 只有持有私密金鑰的人才能解密

這就是用來解決 AES 弱點——金鑰配送問題——的方法。

公開金鑰 = 「鎖頭」、私密金鑰 = 「鑰匙」的概念:
  ・鎖頭(公開金鑰)可以放心發給任何人(只能上鎖)
  ・能打開的只有持有私密金鑰的人
→ 即使事前沒有共享秘密,也能安全地傳送加密資料 🙆
  • 🙆 不需要事先共享秘密。也可用於 數位簽章
  • 😤 但 速度慢。不適合大量資料

那為什麼兩種都要有? → 取長補短(混合式)

  • 對稱金鑰(AES):速度快,但金鑰無法安全傳遞
  • 非對稱金鑰(RSA):可以安全傳遞金鑰,但速度慢

所以實務上會 搭配使用

① 先用非對稱金鑰(RSA)安全地傳送「AES 的共通金鑰」
② 之後的大量資料,改用快速的對稱金鑰(AES)加密

這種「只有金鑰交換用 RSA,內容用 AES」的做法,就是後面會提到的 HTTPS 的本質 👇


雜湊①:SHA(竄改檢查・Checksum)

單向 轉換。同樣的輸入一定會得到同樣的輸出,但不能從輸出回推輸入。

require 'digest'

Digest::SHA256.hexdigest("hello")   # => "2cf24dba5fb0a30e..."
Digest::SHA256.hexdigest("hello!")  # => "ce06092fb948d9ff..."(只差 1 個字元,結果就完全不同)
  • 用途:檔案竄改檢查、Checksum、數位簽章
  • 特性:速度快、固定長度、些微差異就會得到完全不同的值

→ 工作就是確認「資料在傳輸過程中有沒有被改過」。


雜湊②:bcrypt(密碼儲存)與「為什麼不能用 SHA」💥

這裡是 最容易出事的地方

不要用 SHA 儲存密碼

「SHA 也是單向、不能還原,那不就可以拿來存密碼嗎?」——因為 SHA 太快了,所以不適合

假設攻擊者拿到了密碼雜湊
  ↓ SHA 超級快(1 秒可以算幾十億次)
  ↓ 把常見密碼全部拿去雜湊後逐一比對(暴力破解)
→ 短的、簡單的密碼一下子就被破解了 💥

✅ 密碼請用 bcrypt(或 argon2 / scrypt)

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):在雜湊前混入隨機值,所以 即使是同一個密碼,不同的人也會得到不同的雜湊值 → 彩虹表無法發揮作用
沒有 salt: password123 → "abc..."(大家都一樣 → 可被事前計算破解)
有 salt:   password123 + salt_A → "xyz..."
            password123 + salt_B → "def..."(同一密碼也會得到不同雜湊 🙆)

:point_up: 密碼請使用 「不是很快的 SHA」,而是「故意很慢且有 salt 的 bcrypt」。理由是它能抵抗暴力破解與彩虹表。


Rails 的哪裡會用到?🚃

以上這些內容,其實就是 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

Deviseencrypted_password(= 其實是雜湊!)

Devise 的密碼會用 bcrypt 雜湊後,存進 encrypted_password 欄位。

# config/initializers/devise.rb
config.stretches = Rails.env.test? ? 1 : 12  # ← bcrypt 的成本因子(就是剛才的 "12")

:warning: 欄位名稱雖然叫 encrypted_password,但 內容不是加密,而是雜湊,而且 不能解密。這是 Rails / Devise 常見的混淆點。

cookies.signedcookies.encrypted(= SHA 與 AES)

cookies.signed[:user_id]  = 1      # 用 HMAC-SHA 做竄改檢查(內容可讀)
cookies.encrypted[:token] = "abc"  # 用 AES 加密(內容也會被隱藏)

只想防止被竄改就用 signed;如果連內容也要隱藏,就用 encrypted。這正好就是本文「雜湊 vs 加密」的實際應用 🙆


其實全部都有用到:HTTPS 🔒

到目前為止出現的角色,都會在 HTTPS 通訊裡一起組合使用。理解這點之後,整件事就會突然變得很清楚。

連上 HTTPS 時:
① 伺服器憑證驗證     … RSA/ECDSA(非對稱)+ SHA(簽章)
② 共通金鑰傳遞(金鑰交換)… RSA/ECDHE(非對稱)安全共享 AES 的金鑰
③ 實際資料傳輸       … AES(對稱)快速加密
④ 竄改檢查           … SHA 系(HMAC)確認資料是否損壞
  • 非對稱(RSA)速度慢,所以 只用在金鑰交換與簽章
  • 內容本身用快速的對稱(AES)
  • 用 SHA 來確保完整性

→ 這就是前面說的「混合式加密」。「為什麼會有多種方式」的答案就藏在 HTTPS 裡 👍


最後

  • 加密(可以還原)與雜湊(不能還原)是不同的東西,先把這點分開
  • 加密方面,會以快速但有金鑰配送問題的 對稱式(AES),搭配可解決金鑰配送、但較慢的 非對稱式(RSA),以 混合式 使用
  • 密碼不要用 SHA。因為太快了。請使用 故意較慢的 bcrypt(或 argon2)+ salt
  • Rails 的 master.key=AES、Devise 的 encrypted_password=其實是 bcrypt 雜湊、cookies.signed=SHA,全部都對應到本文的內容

如果拿不定主意,可以用這樣的方式思考:「之後還要讀回來的東西用加密(AES),再也不需要還原的密碼用雜湊(bcrypt)」

密碼學的原則是 不要自己從頭實作,改用成熟的函式庫(OpenSSL / bcrypt)。理解原理,同時把實作交給可靠工具,才是最安全的做法 🙆


原文出處:https://qiita.com/akachiryo/items/5a1deaa541d70e11d85f


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝12   💬3   ❤️5
485
🥈
我愛JS
📝3   💬6   ❤️4
173
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登