パスワード強度推定メーター（zxcvbn.js）を実装してみる

## zxcvbn とは？

オンラインストレージサービスを提供している Dropbox が開発、実際に使用されているパスワード強度推定ツールです。

以下の場所からダウンロードできます。

***dropbox/zxcvbn - GitHub***
<https://github.com/dropbox/zxcvbn>

JavaScriptファイルだけであるのなら、こちらからmapファイルも含めてダウンロードできます。
<https://github.com/dropbox/zxcvbn/tree/master/dist>

本稿で使うJavaScript以外にも、いろいろな言語にフォークされておりますので、必要あらば上記ページのリンク先をご参考にしてください。

実際、zxcvbn が、どのような仕様で、つまりどういった設計思想の元に開発されているかは、以下のブログ記事から読むことができます。2012年時点での記事ですが、他のウェブサービスとも比較して、「強度」「推定」の計算と指針が大きくちがうことが示されています。

<https://blogs.dropbox.com/tech/2012/04/zxcvbn-realistic-password-strength-estimation/>

英語を読むのがシンドイという方は、一応拙訳ですが、Qiitaに日本語訳の記事を投稿しています。

***Dropbox開発のzxcvbn（パスワード強度メーター）のブログ記事を全訳してみた***
<https://qiita.com/hibara/items/37ba7a1f8ba2d54ae600>

## 実際に動いているサンプルページ

`zxcvbn.js` を実装した、`index.html` をウェブページとしてアップロードしています。こちらから動作を確認できます。
<https://hibara.github.io/zxcvbn-js-test/>

なお、これらのサンプルページのソースコードは、GitHub で公開しています。こちらを参考にされながら、本稿を読み進めていくと理解が深まるでしょう。

***hibara/zxcvbn-js-test - GitHub***
<https://github.com/hibara/zxcvbn-js-test>

## 使い方

お使いのウェブページ内のソース部分に以下を追加しまます。

```html:index.html
<script src="js/zxcvbn.js"></script>
```

書き方は、

```coffeescript
zxcvbn(password, user_inputs=[])
```
となります。第一引数に「パスワード文字列」、第二引数には「追加辞書」を指定します。実際のコードにしてみたのが、以下です。

```javascript:index.html
const detect_password = () => {
  let user_id = document.getElementById("user-id").value;

  // zxcvbn判定
  const result = zxcvbn(document.getElementById("password").value, [user_id,]);

  // ...
}
```

関数 `zxcvbn` の第二引数は、チェックする追加辞書の配列です。この例では、ユーザーIDの名前をパスワードに使っていないかをチェックするために辞書へ追加しています。

その他にも、基本的には zxcvbn は、アメリカ製ですので、アメリカのテレビ番組や有名人などが辞書に入っていて、必ずしも日本文化に即したパスワードチェックを行っていません。もし独自の日本語に即した推測されやすいパスワード辞書があれば、ここから追加することができます。

## zxcvbn.js が返す結果データについて

前の章で、関数 `zxcvbn` が返してきた変数、`result` に何が入ってくるかについて、ここでは詳しく説明していきます。

