5
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?

ハッシュ戦略ってなんだ?〜bcrypt vs argon2〜

5
Posted at

ハッシュ戦略ってなんだ?〜bcrypt vs argon2〜

ねらい

パスワードハッシュアルゴリズムの選択に迷っているあなたに、bcryptとargon2の違いを実装例付きで解説する。

対象

  • パスワード保存の実装を任された新米エンジニア
  • bcryptを使っているが、そろそろ移行を検討している人
  • 「セキュアなパスワードハッシュ」の本当の意味を知りたい人

ゴール

  • bcryptとargon2の技術的な違いが理解できる
  • それぞれのアルゴリズムの強みと弱点が分かる
  • 実装コードを見て、すぐに使える状態になる

TL;DR

  • bcrypt: 1999年から使われている実績豊富なアルゴリズム。CPU負荷は高いがメモリ負荷は低い(4KB固定)
  • argon2: 2015年のPassword Hashing Competition優勝アルゴリズム。CPU・メモリ両方に負荷をかけられる
  • 推奨: 新規プロジェクトならargon2id、既存システムでbcrypt使ってるなら無理に移行しなくてOK

なぜパスワードを「ハッシュ」する必要があるのか

ある日、上司に呼ばれてこう言われた。

「ユーザー認証機能を作ってくれ。あ、パスワードは絶対にハッシュ化してな」

は?ハッシュ?SHA-256使えばいいんじゃないの?

...と思ったあなたは危険だ。マジで危険。

パスワードハッシュには「遅いこと」が求められる。SHA-256みたいな高速なハッシュ関数は、逆にパスワード保存には向いていない。なぜなら、攻撃者がGPUを使って1秒間に数十億回もハッシュ計算できてしまうからだ。

パスワードハッシュアルゴリズムは、意図的に「計算を遅くする」設計になっている。これによって、攻撃者のブルートフォース攻撃を現実的な時間内では不可能にする。


bcryptとは何者なのか

誕生の経緯

bcryptは1999年、Niels ProvosとDavid Mazièresによって設計された。名前の由来はBlowfish暗号の「b」と、UNIXパスワードシステムの「crypt」を組み合わせたもの。

彼らはUNIXのCRYPT CONFERENCEで「A Future-Adaptable Password Scheme」という論文を発表した。その中でこう述べている:

"bcrypt is based on the Blowfish cipher and is designed to be a computationally-intensive hashing algorithm, based on Bruce Schneier's Blowfish cipher. The work factor of the algorithm is parameterised, so it can be increased as computers get faster."
(bcryptはBlowfish暗号に基づいており、Bruce SchneierのBlowfish暗号をベースにした計算負荷の高いハッシュアルゴリズムとして設計されている。アルゴリズムのワークファクターはパラメータ化されているため、コンピュータが高速化するにつれて増やすことができる)

要するに、「コンピュータが速くなっても、設定を変えれば対抗できる」という思想だ。

bcryptの仕組み

bcryptの特徴は以下の通り:

  1. コストファクター調整可能: roundsパラメータで計算回数を 2^rounds 回に設定できる
  2. ソルト自動生成: 各パスワードに一意のソルトを付与(レインボーテーブル攻撃を防ぐ)
  3. 固定メモリ使用: 4KBのメモリを使用(これが後で問題になる)
  4. 72バイト制限: パスワードは最初の72バイトしか使われない

Pythonでの実装例

import bcrypt

# パスワードのハッシュ化
password = b"correct_horse_battery_staple"
salt = bcrypt.gensalt(rounds=12)  # デフォルトは10
hashed = bcrypt.hashpw(password, salt)

print(f"ハッシュ: {hashed.decode()}")
# 出力例: $2b$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy

# パスワードの検証
if bcrypt.checkpw(password, hashed):
    print("パスワードが一致しました")
else:
    print("パスワードが間違っています")

ハッシュ文字列の構造:

$2b$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
 │  │  │                      │
 │  │  └─ ソルト(22文字)    └─ ハッシュ値(31文字)
 │  └─ コストファクター(12 = 2^12回の反復)
 └─ アルゴリズムバージョン($2a$, $2b$など)

argon2とは何者なのか

Password Hashing Competitionの勝者

2012年から2015年にかけて、世界中の暗号学者が「次世代のパスワードハッシュアルゴリズム」を決めるコンペティションを開催した。それがPassword Hashing Competition(PHC)だ。

24のアルゴリズムが提出され、最終的にargon2が優勝した。設計者はルクセンブルク大学のAlex Biryukov、Daniel Dinu、Dmitry Khovratovich。

2021年9月には、IETF(インターネット技術標準化委員会)によってRFC 9106として正式に標準化された。

RFC 9106より:

