セキュリティ

bcryptとは:コスト係数・72バイト制限とArgon2idへの移行コード【2026年】

bcryptとは:コスト係数・72バイト制限とArgon2idへの移行コード【2026年】

bcryptとは、パスワードを保存するために作られたハッシュ関数です。1回の計算をわざと遅くし、その遅さを「コスト係数」で調整できる点が、SHA-256などの汎用ハッシュとの違いです。この記事では、60文字のハッシュ文字列の読み方、Python・PHP・Spring Securityでの書き方と既定のコスト、日本語のパスワードなら24文字で上限に達する72バイト制限の落とし穴、OWASPが第一候補に挙げるArgon2idとの比較、既存のbcryptハッシュをログインのたびに置き換えていく移行コードまでを、実装者の目線で説明します。

まとめ:既存のbcryptは使い続けてよく、新規はArgon2idから選ぶ判断

bcryptは2026年時点でも現役の方式です。Laravel・Rails・Spring Securityなど主要なフレームワークの標準で、コスト係数を10以上にしていれば、すぐに作り直す必要はありません。

ただし、新しくシステムを作るなら第一候補はArgon2idです。OWASPのPassword Storage Cheat Sheetは、Argon2id、使えなければscrypt、bcryptはレガシーな環境向け、という順位を示しています。

bcryptを使い続ける場合に必ず確かめたいのは72バイト制限です。72バイトを超えた部分は計算に使われず、ライブラリによっては、利用者への通知なしに切り捨てる仕様です。入力の段階で72バイトを超えるパスワードを弾くか、Argon2idへ移るかのどちらかを選びます。移行は、全員にパスワード再設定を求めなくても、ログイン成功のたびに新方式で保存し直す形で進められます。

bcryptとは:Blowfishを土台に1999年に設計されたパスワード専用の関数

汎用ハッシュのSHA-256とbcryptの違い:1回の計算を遅くする設計

bcryptは、Niels ProvosとDavid Mazièresが1999年のUSENIXで発表した論文「A Future-Adaptable Password Scheme」で示した方式です。暗号方式のBlowfishの鍵準備の処理を、パスワードとソルトを使って何度も繰り返します。

SHA-256は、ファイルの改ざん検知などで大量のデータを速く処理するための関数です。速さはパスワードの保存では弱点になります。漏えいしたハッシュに対して攻撃者がGPUで候補を総当たりするとき、1回の計算が速いほど試せる数が増えるからです。bcryptは逆に、1回の計算を数十〜数百ミリ秒かかるように作り、総当たりの費用を引き上げます。ハッシュ化と暗号化の違いや、SHA系の関数の位置付けはハッシュ化とは?暗号化との違いとパスワード保管・改ざん検知の仕組みで整理しています。

60文字のハッシュ文字列の構造:$2b$・コスト・22文字のソルト

bcryptの出力は、方式の印・コスト係数・ソルト・ハッシュ値を1本につないだ60文字の文字列です。ソルトを別の列に保存する必要はありません。

$2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
 │  │  └ 先頭22文字がソルト(128ビット)、残り31文字がハッシュ値
 │  └ コスト係数 12(2の12乗回の繰り返し)
 └ 方式の版(2a・2b・2y など)

版の印は実装の不具合修正の経緯で増えたもので、新しく作るハッシュは$2b$が一般的です。PHPは$2y$、Spring Securityの既定は$2a$を出力しますが、どれも照合の際は互いに読めるように作られています。

Pythonのbcrypt 5.0系でハッシュ化と照合を動かすインストールと検証

pip installからhashpw・checkpwまで動かす最小のコード例

Pythonでは、PyCAが保守するbcryptパッケージを使います。2026年11月時点の最新リリースは5.0系です。

pip install "bcrypt>=5.0"
import bcrypt

password = "correct horse battery staple".encode("utf-8")

# ハッシュ化:gensalt() がソルトとコスト係数(既定12)を作る
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))
print(hashed)          # b'$2b$12$...' の60バイト

# 照合:保存済みハッシュからソルトとコストを読み取って再計算する
print(bcrypt.checkpw(password, hashed))           # True
print(bcrypt.checkpw(b"wrong-password", hashed))  # False