以下、GitHub の [README.md](https://github.com/dropbox/zxcvbn/blob/master/README.md) に書いてあるので、それを以下に拙訳（意訳）します。

変な訳し方があったら、コメントからご指摘いただけると助かります。

``` coffee
result.guesses            # パスワードの解読に必要な推定推測値
result.guesses_log10      # result.guesses の桁数

result.crack_times_seconds # いくつかのシナリオで基づいた推定クラック時間（秒数で）の
                           # 大雑把な見積もり一覧
{
  # パスワード認証の試行をレート制限するサービスに対するオンライン攻撃。
  online_throttling_100_per_hour

  # レート制限しないサービスへのオンライン攻撃、
  # または、攻撃者がレート制限をすり抜けた場合
  online_no_throttling_10_per_second

  # オフライン攻撃。複数の攻撃者を想定し、
  # bcrypt, scrypt, PBKDF2 のような適度な作業効率での、
  # 適切にユーザー毎の salt 設定と、slow hash 関数を使っている。
  offline_slow_hashing_1e4_per_second

  # ユーザー固有に salt 設定を行ったオフライン攻撃だが、
  # SHA-1、SHA-256、または MD5 の fast hash 関数を使っている場合。
  # コア数やマシン数に応じて、1秒間に10億～1兆回の広範囲で合理的な推測が可能。
  # おおよそ、秒間10B。
  offline_fast_hashing_1e10_per_second
}

result.crack_times_display # result.crack_times_seconds と同じ内容、
                           # より分かりやすい文字列表記をしています：
                           # "1秒未満"、"3時間"、"何世紀 "など。

result.score      # 0〜4 の、Integer（強度バーの導入に役立つ）

  0 # かなり推測しやすい： 危険なパスワード。（推測値 < 10の3乗）

  1 # とても推測しやすい： throttleを用いたオンライン攻撃からは守れる。（推測値 < 10の6乗）

  2 # やや推測しやすい： throttleを用いないオンライン攻撃からは守れる。（推測値 < 10の8乗）

  3 # 安全で推測しにくい： オフラインの slow hash シナリオから普通に守れる。（推測値 < 10の10乗）

  4 # とても推測しにくい: オフラインの slow hash シナリオから強力に守れる。（推測値 >= 10の10乗）

result.feedback   # パスワードを選択する助けになるよう言葉によるフィードバック。 score <= 2 のとき。

  result.feedback.warning     # 何が間違っているのかを説明します。たとえば「これはよく使われるパスワードのトップ10です」。
                              # 常に設定されているわけではありません -- 時には空の文字列が入ります。

  result.feedback.suggestions # より推測できないパスワードを選択するの助けとするため、
                              # 提案リスト（文字列が入っていないこともある）。たとえば、「他の単語を追加する」

result.sequence   # zxcvbn が推定計算のベースとしている
                  # パターンのリスト。

result.calc_time  # 答えを計算するのにどれくらいの時間がかかったかを
                  # ミリ秒単位で表示します。
```

### result.score

もっとも重要で、使い途が多いのは、この値でしょう。基本的に `zxcvbn` では、`result.score <= 2` は、すべて「推測しやすい」に分類され、非推奨（場合によっては危険）となります。

もし強度メーターのようなものを設置する場合は、2以下の値は「危険」を示す色にする必要があるでしょう。

### result.crack_times_display

補足すると、`result.crack_times_display` は、前段の `result.crack_times_seconds` の値を分かりやすく「時間」等に置き換えた内容が Object として返ってきます。要するにそのパスワードをクラックするおおよその時間を表示します。

* result.crack_times_display["online_throttling_100_per_hour"]
* result.crack_times_display["online_no_throttling_10_per_second"]
* result.crack_times_display["offline_slow_hashing_1e4_per_second"]
* result.crack_times_display["offline_fast_hashing_1e10_per_second"];

で、それぞれ取得できます。

ソースコード内を見る限りでは、以下のいずれかの組み合わせの表現で返ります。

```javascript
// x = 数値
'less than a second',
x + 'seconds',
x + 'minutes',
x + 'hours',
x + 'day',
x + 'month',
x + 'year',
'centuries',  // 何世紀もかかる
```

### result.feedback.suggestions

ちなみに、`result.score > 2` だと、これらのフィードバックは返りません。

いわゆる、より推測されにくいパスワードにするにはどうしたら良いか？という提案が返ります。ソースコードを見る限り、以下に列挙した文章の提案が返るようです。

大別してこのフィードバックには、`warning` と、`suggestions` で構成されます。ついでに拙訳も付けておきます。

```javascript
suggestions: [
  // いくつかの単語を使用し、一般的なフレーズを避けてください
  "Use a few words, avoid common phrases",
  // 記号、数字、大文字は必要ありません。
  "No need for symbols, digits, or uppercase letters"
];

suggestions: [
  // 別の言葉を追加します。珍しい言葉の方が良いでしょう。
  'Add another word or two. Uncommon words are better.'
];

{

  warning: [
    // 直線的なキー配列は推測しやすい
    'Straight rows of keys are easy to guess',
    // 短いキーボードパターンは推測しやすい
    'Short keyboard patterns are easy to guess'
  ],
  suggestions: [
    // ターン数を増やしてより長いキーボード配列パターンを使ってください
    'Use a longer keyboard pattern with more turns'
  ]
}

{
  warning: [
    // 「aaa」といった繰り返しは推測されやすいです。
    'Repeats like "aaa" are easy to guess',
    // 「abcabcabcabc」のような繰り返しは、「abc」よりもわずかに推測しにくいだけです。
    'Repeats like "abcabcabc" are only slightly harder to guess than "abc"'
  ],
  suggestions: [
    // 単語や文字列の繰り返しは避けましょう
    'Avoid repeated words and characters'
  ]
}

{
  // abcや6543のような連続は簡単に推測できます
  warning: "Sequences like abc or 6543 are easy to guess",
  // 連続は避けましょう
  suggestions: ['Avoid sequences']
}

{
  // 最近の年号は推測されやすいです
  warning: "Recent years are easy to guess",
  suggestions: [
    // 最近の年号は避けましょう
    'Avoid recent years',
    // あなたに関係する年号は避けましょう
    'Avoid years that are associated with you'
  ]
}

{
  // 日付はよく簡単に推測されます
  warning: "Dates are often easy to guess",
  // あなたに関係する日付や年号は避けましょう
  suggestions: 'Avoid dates and years that are associated with you'
}

warning:[
  // これは一般的に使われるパスワードのトップ10です
  'This is a top-10 common password',
  // これは一般的に使われるパスワードのトップ100です
  'This is a top-100 common password',
  // これはとても一般的に使われるパスワードです
  'This is a very common password',
  // これは一般的に使われるパスワードに似ています
  'This is similar to a commonly used password',
  // 単語単体は簡単に推測されます
  'A word by itself is easy to guess',
  // 名前と苗字だけでは、それ自体が推測しやすいです
  'Names and surnames by themselves are easy to guess',
  // 一般的な名前と名字は推測しやすいです
  'Common names and surnames are easy to guess'
];
suggestions: [
  // 大文字はあまり役に立たない
  "Capitalization doesn't help very much",
  // すべての大文字は、すべての小文字と同じくらい簡単に推測することができます
  "All-uppercase is almost as easy to guess as all-lowercase",
  // 逆転した単語は推測するのはさほど難しくない
  "Reversed words aren't much harder to guess",
  //「a」 の代わりに「@」のような予測可能な代用はあまり役に立ちません
  "Predictable substitutions like '@' instead of 'a' don't help very much"
];
```

### result.sequence

また、`result.sequence` には、判定にどういう処理したかという過程内容が、Object配列で入ってきます。一つ以上、返ってくる可能性がありますので、ここを取得して処理する際には注意が必要です。

以上です。