🔐 SHA256とBCryptの違いとは？Rubyで学ぶハッシュとセキュリティの基本

## はじめに

Railsで認証機能を実装しているとき、パスワードのハッシュについて調べていました。

Railsの教材だと、ログイン機能の実装には人気の認証ライブラリ`devise` を使わない場合に `bcrypt` ライブラリを使っていることが多いですが、一方で `OpenSSL::Digest::SHA256` を使って自前でハッシュ化する方法も存在します。

SHA256 と bcrypt、「同じ ”ハッシュ化” なのに、何が違うの？」 と思いこの記事を書くことにしました。

「sha256 bcrypt 違い」などのワードでGoogle検索してみましたが、2つの違いについてセキュリティの観点からしっかり解説された日本語の記事は意外と少ない印象です。
本記事では、その違いをコードと実例でわかりやすく解説していきます。


## 目次

1. [そもそも「ハッシュ化」とは？暗号化と何が違うの？](#1-そもそもハッシュ化とは暗号化と何が違うの)
2. [SHA256とBCrypt、どう違うの？](#2-sha256とbcryptどう違うの)
3. [BCryptは「わざと遅い」](#3-bcryptはわざと遅い)
4. [SHA256は「速すぎる」〜レインボーテーブル攻撃とは](#4-sha256は速すぎるレインボーテーブル攻撃とは)
5. [ソルトって何？なぜ安全なの？](#5-ソルトって何なぜ安全なの)
6. [「何万回も試せないでしょ」は甘い？現実の攻撃手法](#6-何万回も試せないでしょは甘い現実の攻撃手法)
7. [SHA256 vs BCrypt 比較まとめ](#7sha256-vs-bcrypt-比較まとめ)
8. [Railsで使うならこうする](#8railsで使うならこうする)
9. [結論:認証処理ではbcryptを使おう](#9-結論ログイン処理ではbcryptを使おう)


## 1. そもそも「ハッシュ化」とは？暗号化と何が違うの？
ハッシュとは **入力値を固定長の値に変換する一方向性の関数** です。一方向性というのは、「元の値を復元できない」という意味です。

パスワードのような機密情報を保存する際、平文（そのままの文字列）で保存するのは危険です。そこで、ハッシュ化して保存しておきます。

### ハッシュ ≠ 暗号化
「ハッシュ」と「暗号化」は **まったくの別物** です。

暗号化とは、「復号（元に戻すこと）」ができる仕組みです。たとえば、共通鍵や公開鍵などを使って、**暗号化されたデータを正規の手順で元に戻せる** ようになっています。

しかしハッシュ化は、元に戻すことが前提ではありません。一方向の処理で、同じ入力から常に同じ出力が得られるものの、そこから元の入力を導き出すことは不可能です。復号するのではなく、 **同じ関数を使って再計算し、値が一致するかどうかをチェック** します。これにより、パスワードの照合ができる仕組みになっています。


## 2. SHA256とBCrypt、どう違うの？

まずは、「password」という文字列をハッシュ化した際の挙動について、Rubyのコードで見比べてみましょう。

### SHA256でハッシュ化

```ruby
require 'openssl'

hash = OpenSSL::Digest::SHA256.hexdigest("password")
puts hash
# => "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
```

「password」という文字列に対して、ハッシュ値が一意に決まります。

### BCryptでハッシュ化

```ruby
require 'bcrypt'

hash = BCrypt::Password.create("password")
puts hash
# => $2a$12$...（毎回ちがう文字列が出る！）
```

「password」という文字列に対して、ハッシュ値が実行するたびに変わります。


### 🔐 SHA256の特徴
SHA256では、`OpenSSL::Digest::SHA256.hexdigest("password")` のように書くと、毎回同じ入力から同じハッシュ値が得られます。これはシンプルですが、同じパスワードは同じハッシュになるため、攻撃者にとって推測しやすくなります。

### 🔐 BCryptの特徴
一方、BCryptでは `BCrypt::Password.create("password")` とすることで、毎回異なるハッシュが生成されます。これは内部でランダムなソルトという文字列([ソルトって何？なぜ安全なの？](#5-ソルトって何なぜ安全なの)で説明)が自動的に加えられているためです。同じパスワードでも異なるハッシュになることで、推測や比較が困難になります。


## 3. BCryptは「わざと遅い」

BCryptは意図的に処理を遅くして、攻撃者が大量に試せないように工夫されたアルゴリズムです。

```ruby
BCrypt::Password.create("password", cost: 12)
```

この `cost` が処理の重さ（複雑さ）を決めるパラメータで、値が1増えると処理時間は指数関数的に遅くなります。

| cost | 処理時間の目安 |
|------|----------------|
| 10   | 約100ms        |
| 12   | 約300ms        |
| 14   | 約1秒          |

つまり、**BCryptは「時間をかけさせること」で安全性を高めている**のです。

### ただし、costを上げすぎるとユーザー体験に影響が出ることも...
セキュリティ強化のために cost を高く設定しすぎると、ユーザーにもデメリットが生じる可能性があります。

- ⚠️ **ログインや新規登録の処理が重くなる**
cost=12 で約0.3秒、cost=14 では約1秒、cost=16 だと約4秒かかる。ユーザーが「遅い」と感じる閾値に近づいてきます。

- ⚠️ **大規模サービスではサーバー負荷が増加**
同時ログイン数が多いサービスでは、照合処理がボトルネックとなってレスポンス遅延を招くリスクもあります。

- ⚠️ **UXの悪化につながる恐れも**
少しの待ち時間でも、ユーザーは「使いづらい」と感じてしまうことがあります。

:::note info
**参考：deviseでは cost=12 がデフォルト**
Railsの `devise` では、BCryptの `cost` はデフォルトで `12` に設定されています。これは「安全性と処理速度のバランス」として広く採用されている数値です。
:::

### costの設定は「バランス」が重要
BCryptのcostは高ければ高いほど安全、というわけではありません。
セキュリティとパフォーマンスのバランスを見極めて、自分のサービスに合った値を設定することが大切です。


## 4. SHA256は「速すぎる」〜レインボーテーブル攻撃とは

SHA256は非常に高速なハッシュ関数です。その性能の高さが逆に仇になります。

### レインボーテーブル攻撃とは？

あらかじめよく使われるパスワードとそのSHA256ハッシュを全部計算しておいて、照合に使う手法です。

```ruby
OpenSSL::Digest::SHA256.hexdigest("password")
# => "5e884898da28047151d0e56f8dc62927..."
```

この出力は誰がどこでやっても同じです。つまり、攻撃者は「このハッシュは "password" だ」と覚えておけばOKなのです。

これは**ソルトがないから起こる問題です。**


## 5. ソルトって何？なぜ安全なの？

ソルトとは、**パスワードに足すランダムな値**のことです。

```ruby
BCrypt::Password.create("password")
# => "$2a$12$abc123...xxxx"（毎回違う）
```

ソルトがあることで、「同じパスワード」でも毎回違うハッシュ値になります。  
攻撃者は、毎回ソルトを含めた計算をやり直す必要があるため、大量の照合が非効率になります。

```ruby
# ソルトがなければ
OpenSSL::Digest::SHA256.hexdigest("password")
# => "5e884898..."（誰でも同じ）

# ソルトがあれば
BCrypt::Password.create("password")
# => "$2a$12$..."（毎回異なる）
```

**ソルトはバレてもOK**
攻撃者がソルトを知っても、そのソルトに対する攻撃をまた一から始めなければいけません。
つまり「手間を増やす」ことで現実的な攻撃を防ぐのがソルトの目的です。


## 6. 「何万回も試せないでしょ」は甘い？現実の攻撃手法

「ログインに失敗したらアカウントがロックされるから、何万回も試せないでしょ？」という疑問が出てくると思います。

でも、攻撃者はログイン画面で試すのではありません。以下のような方法があります。

### ✅ 方法1：データベースが流出したときにオフラインで攻撃

一番可能性が高く危険なケースです。
攻撃者が `users` テーブルなどからハッシュ済みパスワード（`password_digest`）を入手した場合、自分のPCで好きなだけ試せます。

```ruby
# 流出したハッシュ値がこれだったとする
stored_hash = "5e884898da28047151d0e56f8dc62927..."

# 攻撃者が辞書攻撃で試す
guesses = ["123456", "password", "qwerty"]
guesses.each do |guess|
  if OpenSSL::Digest::SHA256.hexdigest(guess) == stored_hash
    puts "ヒットしました! パスワードは #{guess}"
    # => ヒットしました! パスワードは password
  end
end
```

このように、**ログイン試行回数の制限は意味を持ちません。**

### ✅ 方法2：Botネットなどを使った分散攻撃

IPアドレス制限やユーザーごとのロック機能があっても、世界中の感染PCを使って分散的に攻撃すれば回避できます。  
特にSHA256のように軽い処理だと、こうした攻撃に耐えられない可能性があります。


## 7：SHA256 vs BCrypt 比較まとめ

| 項目             | SHA256                         | BCrypt                         |
|------------------|----------------------------------|----------------------------------|
| 処理速度         | 非常に高速                     | わざと遅くしている             |
| ソルト対応       | していない（自前実装が必要）     | 自動で対応                      |
| ハッシュ値の情報 | ハッシュ値のみ                  | costやソルトも埋め込まれる        |
| パスワード保存   | 推奨されない                   | デファクトスタンダード           |

→ BCryptは **「遅くて安全」**、SHA256は **「速くて危険」** なのです


## 8：Railsで使うならこうする

Railsでは、`has_secure_password` を使うことでBCryptによるセキュアな実装がとても簡単になります。

```ruby
# マイグレーション
add_column :users, :password_digest, :string

# User model
has_secure_password

# 登録
User.create(name: "taro", password: "secret", password_confirmation: "secret")

# 認証
user = User.find_by(name: "taro")
user.authenticate("secret") # => userオブジェクトが返る
```

この仕組みの裏では、BCryptが使われており、ソルトやcostの処理もすべてRailsがよしなにやってくれます。


## 9. 結論：ログイン処理ではBCryptを使おう

- SHA256は高速で便利ですが、**パスワードハッシュに使うべきではありません**。
- BCryptは「処理が遅い」「ソルト付き」「costで調整可能」なので、**総当たり攻撃に強い**のです。

セキュリティは “見えない部分” こそ大事です。BCryptを使って、安全な実装を心がけましょう。

## おわりに

この記事では、SHA256とBCryptの違いをコードと攻撃手法の視点から解説しました。

ハッシュと暗号化の違い、ソルトの役割、レインボーテーブル攻撃のリスク、そしてBCryptの強さ。
どれも普段は意識しないかもしれませんが、認証で非常に重要な基礎知識です。

本記事が、「なぜSHA256ではなくBCryptを使うのか」という疑問の解消に役立てば幸いです。