42
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Railsで学ぶ 暗号化とハッシュ 〜master.keyやDeviseは内部で何をしている?〜

42
Posted at

お疲れ様です! :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の弱点だった 鍵配送問題 を解決します。

公開鍵 = 「南京錠」、秘密鍵 = 「その鍵」のイメージ:
  ・南京錠(公開鍵)は誰にでも配って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..."(同じパスでも別ハッシュ 🙆)

:point_up: パスワードには 「速い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")

: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はNG。速すぎるから。わざと遅いbcrypt(argon2)+ソルト を使う
  • Railsの master.key=AES、Deviseのencrypted_password=実はbcryptハッシュ、cookies.signed=SHA、と全部この記事の話

迷ったら、「後で読み返すものは暗号化(AES)、二度と戻さないパスワードはハッシュ(bcrypt)」 で考えると整理できます。

暗号は「自分で実装しない・枯れたライブラリ(OpenSSL/bcrypt)を使う」が鉄則なので、仕組みを理解しつつ実装は任せるのが安全です 🙆

42
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
42
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?