引数は文字列ではなくバイト列です。encode("utf-8")を忘れるとTypeErrorになります。照合には==での比較を使わず、必ずcheckpwを使います。

コスト係数を1つ上げると計算時間が2倍になる性質と手元での計り方

コスト係数は繰り返し回数の指数です。12なら2の12乗回、13なら2の13乗回なので、1つ上げるたびに計算時間がおよそ2倍になります。どの値にするかは、本番と同じ性能のサーバーで実際に計って決めます。

import time
import bcrypt

pw = b"benchmark-password"
for rounds in range(10, 15):
    salt = bcrypt.gensalt(rounds=rounds)
    start = time.perf_counter()
    bcrypt.hashpw(pw, salt)
    print(rounds, f"{(time.perf_counter() - start) * 1000:.0f} ms")

OWASPはコストの最低値を10とし、ハッシュの計算は1秒未満に収めるよう書いています。ログインが集中する時間帯にはCPUを奪い合うため、1回あたり数百ミリ秒に収まる最大の値を選ぶのが現実的です。コストを上げても既存のハッシュは古い値のまま照合できるので、ログイン時に作り直せば徐々に新しい値へ移ります(後述の移行コードで扱います)。

PHPとSpring Securityでbcryptを使う際の既定コストの違い

主要な言語の標準機能も、既定でbcryptを使います。表のPHP標準関数はpassword_hash(PASSWORD_DEFAULT)、Spring Securityの該当する実装はBCryptPasswordEncoderです。既定のコストは言語ごとに違うため、何も指定していないシステムでは実際の値を確認しておきます。

実装 既定のコスト 一次情報の記載
Python bcrypt(gensalt) 12 READMEに「defaults to 12」
PHP標準関数 12(PHP 8.4.0から) 8.4.0で10から12へ引き上げ
Spring Security 10 約1秒で照合できるよう調整を推奨

PHPのpassword_hashのマニュアルは、PASSWORD_DEFAULTが将来別の方式に変わり得るとして、ハッシュの列を60バイトより長く、255バイト程度にしておくよう勧めています。Argon2idへ移ると文字列が長くなるため、列の長さは移行の前に確かめておく項目です。Spring Security 7.1系のPassword Storageの説明は既定の強さを10とし、照合が約1秒になるよう各自で調整するよう書いています。Spring Bootでの認証全体の組み方はSpring Bootの認証実装|DB認証・ログインAPI・remember-me・JWTの書き方にまとめました。

72バイト制限で起きる事故:日本語パスワードは24文字で上限に届く

UTF-8で日本語は1文字3バイト:NISTの64文字推奨とぶつかる計算

bcryptが計算に使うのは、パスワードの先頭72バイトまでです。上限は文字数ではなくバイト数で数えます。UTF-8では英数字が1文字1バイト、ひらがな・カタカナ・漢字の多くは1文字3バイトなので、日本語だけのパスフレーズなら24文字で上限に達します。

一方でNIST SP 800-63B-4は、パスワードの最大長として少なくとも64文字を受け付けるよう推奨し、Unicodeの文字も1文字として数えるよう定めています。日本語64文字は192バイトです。NISTの推奨どおりに長いパスフレーズを許すと、bcryptでは後半の120バイトが計算に入りません。長いパスワードを勧める社内ポリシーの決め方はパスワード管理のベストプラクティス:NIST SP 800-63B-4の要件と企業ポリシーの決め方で扱っています。

OktaとSpring Securityで起きた72バイト超の照合すり抜けの事例

この制限は机上の話ではありません。Oktaは2024年11月1日のセキュリティアドバイザリで、AD/LDAP連携の認証キャッシュの不具合を公表しました。キャッシュのキーを「ユーザーID+ユーザー名+パスワード」をつないだ文字列のbcryptで作っていたため、ユーザー名が52文字以上だと、前半だけで72バイトの枠が埋まり、パスワードを確かめないままキャッシュが一致する余地がありました。対象は2024年7月23日〜10月30日の認証で、同社はキーの生成をPBKDF2へ切り替えています。

