お疲れ様です! ![]()
前回までで「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..." ──✕ 戻せない ❌ 一方通行
ハッシュは「暗号化」ではありません。 復号できないので。ここを混同するのが一番多い誤解です。
なぜハッシュは戻せないのか? 入力は無限なのに、出力は固定長に圧縮されるから。情報の一部が捨てられるので、そもそも「戻す」という操作が定義できないんですね。
暗号化①:対称鍵 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の弱点だった 鍵配送問題 を解決します。
公開鍵 = 「南京錠」、秘密鍵 = 「その鍵」のイメージ:
・南京錠(公開鍵)は誰にでも配ってOK(かけることしかできない)
・開けられるのは秘密鍵を持つ本人だけ
→ 事前に秘密を共有しなくても、安全に暗号化データを送れる 🙆
- 🙆 事前に秘密を共有しなくていい。デジタル署名にも使える
- 😤 でも 遅い。大量データには不向き
で、なぜ両方あるの? → いいとこ取り(ハイブリッド)
- 対称鍵(AES):速いが、鍵を安全に渡せない
- 非対称鍵(RSA):鍵を安全に渡せるが、遅い
なので 組み合わせる のが定石です。
① 非対称鍵(RSA)で「AESの共通鍵」だけを安全に渡す
② 以降の大量データは、速い対称鍵(AES)で暗号化
この「鍵の受け渡しだけRSA、本文はAES」が、後で出てくるHTTPSの正体です 👇
ハッシュ①:SHA(改ざん検知・チェックサム)
一方向の変換。同じ入力からは必ず同じ出力が出るけど、出力から入力には戻せない。
require 'digest'
Digest::SHA256.hexdigest("hello") # => "2cf24dba5fb0a30e..."
Digest::SHA256.hexdigest("hello!") # => "ce06092fb948d9ff..." (1文字違うだけで全く別物)
- 用途:ファイルの改ざん検知、チェックサム、デジタル署名
- 特徴:速い・固定長・わずかな違いで全く別の値になる
→ 「データが途中で変わってないか」を確認するのが仕事です。
ハッシュ②:bcrypt(パスワード保存)と「SHAではダメな理由」💥
ここが 一番の事故ポイント です。
パスワードをSHAで保存しない
「SHAも一方向で戻せないなら、パスワードに使えるのでは?」——SHAが“速すぎる” ので避けたい
攻撃者がパスワードのハッシュを入手したとする
↓ SHAは超高速(1秒に何十億回も計算できる)
↓ よくあるパスワードを片っ端からハッシュ化して照合(総当たり)
→ 短い・単純なパスワードはあっという間に割られる 💥
✅ パスワードには bcrypt(or argon2 / scrypt)
bcryptは、パスワード保存のために わざと遅く 設計されています。
require 'bcrypt'
hashed = BCrypt::Password.create("my_password")
# => "$2a$12$C6UzMDM.H6dfI/f/IKcEeO..."(ソルト込み・意図的に遅い)
BCrypt::Password.new(hashed) == "my_password" # => true
BCrypt::Password.new(hashed) == "wrong" # => false
bcryptが優れている理由は2つ:
-
① わざと遅い(コストファクタ):
$2a$12$の12が計算コスト。上げるほど1回が重くなり、総当たりの効率を意図的に下げる。ハードが速くなったらコストを上げて追従できる - ② ソルト(salt)が入っている:ランダムな値を混ぜてからハッシュ化するので、同じパスワードでも人によって別のハッシュになる → レインボーテーブルが効かない
ソルトなし: password123 → "abc..."(誰でも同じ → 事前計算でバレる)
ソルトあり: password123 + salt_A → "xyz..."
password123 + salt_B → "def..."(同じパスでも別ハッシュ 🙆)
パスワードには 「速いSHA」ではなく「わざと遅い+ソルトの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)。だから1本漏れると全部読める → .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通信の中で全部組み合わさっています。これを知ると一気に腹落ちします。
HTTPSに接続するとき:
① サーバー証明書の検証 … RSA/ECDSA(非対称)+ SHA(署名)
② 共通鍵の受け渡し(鍵交換)… RSA/ECDHE(非対称)で AESの鍵を安全に共有
③ 実際のデータのやり取り … AES(対称)で高速に暗号化
④ 改ざん検知 … SHA系(HMAC)でデータが壊れてないか検証
- 非対称(RSA)は遅いので 鍵交換と署名だけ
- 本文は速い対称(AES)
- SHAで完全性を担保
→ さっきの「ハイブリッド暗号」そのもの。「なぜ複数あるのか」の答えがHTTPSに詰まっています 👍
最後に
- 暗号化(戻せる)とハッシュ(戻せない)は別物。まずここを分ける
- 暗号化は、速いが鍵配送が課題の 対称(AES) と、鍵配送を解決するが遅い 非対称(RSA) を ハイブリッド で併用する
- パスワードにSHAはNG。速すぎるから。わざと遅いbcrypt(argon2)+ソルト を使う
- Railsの
master.key=AES、Deviseのencrypted_password=実はbcryptハッシュ、cookies.signed=SHA、と全部この記事の話
迷ったら、「後で読み返すものは暗号化(AES)、二度と戻さないパスワードはハッシュ(bcrypt)」 で考えると整理できます。
暗号は「自分で実装しない・枯れたライブラリ(OpenSSL/bcrypt)を使う」が鉄則なので、仕組みを理解しつつ実装は任せるのが安全です 🙆