HIBP APIを使ってパスワード流出チェック

Have I Been Pwned(HIBP)で定期的にメールアドレスの流出チェックをしているが、2019/1に流出したCollection #1に、メールアドレスが含まれていた。

併せてパスワードも流出しているということで、チェックしようと思ったが、パスワードマネージャーとして使用しているKeePassには100件以上登録しているので面倒。

とりあえずHIBP運営者のTroy Huntさんのブログを見ていたら、1PasswordのWatchtower機能にHIBPのAPIが統合された、という記述を発見。

APIがあるならパスワード全部チェックできるかと思い、試したのが2019/1月末。備忘のためメモ。

HIBP APIの確認

HIBP APIの現在のバージョンは2。匿名性モデルに沿って実装されており、生パスワードは送信されない。

haveibeenpwned.com

パスワードAPIの使い方は、以下のようになる。

  1. UTF-8エンコードされたパスワードをSHA1ハッシュに変換
  2. https://api.pwnedpasswords.com/range/{SHA1ハッシュの先頭5文字} にGETリクエストを送信
  3. {送信した5文字から始まるSHA1ハッシュの残り35文字}:流出数 が返る、アルファベットは大文字

Git Bashと付属のOpenSSL、grepを使い、最弱のパスワードとしておなじみの「123456」で試してみる。

# OpenSSLで生成したハッシュのアルファベットは小文字なので、grepではiオプションで大文字小文字を無視させる
DIGEST=$(echo -n 123456 | openssl sha1 | cut -d ' ' -f 2)
curl -sS https://api.pwnedpasswords.com/range/${DIGEST:0:5} | grep -i ${DIGEST:5}
# D09CA3762AF61E59520943DC26494F8941B:23174662

2,300万回...

KeePassからのパスワードエクスポート

File > Exportからエクスポートできる。

CSVでエクスポートすればいいと思ったが、「KeePass CSV (1.x)」を選択すると、インポート/エクスポート用のためか、すべての項目が出力されてしまう。

Notesに改行など入っているため、パスワードの抽出が面倒。

また、パスワード以外にもカード類の暗証番号や、秘密の質問の答えなどを管理しているため、それらは除外したい。

「Customizable HTML File」を選択すると、パスワードがcodeタグで囲まれたUTF-8のHTMLを出力できる。

Layoutの設定はデフォルトで出力して、Google Chromeで開き、デベロッパーツールで以下のJavaScriptを実行して、必要なパスワードをクリップボードにコピー。

const ta = document.body.appendChild(document.createElement('textarea'));
Array.from(document.querySelectorAll('code'), c => c.textContent)
    .filter(p => p.length > 4 && p.match(/^[\x20-\x7e]+$/))
    .forEach(p => ta.value += p + '\n');
ta.select();
document.execCommand('copy');

適当なファイルにペーストして保存しておく。

生パスワードをクリップボードにコピーしたり、保存したりはまずいが、エクスポートの段階で保存されているので妥協。

ハッシュ化するライブラリを読み込めば、JavaScriptで直接APIを実行するのは難しくないだろう。

エクスポートしたパスワードの流出チェック

ファイルからパスワードを読み込むように、シェルスクリプトを修正。

INPUT_FILE=...
RESULT_FILE=...

# 進捗状況確認用
LINE_NUMBER=1

# 連続でアクセスするのは悪いので、適当に待機
WAIT_SECOND=5

echo 'start...'

while read RAW_PASSWORD; do
  DIGEST=$(echo -n ${RAW_PASSWORD} | openssl sha1 | cut -d ' ' -f 2)
  RESULT=$(curl -sS https://api.pwnedpasswords.com/range/${DIGEST:0:5} | grep -i ${DIGEST:5})

  if [ -n "${RESULT}" ]; then
    echo "${LINE_NUMBER},${RESULT#*:}" >> ${RESULT_FILE}
  fi

  echo "${LINE_NUMBER} checked."
  let LINE_NUMBER++
  sleep ${WAIT_SECOND}s
done < ${INPUT_FILE}

echo 'end.'

試しに、2018年のパスワードワースト10で実行してみる。

123456
password
123456789
12345678
12345
111111
1234567
sunshine
qwerty
iloveyou

さすがの結果に。中でも「123456」は、まさに桁違い。

1,23174662
2,3645804
3,7671364
4,2889079
5,2333232
6,3093220
7,2484157
8,405578
9,3810555
10,1593388

感想

事前認証やHTTPヘッダーの設定なども不要で、簡単に使用できた。

確認の結果、現在使用しているパスワードは流出していなかったので一安心。