Spring Securityでも、CVE-2025-22228(2025年3月19日公開)として、72文字を超えるパスワードで先頭72文字が同じならBCryptPasswordEncoder.matchesがtrueを返す問題が修正されました。OSS版では6.4.4と6.3.8が修正版です。古い版のまま運用しているシステムは、まず版を確かめます。

72バイト超の入力を拒否するか事前ハッシュするか:OWASPの注意点

Pythonのbcryptは5.0.0の変更履歴で、72バイトを超えるパスワードを渡すとValueErrorを出すように変わりました。それまではOpenBSD版にならって黙って切り捨てていました。4系から上げると、長いパスワードの利用者がログインできなくなる場合があるため、上げる前に入力の検証を入れておきます。

MAX_BCRYPT_BYTES = 72

def validate_password_length(raw):
    size = len(raw.encode("utf-8"))
    if size > MAX_BCRYPT_BYTES:
        raise ValueError(
            f"パスワードは英数字72文字・日本語24文字程度までにしてください(現在{size}バイト)"
        )

72バイトを超えても使えるよう、先にSHA-256などで縮めてからbcryptに渡す「事前ハッシュ」という方法もあります。ただしOWASPは、事前ハッシュの出力にnullバイトが含まれると途中で切れる実装がある点と、漏えい済みの単純なハッシュと突き合わせて解かれる「password shucking」の危険を挙げています。自前で組むより、入力を72バイトで止めるか、上限の無いArgon2idに移るほうが安全です。

bcrypt・Argon2id・scrypt・PBKDF2の比較とOWASP推奨値の読み方

OWASPの推奨順位と各方式の最小パラメータを並べた選定用の比較表

OWASPのチートシートが示す4方式の位置付けと、最小の設定値を並べると次のようになります。

方式 OWASPでの位置付け 最小設定の例 入力長の上限
Argon2id 第一候補 m=19MiB・t=2・p=1 実質なし
scrypt Argon2idが使えない場合 N=2^17・r=8・p=1 実質なし
bcrypt レガシーな環境向け コスト10以上 72バイト
PBKDF2 FIPS-140準拠が必要な場合 HMAC-SHA256で60万回 実質なし

Argon2idとscryptは、計算時間に加えて大量のメモリを使わせる設計です。GPUや専用回路は計算は速くてもメモリを並列に多く積むのが高くつくため、総当たりの費用がさらに上がります。bcryptが使うメモリは4KB程度と小さく、この点でArgon2idに劣ります。PBKDF2は米国政府の暗号モジュール基準FIPS-140に沿う必要がある場合の選択肢で、それ以外の新規案件で選ぶ理由は乏しいです。

Argon2idのメモリ19MiB設定とRFC 9106の64MiB設定の選び方

Argon2idの推奨値は文書によって違います。OWASPは、メモリ19MiB・繰り返し2回・並列度1など、同等の強さになる5通りの組み合わせを並べています。一方、Argon2の仕様であるRFC 9106(2021年9月)の第2推奨は、メモリ64MiB・繰り返し3回・並列度4です。PythonのArgon2実装argon2-cffi(25.1系)のPasswordHasherは、このRFCの第2推奨を既定値にしています。

選び方の基準はサーバーのメモリです。64MiBの設定で同時に100件のログインを処理すると、それだけで約6.4GiBを使います。コンテナ1台あたりのメモリが小さい構成や、ログインが集中するサービスでは、OWASPの19MiBの組み合わせから始め、負荷試験で余裕があれば上げる順番が無難です。

既存のbcryptハッシュからArgon2idへログイン時に段階移行する実装

ログイン成功時に再ハッシュする移行コードとcheck_needs_rehash

ハッシュは元のパスワードに戻せないため、保存済みのbcryptをそのままArgon2idに変換することはできません。平文のパスワードが手元に来るのはログインの瞬間だけなので、その時に照合して、成功したら新方式で保存し直します。OWASPも、利用者が次に認証した時点での作り直しを一般的な方法としています。

import bcrypt
from argon2 import PasswordHasher
from argon2.exceptions import InvalidHashError, VerificationError