"Argon2 is a memory-hard function. It is a streamlined design. It aims at the highest memory-filling rate and effective use of multiple computing units, while still providing defense against trade-off attacks."
(Argon2はメモリハード関数である。それは合理化された設計であり、トレードオフ攻撃に対する防御を提供しながら、最高のメモリ充填率と複数の計算ユニットの効果的な使用を目指している)

argon2の3つのバリアント

argon2には3つのバリアントがある:

  1. argon2d: データ依存のメモリアクセス。GPU攻撃に強いが、サイドチャネル攻撃に弱い
  2. argon2i: データ非依存のメモリアクセス。サイドチャネル攻撃に強いが、GPU攻撃への耐性は少し低い
  3. argon2id: argon2dとargon2iのハイブリッド。RFC 9106で推奨されている

RFC 9106の推奨設定:

"The Argon2id variant with t=1 and 2 GiB memory is the FIRST RECOMMENDED option and is suggested as a default setting for all environments."
(t=1、2GiBメモリのArgon2idバリアントが第一推奨オプションであり、すべての環境のデフォルト設定として推奨される)

argon2の仕組み

argon2は3つのパラメータで細かく調整できる:

  1. time_cost (t): 反復回数(計算時間を制御)
  2. memory_cost (m): 使用メモリ量(KiB単位)
  3. parallelism (p): 並列度(スレッド数)

Pythonでの実装例

from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

# デフォルト設定でハッシャーを作成
# デフォルト: time_cost=3, memory_cost=65536 (64MB), parallelism=4
ph = PasswordHasher()

# パスワードのハッシュ化
password = "correct_horse_battery_staple"
hashed = ph.hash(password)

print(f"ハッシュ: {hashed}")
# 出力例: $argon2id$v=19$m=65536,t=3,p=4$somebase64salt$somebase64hash

# パスワードの検証
try:
    ph.verify(hashed, password)
    print("パスワードが一致しました")
except VerifyMismatchError:
    print("パスワードが間違っています")

# ハッシュの再計算が必要か確認
if ph.check_needs_rehash(hashed):
    print("パラメータが古いので再ハッシュを推奨")

カスタムパラメータでの使用:

from argon2 import PasswordHasher

# カスタム設定
ph = PasswordHasher(
    time_cost=2,           # 反復回数
    memory_cost=102400,    # 100 MB
    parallelism=8,         # 8スレッド
    hash_len=32,           # ハッシュ長
    salt_len=16            # ソルト長
)

password = "my_super_secret_password"
hashed = ph.hash(password)

ハッシュ文字列の構造:

$argon2id$v=19$m=65536,t=3,p=4$c29tZXNhbHQ$somebase64hash
 │        │   │                │          │
 │        │   │                │          └─ ハッシュ値(base64)
 │        │   │                └─ ソルト(base64)
 │        │   └─ パラメータ(m=メモリ, t=時間, p=並列度)
 │        └─ バージョン(19 = v1.3)
 └─ バリアント(id = argon2id)

bcrypt vs argon2:徹底比較

1. セキュリティ強度

bcryptの弱点:

  • メモリ使用量が4KB固定 → GPU/ASIC攻撃に弱い
  • 出力長が184ビット → 現代の基準では短い
  • 72バイト制限 → 長いパスフレーズの強度が活かせない

argon2の強み:

  • メモリハード関数 → GPU/ASIC攻撃を大幅に困難化
  • メモリ使用量を自由に設定可能 → ハードウェアに応じた調整が可能
  • パスワード長の制限がほぼない

2. パフォーマンス

興味深い事実がある。実はargon2は、500ms未満の高速な設定では、bcryptよりも攻撃に弱くなる可能性がある。

Stack Overflowでの議論より:

"Multiple judges for the Password Hashing Competition have since acknowledged that once you tune Argon2 to be as responsive as indicated by UX studies for interactive authentication (<=500ms), it's actually worse than bcrypt from a defense perspective"
(Password Hashing Competitionの複数の審査員が後に認めたところでは、Argon2をインタラクティブ認証のためのUX研究が示す応答性(500ms以下)に調整すると、実際には防御の観点からbcryptよりも悪くなる)

つまり、「速さを求めるとargon2の利点が失われる」ということだ。

推奨パフォーマンス設定:

bcrypt:

  • cost factor: 12〜13
  • 目標時間: 250〜500ms

argon2id:

  • time_cost: 1〜3
  • memory_cost: 64MB〜2GB
  • parallelism: 4
  • 目標時間: 500ms以上

3. 実装とサポート

bcrypt:

  • ほぼすべてのプログラミング言語で利用可能
  • 20年以上の実績
  • ライブラリが成熟している

