zxcvbn.jsを使ってパスワード強度チェッカーを実装する

はじめに

パスワードの強度をチェックする機能は、ユーザー登録フォームやパスワード変更画面でよくみる。
従来の「8文字以上、大文字小文字数字記号を含む」といった単純なルールベースのチェックでは、Password123!のような推測されやすいパスワードも「強い」と判定されてしまう問題がある。

そこで、Dropboxが開発したzxcvbn.jsを使用することで、より実用的なパスワード強度評価を実装できる。
zxcvbnは辞書攻撃、パターンマッチング、文字列の繰り返しなど、実際の攻撃手法を考慮した強度判定を行う。

今回は、zxcvbn.jsを使ったパスワード強度チェッカーを実装してみる。

今回の成果物

環境

zxcvbn.js 4.4.2 (CDN経由)
HTML5
CSS3
JavaScript (ES6)

準備

zxcvbn.jsの読み込み

CDN経由で簡単に利用できる。

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/zxcvbn.js"></script>

npmでインストールする場合

npm install zxcvbn

zxcvbn.jsの仕様

基本的な使い方

const result = zxcvbn('password123');
console.log(result);

戻り値の構造

zxcvbn関数は以下のような詳細な情報を含むオブジェクトを返す。

score(スコア)

0から4の5段階でパスワードの強度を評価する。

スコア評価説明
0非常に弱いすぐにクラックされる
1弱い短時間でクラックされる
2やや弱いオフライン攻撃で比較的容易にクラック可能
3普通一定の耐性がある
4強いクラックが非常に困難

feedback(フィードバック)

{
  warning: "これは一般的なパスワードです",
  suggestions: [
    "もう少し長くしてください",
    "予測しにくい単語を追加してください"
  ]
}
  • warning: 現在のパスワードの主な問題点
  • suggestions: 改善のための具体的な提案

crack_times_seconds(クラック時間)

様々な攻撃シナリオでのクラックにかかる時間を秒単位で返す。

{
  online_throttling_100_per_hour: 360,      // オンライン攻撃(スロットル有り)
  online_no_throttling_10_per_second: 0.4,  // オンライン攻撃(スロットル無し)
  offline_slow_hashing_1e4_per_second: 0.0001, // オフライン攻撃(遅いハッシュ)
  offline_fast_hashing_1e10_per_second: 1e-10  // オフライン攻撃(高速ハッシュ)
}

crack_times_display(人が読める形式のクラック時間)

{
  online_throttling_100_per_hour: "6分",
  online_no_throttling_10_per_second: "1秒未満",
  offline_slow_hashing_1e4_per_second: "1秒未満",
  offline_fast_hashing_1e10_per_second: "瞬時"
}

sequence(検出されたパターン)

パスワード内で検出された脆弱なパターンの配列。

[
  {
    pattern: "dictionary",  // 辞書攻撃で見つかる単語
    token: "password",      // 実際の文字列
    matched_word: "password"
  },
  {
    pattern: "sequence",    // 連続した文字
    token: "123",
    sequence_name: "digits"
  }
]

ユーザー辞書の指定

ユーザー名やメールアドレスなど、個人情報を含むパスワードを検出できる。

const result = zxcvbn('john1234', ['john', '[email protected]']);
// ユーザー名を含むパスワードとして低評価される

実装内容

HTML構造

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>パスワード強度チェッカー</title>
</head>
<body>
    <div class="container">
        <h1>パスワード強度チェッカー</h1>
        <input type="password" id="password" placeholder="パスワードを入力してください">
        <div class="strength-bar">
            <div id="strengthBar" class="strength-bar-fill"></div>
        </div>
        <p id="strengthText" class="strength-text"></p>
        <p id="feedback" class="feedback"></p>
        <label class="show-password">
            <input type="checkbox" id="showPassword">
            パスワードを表示
        </label>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/zxcvbn.js"></script>
    <script src="script.js"></script>
</body>
</html>
style.css
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    background: #f5f5f5;
}

.container {
    background: white;
    padding: 2rem;
    border-radius: 8px;
    box-shadow: 0 2px 8px rgba(0,0,0,0.1);
    width: 90%;
    max-width: 400px;
}

h1 {
    font-size: 1.5rem;
    margin-bottom: 1.5rem;
    color: #333;
}

input[type="password"],
input[type="text"] {
    width: 100%;
    padding: 0.75rem;
    border: 2px solid #ddd;
    border-radius: 4px;
    font-size: 1rem;
    transition: border-color 0.3s;
}

input[type="password"]:focus,
input[type="text"]:focus {
    outline: none;
    border-color: #4CAF50;
}

.strength-bar {
    width: 100%;
    height: 8px;
    background: #e0e0e0;
    border-radius: 4px;
    margin: 1rem 0 0.5rem;
    overflow: hidden;
}

.strength-bar-fill {
    height: 100%;
    transition: width 0.3s, background-color 0.3s;
    width: 0;
}