ph = PasswordHasher()  # 既定:Argon2id・t=3・m=64MiB・p=4

def verify_and_upgrade(user, raw_password):
    stored = user.password_hash
    pw_bytes = raw_password.encode("utf-8")

    # 1. 旧方式(bcrypt)のハッシュ:照合に成功したらArgon2idで保存し直す
    if stored.startswith(("$2a$", "$2b$", "$2y$")):
        if len(pw_bytes) > 72:          # bcrypt 5.0系は72バイト超でValueError
            return False
        if not bcrypt.checkpw(pw_bytes, stored.encode("ascii")):
            return False
        user.password_hash = ph.hash(raw_password)
        user.save()
        return True

    # 2. 新方式(Argon2id)のハッシュ
    try:
        ph.verify(stored, raw_password)
    except (VerificationError, InvalidHashError):
        return False

    # 3. パラメータを上げた後なら、古い設定のハッシュを作り直す
    if ph.check_needs_rehash(stored):
        user.password_hash = ph.hash(raw_password)
        user.save()
    return True

保存先の列は、Argon2idの文字列が入る長さ(100文字前後)に広げておきます。移行の進み具合を確認する指標は、列の先頭が$2で始まる行の残数です。数か月たってもログインしない利用者のbcryptは残り続けるため、一定期間後に再設定を求めるかどうかを決めておきます。

PHPなら同じ流れを標準関数だけで書けます。

if (password_verify($raw, $row['hash'])) {
    if (password_needs_rehash($row['hash'], PASSWORD_ARGON2ID)) {
        $newHash = password_hash($raw, PASSWORD_ARGON2ID);
        // UPDATE users SET hash = :hash WHERE id = :id
    }
}

MD5やSHA-1の旧ハッシュを包み直す方法と全員リセットを選ぶ条件

保存済みのハッシュがMD5やソルト無しのSHA-1など、すでに破られやすい方式の場合は、ログインを待つ間も危険が続きます。OWASPはこの場合の方法を2つ挙げています。1つは全員のパスワードを失効させて再設定させる方法、もう1つは既存のハッシュ値をそのまま新方式の入力にして包み直す方法です。

包み直しでは、argon2id(md5(パスワード))の形で全件をすぐ作り直せます。漏えいしたときに弱い方式のまま残っている行が無くなる点が利点です。照合は「入力をMD5にしてからArgon2idで確かめる」処理になり、素のArgon2idへ置き換えるタイミングは、次のログインが成功した時点です。古いハッシュがすでに外部へ漏れた疑いがあるなら、包み直しでは守れないため、全員リセットを選びます。漏えい済みのパスワードを照合して止める仕組みはパスワードリスト攻撃とは?検知の指標と漏えい照合・レート制限の実装で解説しています。

DelegatingPasswordEncoderで新旧方式を混在させる保存形式

Spring Securityは、方式の混在を前提にしたDelegatingPasswordEncoderを用意しています。ハッシュの先頭に{bcrypt}や{argon2@SpringSecurity_v5_8}のような方式名を付けて保存し、照合時にその名前で処理を振り分けます。