argon2:

  • 主要言語でサポートされているが、bcryptほど普及していない
  • まだ比較的新しい(2015年〜)
  • RFC 9106で標準化されたため、今後普及が加速する

4. 移行戦略

既存システムでbcryptを使っている場合の移行方法:

from passlib.context import CryptContext

# 両方のアルゴリズムをサポート
pwd_context = CryptContext(
    schemes=["argon2", "bcrypt"],
    deprecated="auto"
)

def authenticate_user(username, password, stored_hash):
    """ユーザー認証"""
    # パスワードを検証
    if pwd_context.verify(password, stored_hash):
        # 古いハッシュか確認
        if pwd_context.needs_update(stored_hash):
            # argon2で再ハッシュ化
            new_hash = pwd_context.hash(password)
            update_user_hash(username, new_hash)
        return True
    return False

def register_user(username, password):
    """新規ユーザー登録"""
    # 新規ユーザーはargon2でハッシュ化
    hashed = pwd_context.hash(password)
    save_user(username, hashed)

この方法なら、既存のbcryptハッシュをそのまま使いながら、新規ユーザーと既存ユーザーのログイン時に徐々にargon2に移行できる。


実践的な選択基準

新規プロジェクトの場合

argon2idを選ぶべきケース:

  • メモリに余裕がある(サーバーに64MB以上割り当てられる)
  • 高セキュリティが求められる(金融、医療、認証サービスなど)
  • 最新の標準に準拠したい

bcryptを選ぶべきケース:

  • メモリ制約が厳しい(組み込みシステム、IoTデバイスなど)
  • 既存のbcryptインフラが整っている
  • 開発チームがbcryptに慣れている

既存プロジェクトの場合

すぐに移行すべき:

  • MD5、SHA-1、SHA-256などの高速ハッシュを使っている場合

徐々に移行を検討:

  • bcryptを使っているが、cost factorが10未満の場合
  • より高いセキュリティレベルが求められるようになった場合

移行不要:

  • bcryptでcost factor 12以上を使っている
  • セキュリティ要件が満たされている
  • メモリ制約が厳しい

よくある質問

Q1: cost factorやmemory_costはどう決めればいい?

A: 実際にベンチマークを取ろう。

bcryptの場合:

import bcrypt
import time

password = b"test_password"

for rounds in range(10, 15):
    start = time.time()
    hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds))
    elapsed = time.time() - start
    print(f"Rounds {rounds}: {elapsed*1000:.1f}ms")

argon2の場合:

from argon2 import PasswordHasher
import time

password = "test_password"

for mem in [32768, 65536, 102400]:  # 32MB, 64MB, 100MB
    ph = PasswordHasher(memory_cost=mem, time_cost=3, parallelism=4)
    start = time.time()
    hashed = ph.hash(password)
    elapsed = time.time() - start
    print(f"Memory {mem//1024}MB: {elapsed*1000:.1f}ms")

目標は250〜500msに収めること。ユーザー体験を損なわず、攻撃者には十分な負荷をかけられる。

Q2: ソルトは自分で生成する必要がある?

A: 絶対にダメ。ライブラリに任せろ。

bcryptもargon2も、内部で暗号学的に安全な乱数を使ってソルトを自動生成してくれる。自分でrandom.randint()とか使ってソルトを作ると、セキュリティホールになる。

Q3: ハッシュを二重にするとより安全?

A: やめろ

# 絶対にやってはいけない
hash1 = bcrypt.hashpw(password, salt1)
hash2 = bcrypt.hashpw(hash1, salt2)  # これは意味がない

これは「hash shucking」という攻撃を容易にするだけだ。素直にcost factorやmemory_costを上げろ。

Q4: argon2のバリアントはどれを選べばいい?

A: argon2id一択

RFC 9106でも推奨されている。argon2dのGPU耐性とargon2iのサイドチャネル耐性の両方を持っている。


まとめ

パスワードハッシュ戦略の選択は、「最新が最強」という単純な話ではない。

新規プロジェクト: argon2idを使え。特にメモリに余裕があるなら、これ以上の選択肢はない。

既存プロジェクト(bcrypt使用中): 焦って移行する必要はない。cost factor 12以上で運用できているなら、それで十分だ。移行するなら、ユーザーのログイン時に徐々に切り替える戦略を取れ。

絶対にやるべきこと: MD5、SHA-1、SHA-256のような高速ハッシュを使っているなら、今すぐbcryptかargon2に移行しろ。

最後に、セキュリティの世界に「永遠に安全」なものはない。定期的にパラメータを見直し、新しい脅威に対応していく姿勢が何より重要だ。


参考文献

bcrypt関連

argon2関連

比較・ベンチマーク

5
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
5
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?