/* スコア別の色設定 */
.score-0 { width: 20%; background: #f44336; }
.score-1 { width: 40%; background: #ff9800; }
.score-2 { width: 60%; background: #ffeb3b; }
.score-3 { width: 80%; background: #8bc34a; }
.score-4 { width: 100%; background: #4CAF50; }

.strength-text {
    font-size: 0.9rem;
    font-weight: bold;
    margin-bottom: 0.5rem;
}

.feedback {
    font-size: 0.85rem;
    color: #666;
    min-height: 1.2rem;
    line-height: 1.4;
}

.show-password {
    display: flex;
    align-items: center;
    margin-top: 1rem;
    font-size: 0.9rem;
    cursor: pointer;
    user-select: none;
}

.show-password input {
    margin-right: 0.5rem;
    cursor: pointer;
}
script.js
const passwordInput = document.getElementById('password');
const strengthBar = document.getElementById('strengthBar');
const strengthText = document.getElementById('strengthText');
const feedback = document.getElementById('feedback');
const showPasswordCheckbox = document.getElementById('showPassword');

// スコアに対応するラベル
const strengthLabels = ['', '弱い', 'やや弱い', '普通', '強い'];

// パスワード入力時の処理
passwordInput.addEventListener('input', function() {
    const password = this.value;
    
    // 空の場合は初期化
    if (!password) {
        strengthBar.className = 'strength-bar-fill';
        strengthText.textContent = '';
        feedback.textContent = '';
        return;
    }
    
    // zxcvbnでパスワードを評価
    const result = zxcvbn(password);
    const score = result.score;
    
    // スコアに応じてバーを更新
    strengthBar.className = `strength-bar-fill score-${score}`;
    strengthText.textContent = `強度: ${strengthLabels[score]}`;
    
    // フィードバックを表示
    let feedbackText = '';
    
    if (result.feedback.warning) {
        feedbackText = result.feedback.warning;
    }
    
    if (result.feedback.suggestions.length > 0) {
        feedbackText += (feedbackText ? ' / ' : '') + 
                        result.feedback.suggestions.join(', ');
    }
    
    // クラック時間を追加
    const crackTime = result.crack_times_display.offline_slow_hashing_1e4_per_second;
    if (crackTime) {
        feedbackText += feedbackText ? ` (クラックまで: ${crackTime})` : 
                       `クラックまで: ${crackTime}`;
    }
    
    feedback.textContent = feedbackText;
});

// パスワード表示/非表示の切り替え
showPasswordCheckbox.addEventListener('change', function() {
    passwordInput.type = this.checked ? 'text' : 'password';
});

実装に関して

1. リアルタイムフィードバック

inputイベントを使用し、ユーザーが入力するたびにリアルタイムで強度を評価する。

passwordInput.addEventListener('input', function() {
    const result = zxcvbn(this.value);
    // 即座に結果を反映
});

2. フィードバック

スコアに応じて色分けされたプログレスバーを表示し、強度を把握できるようにする。

.score-0 { background: #f44336; }  /* 赤 */
.score-4 { background: #4CAF50; }  /* 緑 */

3. 改善の提案

zxcvbnが提供するfeedback.suggestionsを表示することで、ユーザーがどのようにパスワードを改善すればよいか理解できる。

if (result.feedback.suggestions.length > 0) {
    feedbackText = result.feedback.suggestions.join(', ');
}

4. クラック時間の表示

offline_slow_hashing_1e4_per_secondを使用して、現実的な攻撃シナリオでのクラック時間を表示する。

const crackTime = result.crack_times_display.offline_slow_hashing_1e4_per_second;

パスワード強度の例

実際にさまざまなパスワードを試した結果:

パスワードスコア評価フィードバック
password0非常に弱いこれは一般的なパスワードです
Password1231弱い予測可能なパターンです
MyP@ssw0rd!2やや弱い一般的な置き換えは避けてください
correct horse battery staple4強いクラックまで: 数世紀
Tr0ub4dor&32やや弱い短すぎます
J8#mK9$pL2@nQ54強いクラックまで: 数世紀

zxcvbn.jsについて

メリット

  • 辞書攻撃、パターンマッチング、繰り返しなど実際の攻撃を考慮
  • 「弱い」だけでなく、改善方法を提示
  • 約800KBと比較的小さいファイルサイズ
  • サーバーへの通信不要
  • 英語以外の辞書も含まれている

デメリット

  • CDN経由でも約800KBの読み込みが必要
  • 辞書ファイルの読み込み完了まで時間がかかる場合がある
  • 日本語の辞書攻撃には完全対応していない

参考

おわりに

よくあるパスワード強度チェッカーについて実装してみたく、ライブラリを探してみたところzxcvbn.jsがあったので使用してみた。
単純なルールベースではなく、実際の攻撃手法を考慮した実用的なパスワード強度チェッカーとなっているので良さげ。
ライブラリが返却してくれるレスポンスも手厚いものがあるので、工夫次第で色々できそうなのもよい感じだった。

ユーザー登録フォームやパスワード変更画面に実装することで、アカウントのセキュリティ向上に貢献できると思った。

Hugo で構築されています。
テーマ StackJimmy によって設計されています。