@Bean
PasswordEncoder passwordEncoder() {
    // 既定のエンコードはbcrypt。{id}を見て旧方式のハッシュも照合できる
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

// 保存される形式の例
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

Argon2idを新しい既定にしたい場合は、方式名とPasswordEncoderの対応表を自分で組み、エンコードに使う名前(idForEncode)をArgon2側に向けます。先頭に方式名が付いていない古いハッシュは照合できないため、移行前のデータには一括で{bcrypt}を付けておきます。

bcryptを使い続けてよい条件とArgon2idへ切り替えるべき場面

bcryptを残してよい条件:コスト10以上・72バイト検査・修正済みの版

次の3つを満たしていれば、bcryptを残したままで問題ありません。移行の工数は、ほかの弱点に回したほうが効果が大きいからです。

  1. コスト係数が10以上で、サーバー性能に合わせて見直している
  2. 72バイトを超えるパスワードを入力の段階で弾いているか、上限を画面に明示している
  3. Spring Securityなら6.4.4・6.3.8以降、Pythonのbcryptなら5.0系など、72バイト問題に対応した版を使っている

3つのうち1つでも欠けている場合は、まず欠けている項目を直します。2と3は数時間の改修で済むことがほとんどです。

Argon2idへ移すべき場面とGyazo事例に見るハッシュ方式の説明責任

新しくシステムを作る場合、長い日本語のパスフレーズを許したい場合、ハッシュを含むデータベースが外部に置かれていて漏えい時の被害想定を厳しく見積もる必要がある場合は、Argon2idを選びます。既存のbcryptは前の章の段階移行で置き換えます。

漏えいが起きたときには、方式とコストを示せるかどうかが利用者への説明を左右します。2026年9月に公表されたGyazoの不正アクセスでは、運営会社Helpfeelの発表によると、約2,362万件の利用者情報にパスワードのハッシュ値が含まれていましたが、方式は記載されていませんでした。経緯はGyazoの不正アクセスと2,362万件の流出で詳しく解説しました。Argon2idやコストを十分に上げたbcryptなら、流出しても解読に時間がかかると説明できます。ハッシュ方式の移行や認証部分の切り出しを含めた設計の見直しは、認証基盤・ID管理システム開発で相談を受け付けています。

よくある質問

bcryptについて、検索でよく調べられている疑問に答えます。

bcryptでハッシュ化したパスワードは復号できますか?

できません。bcryptはハッシュ関数で、暗号化と違って元に戻す鍵がありません。攻撃者にできるのは、候補のパスワードを1つずつbcryptにかけて一致を探す総当たりだけです。コスト係数で1回の計算を遅くしてあるため、総当たりの費用が上がります。ただし「password」のような推測しやすいパスワードは、遅くしても早い段階で見つかります。

bcryptのコスト係数はいくつに設定すればよいですか?

OWASPの最低値は10です。PythonのbcryptとPHP 8.4以降の既定は12、Spring Securityの既定は10です。最終的には本番と同じ性能のサーバーで計算時間を計り、ログインの応答として許せる範囲(数百ミリ秒〜1秒未満)に収まる最大の値を選びます。値は1つ上げるごとに計算時間がほぼ2倍になります。

bcryptとArgon2はどちらを使うべきですか?

新規のシステムならArgon2id、既存のシステムでbcryptがコスト10以上で動いているならそのままで構いません。Argon2idはメモリを大量に使わせる設計のため、GPUでの総当たりに強く、72バイトの上限もありません。ライブラリやフレームワークがArgon2idに対応していない環境では、bcryptかscryptを選びます。

bcryptでソルトを別に保存する必要はありますか?

必要ありません。bcryptのハッシュ文字列には、方式の版・コスト係数・22文字のソルトが含まれています。照合の関数が再計算に使うソルトとコストの読み取り元は、このハッシュ文字列です。ソルトの列を別に作ったり、自分でソルトを生成して連結したりすると、かえって実装の誤りの原因になります。

bcryptの72バイト制限は文字数で何文字までですか?

UTF-8の場合、英数字と記号なら72文字、ひらがな・カタカナ・漢字の多くは1文字3バイトなので24文字が目安です。絵文字は1文字4バイトのものが多く、さらに少なくなります。長いパスフレーズを許すサービスでは、入力時にバイト数で検査するか、上限の無いArgon2idを使います。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.11.03 テックブログ ノジマ不正アクセスで約700万件漏えい:対象確認・窓口と乗っ取り注文を止める実装
  2. 2026.10.29 テックブログ ソニー生命の情報漏洩38.5万件|ログインできない理由、再開状況と窓口の電話番号
  3. 2026.11.03 テックブログ クラブツーリズム不正アクセス:約432万件の対象確認と偽メールの見分け方・流出検知の設計
  4. 2026.11.02 テックブログ アララの不正アクセス:メール約7,700万件の持ち出しと契約企業が行うAPIキー再設定・代替送信・連名報告
  5. 2026.11.02 テックブログ サントリーの不正アクセスで約10万人分漏えい|M365のファイル大量ダウンロードを監査ログで調べる手順

RELATED POSTS 関連記事

目次