サンプル・プログラムの実行例
目次
サンプル・プログラム
passwordStrength2.html | サンプル・プログラム(UI:JavaScript側) |
passwordStrength2API.php | サンプル・プログラム(WebAPI:PHP側) |
passwords.dic | ブラックリスト辞書(サンプル) |
pahooInputData.php | データ入力に関わる関数群。 使い方は「PHPでGET/POSTでフォームから値を受け取る」「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。 |
バージョン | 更新日 | 内容 |
---|---|---|
2.0.1 | 2024/05/04 | DECODE_TIME の値を改訂 |
2.0.1 | 2023/03/21 | bug-fix |
2.0.0 | 2023/02/23 | UIをJavaScriptに分離 |
1.0.0 | 2020/02/01 | passwordStrength.phpの初版 |
バージョン | 更新日 | 内容 |
---|---|---|
2.0.0 | 2023/02/23 | ブラックリスト辞書・Wikipedia参照廃止,WebAPIに変更 |
1.0.0 | 2020/02/01 | passwordStrength.phpの初版 |
バージョン | 更新日 | 内容 |
---|---|---|
1.5.0 | 2024/01/28 | exitIfExceedVersion() 追加 |
1.4.2 | 2024/01/28 | exitIfLessVersion() メッセージ修正 |
1.4.1 | 2023/09/30 | コメントの訂正 |
1.4.0 | 2023/09/09 | $_GET, $_POST参照をfilter_input()関数に置換 |
1.3.0 | 2023/07/11 | roundFloat() 追加 |
プログラムの流れ
解説:ブラックリスト辞書探索API
URL |
---|
https://www.pahoo.org/e-soul/webtech/php02/program/passwordStrength2API.php |
フィールド名 | 要否 | 内 容 |
---|---|---|
version | 必須 | コールするAPIバージョン。"1.0.0"固定。 |
digest | 必須 | パスワードをMD5でハッシュ化した16進文字列。 |
148: //メイン・プログラム =======================================================
149: //パラメータを受け取る
150: $errmsg = '';
151: $patterns = array('/^[0-9\.]+$/iu');
152: $version = getValidString('version', $errmsg, $def='', 5, 5, TRUE, $patterns);
153: if ($errmsg == '') {
154: if (API_VERSION == $version) {
155: $patterns = array('/^[0123456789ABCDEF]+$/iu');
156: $digest = getValidString('digest', $errmsg, $def='', DIGEST_LENGTH * 2, DIGEST_LENGTH * 2, TRUE, $patterns);
157: } else {
158: $errmsg = 'APIのバージョンが異なります';
159: }
160: }
161: $words = $ret = '';
162: if ($errmsg == '') {
163: $words = countDictionary();
164: $ret = inDictionary(hex2bin($digest), $errmsg) ? 'match' : 'unmatch';
165: }
166:
167: //API応答(JSON)
168: $arr = array(
169: 'version' => API_VERSION, //APIバージョン
170: 'error' => $errmsg, //エラー・メッセージ
171: 'result' => $ret, //'match':辞書にある,'unmatch':辞書にない
172: 'words' => $words //辞書に登録されているパスワード数
173: );
174: echo json_encode($arr);
クライアント(JavaScript)からパスワードをハッシュ化して問い合わせることで、サーバ側にあるブラックリスト辞書を突合し、結果をJSON形式で返すWebAPIである。
ハッシュ化関数は MD5 を利用した。すでに弱点が発見されており、暗号化ハッシュとしての強度は失われているが、本用途ではそこまで強度が求められないことと、ハッシュ値が16バイト固定長と短く、辞書ファイルのサイズを小さくできることから、これを利用することにした。
このブラックリスト辞書は、「C++でパスワードの強度を調べる」で作成したものを転用している。
解説:指定したハッシュ値が辞書に存在するかどうか
122: /**
123: * 指定したハッシュ値が辞書に存在するかどうかを求める.
124: * @param string $digest ハッシュ値(MD5)
125: * @param string $errmsg エラー・メッセージ格納用
126: * @return bool TRUE:含まれている/FALSE:含まれていない,又はエラー
127: */
128: function inDictionary($digest, &$errmsg) {
129: //辞書ファイルに含まれているかどうか探す.
130: $ret = FALSE;
131: $infp = @fopen(FILENAME_DIC, 'rb');
132: if ($infp == NULL) {
133: $errmsg = "辞書ファイルが見当たりません";
134: return FALSE;
135: }
136: while (! feof($infp)) {
137: $dd = fread($infp, DIGEST_LENGTH);
138: if ($dd == $digest) {
139: $ret = TRUE;
140: break;
141: }
142: }
143: fclose($infp);
144:
145: return $ret;
146: }
ブラックリスト辞書は固定長バイナリファイルなので、whileループで回して合致するハッシュ値があるかどうかを調べている。
解説:辞書ファイルに登録されているパスワード数
100: /**
101: * 辞書ファイルに登録されているパスワード数を求める.
102: * パスワード数は"1234","約10万"のような文字列で返す.
103: * @param なし
104: * @return string パスワード数
105: */
106: function countDictionary() {
107: $fsize = (double)filesize(FILENAME_DIC) / DIGEST_LENGTH;
108:
109: if ($fsize < 10000) {
110: $ret = sprintf(" (登録語数:%.0f)", $fsize);
111: } else if ($fsize < 100000000) {
112: $ret = sprintf(" (登録語数:約%.0f万)", $fsize / 10000);
113: } else if ($fsize < 1000000000000) {
114: $ret = sprintf(" (登録語数:約%.0f億)", $fsize / 100000000);
115: } else {
116: $ret = sprintf(" (登録語数:1兆以上)");
117: }
118:
119: return $ret;
120: }
ブラックリスト辞書は固定長バイナリファイルなので、ハッシュ値の長さ DIGEST_LENGTH で除算すれば、登録語数を得られる。
読みやすいように、語数に応じて日本語に変換した文字列を戻す。
解説:JavaScript側
12: <!DOCTYPE html>
13: <html lang="ja">
14: <head>
15: <meta charset="UTF-8">
16: <title></title>
17: <style>
18: #result {
19: font-wqeght: bold;
20: color: blue;
21: }
22: #error {
23: font-wqeght: bold;
24: color: red;
25: }
26: </style>
27:
28: <!-- jQuery -->
29: <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.3/jquery.min.js"></script>
30: <!-- CDN:sprintf関数 -->
31: <script src="https://cdnjs.cloudflare.com/ajax/libs/sprintf/1.1.2/sprintf.min.js"></script>
32: <!-- MD5計算ライブラリ (c)mitsunari@cybozu labs -->
33: <!-- https://labs.cybozu.co.jp/blog/mitsunari/2007/07/md5js_1.html -->
34: <script src="md5.js"></script>
35:
36: <script>
37: // 初期値 ==================================================================
38: //プログラム・タイトル
39: const TITLE = 'パスワードの強度を調べる(その2)';
40: //参照サイト
41: const REFERENCE = 'https://www.pahoo.org/e-soul/webtech/php02/php02-56-11.shtm';
42: //表示幅(ピクセル)
43: const WIDTH = 600;
44:
45: //ブラックリスト辞書探索API【passwordStrength2API.phpに合わせて変更すること】
46: const SEARCH_BLACKLIST_API= 'https://www.pahoo.org/e-soul/webtech/php02/program/passwordStrength2API.php';
47: //APIバージョン
48: const API_VERSION = '1.0.0';
49: //パスワードの最小長
50: const PASSWORD_MINIMUM_LENGTH = 8;
51: //パスワードの最大長
52: const PASSWORD_MAXIMUM_LENGTH = 64;
53: //パスワードの初期値
54: const DEF_QUERY = 'pa$$w0rd';
55: //パスワード解読に要する時間(1パターンあたり)【変更不可】
56: const DECODE_TIME = 5.61e-12; //RTX 4090 https://gigazine.net/news/20240502-nvidia-gpus-solve-password/
外部ライブラリとして、APIコールを省力化するために jQuery を(「プログラミング入門 - 7.2 jQueryによる実装」参照)、表示が読みやすいように CDN:sprintf を(「プログラミング入門 - 6.2 書式付き出力」参照)、MD5値を計算するために MD5計算ライブラリ((c)mitsunari@cybozu labs)を利用している。
定数は、【変更不可】としているもの以外は変更可能である。
前述のブラックリスト辞書探索APIを呼び出すURLは SEARCH_BLACKLIST_AP に代入すること。
解説:指定した文字列が数字だけかどうか
87: /**
88: * 指定した文字列が数字だけかどうかを求める.
89: * @param string str 文字列
90: * @return bool true:数字だけである/false:ではない
91: */
92: function isNumber(str) {
93: let pat = '^[0-9]+$';
94: reg = new RegExp(pat);
95:
96: return (str.match(reg) == null) ? false : true;
97: }
JavaScriptによる正規表現については、「プログラミング入門 - 5.4 正規表現」をご覧いただきたい。
解説:指定した文字列が英数n文字以下かどうか
99: /**
100: * 指定した文字列が英数字だけかどうかを求める.
101: * @param string str 文字列
102: * @return bool true:英数字だけである/false:ではない
103: */
104: function isAlphanumeric(str) {
105: let pat = '^[A-Z|a-z|0-9]$';
106: reg = new RegExp(pat);
107:
108: return (str.match(reg) == null) ? false : true;
109: }
解説:指定した文字列が辞書に存在するかどうか
111: /**
112: * 指定した文字列が辞書に存在するかどうかを求める.
113: * 指定した文字列を小文字に統一して辞書ファイルと比較する.
114: * @param string str 文字列
115: * @return string 辞書にあるパスワード数/null:一致するパスワードはない
116: */
117: function inDictionary(str) {
118: let s0 = str.toLowerCase()
119: let digest = CybozuLabs.MD5.calc(s0);
120: let ret = null;
121:
122: //WebAPI呼び出し
123: $.ajax({
124: url: SEARCH_BLACKLIST_API,
125: type: 'POST',
126: async: false, //非同期通信フラグの指定
127: timeout: 1000, //タイムアウト時間(ミリ秒)
128: data: {
129: version: API_VERSION,
130: digest: digest,
131: },
132: })
133:
134: //WebAPI接続成功
135: .done(function (result) {
136: console.log(result);
137: let obj = JSON.parse(result);
138: //エラー・チェック
139: if (obj.error != '') {
140: console.error(obj.error);
141: $('#error').html('エラー:' + obj.error);
142: //一致している
143: } else if (obj.result == 'match') {
144: //辞書に含まれているパスワード数
145: if (obj.words != '') {
146: ret = obj.words;
147: } else {
148: ret = '';
149: }
150: }
151: })
152:
153: //WebAPI接続失敗
154: .fail(function (data) {
155: console.error(data);
156: errmsg = 'WebAPIに接続できません.'
157: console.error(errmsg);
158: $('#error').html('エラー:' + errmsg);
159: });
160:
161: return ret;
162: }
入力されたパスワードを、上述のMD5計算ライブラリを用いてハッシュ化し、jQuery を用いてAPIを呼び出す。詳しくは、「プログラミング入門 - 7.2 jQueryによる実装」をご覧いただきたい。
解説:指定した文字列が記号を含まないかどうか
164: /**
165: * 指定した文字列が記号を含まないかどうかを求める.
166: * @param string str 文字列
167: * @return bool true:記号が含んでいない/false:含んでいる
168: */
169: function isContainNoSymbol(str) {
170: let pat = '^[A-Z|a-z|0-9]+$';
171: reg = new RegExp(pat);
172:
173: return (str.match(reg) == null) ? false : true;
174: }
解説:指定した文字列に連続した文字を含むかどうか
176: /**
177: * 指定した文字列に連続した文字を含むかどうかを求める.
178: * @param string str 文字列
179: * @return bool true:連続した文字を含む/false:含まない
180: */
181: function isSeqCharacters(str) {
182: let ret = false;
183: let len = str.length;
184: if (len > 1) {
185: for (let i = 1; i < len; i++) {
186: if (str[i] == str[i - 1]) {
187: ret = true;
188: break;
189: }
190: }
191: }
192: return ret;
193: }
解説:総当たりで解読するときの時間
195: /**
196: * 指定したパスワードを総当たりで解読するときの時間を求める.
197: * @param string psw パスワード
198: * @return string 解読時間
199: */
200: function calcDecodeTime(psw) {
201: //文字種と桁数を求める.
202: let num = 0
203: let len = 0;
204: if (psw.match(/^[0-9]+$/) != null) {
205: num = 10;
206: } else if (psw.match(/^[A-Z]+$/) != null) {
207: num = 26;
208: } else if (psw.match(/^[0-9|A-Z]+$/) != null) {
209: num = 36;
210: } else if (psw.match(/^[a-z]+$/) != null) {
211: num = 26;
212: } else if (psw.match(/^[0-9|a-z]+$/) != null) {
213: num = 36;
214: } else if (psw.match(/^[A-Z|a-z]+$/) != null) {
215: num = 52;
216: } else if (psw.match(/^[0-9|A-Z|a-z]+$/) != null) {
217: num = 62;
218: } else {
219: num = 86;
220: }
221: len = psw.length;
222:
223: //解読時間を計算する.
224: let ret = '';
225: let sec = Math.pow(num, len) * DECODE_TIME;
226: if (sec <= 1) {
227: ret = sprintf('1秒以下で解読できる.');
228: } else if (sec < 60) {
229: ret = sprintf('解読に約%.0f秒かかる.', sec);
230: } else if (sec < 60 * 60) {
231: ret = sprintf('解読に約%.0f分かかる.', sec / 60);
232: } else if (sec < 60 * 60 * 24) {
233: ret = sprintf('解読に約%.0f時間かかる.', sec / (60 * 60));
234: } else if (sec < 60 * 60 * 24 * 30) {
235: ret = sprintf('解読に約%.0f日かかる.', sec / (60 * 60 * 24));
236: } else if (sec < 60 * 60 * 24 * 30 * 12) {
237: ret = sprintf('解読に約%.0fヶ月かかる.', sec / (60 * 60 * 24 * 30));
238: } else if (sec < 60 * 60 * 24 * 30 * 12 * 1000) {
239: ret = sprintf('解読に約%.0f年かかる.', sec / (60 * 60 * 24 * 30 * 12));
240: } else {
241: ret = sprintf('解読に1000年以上かかる.');
242: }
243:
244: return ret;
245: }
使用されている文字種の数を変数 num に、パスワード長を変数 len に格納し、num の len 乗が総当たりパターン数となる。これに1パターンあたりの解読時間 DECODE_TIME を乗ずることで解読時間(秒)を求める。NVIDIA製GPU「RTX 4090」1基で、英小文字8文字からなるパスワードを5分で解読できるという記事(NVIDIAの高性能グラボは複雑なパスワードも短時間で突破可能
\[ \displaystyle DECODE\_TIME = \frac{5 \times 60}{(26 \times 2) ^ 8} \]
と算出した。
この関数は、結果を読みやすいように日本語文字列に変換して戻す。
解説:パスワードの強度を求める
247: /**
248: * パスワードの強度を求める.
249: * 強度は1~5の整数で,数字が大きいほど強度が強い.
250: * 1:数字のみ
251: * 2:英数n文字以下
252: * 3:ブラックリスト辞書に存在する
253: * 4:1~3をクリアし,記号が含まれていない
254: * 5:1~3をクリアし,記号が含まれている
255: * 6:1~5をクリアし,連続した文字がない
256: * @param なし
257: * @return なし
258: */
259: function getPasswordStrength() {
260: //結果をクリアすいる.
261: $('#result').html('');
262: //エラーメッセージをクリアする.
263: $('#error').html('');
264:
265: //入力したパスワード
266: let psw = $('#psw').val();
267: //空白除去
268: psw = psw.trim();
269:
270: //パスワードの強度を求める
271: let ret = '不明';
272: if ((psw.length < PASSWORD_MINIMUM_LENGTH) || (psw.length > PASSWORD_MAXIMUM_LENGTH)) {
273: ret = sprintf('強度0:%d文字未満,または%d文字超の長さ.', PASSWORD_MINIMUM_LENGTH, PASSWORD_MAXIMUM_LENGTH);
274: } else if (isNumber(psw)) {
275: ret = '強度1:数字のみ.';
276: } else if (isAlphanumeric(psw, PASSWORD_MINIMUM_LENGTH)) {
277: ret = '強度2:英数字のみ.';
278: } else if ((ss = inDictionary(psw)) != null) {
279: ret = '強度3:ブラックリスト辞書に存在する.' + ss;
280: } else if ($('#error').html() != '') { //APIエラー
281: return;
282: } else if (isContainNoSymbol(psw)) {
283: ret = '強度4:ブラックリスト辞書に存在せず,記号を含まない.';
284: } else if (isSeqCharacters(psw)) {
285: ret = '強度5:ブラックリスト辞書に存在せず,記号を含んでいる.';
286: } else {
287: ret = '強度6:ブラックリスト辞書に存在せず,記号を含んでおり,連続する文字はない.';
288: }
289:
290: //解読時間を求める.
291: ret = ret + "<br />" + calcDecodeTime(psw);
292:
293: $('#result').html(ret);
294: }
参考サイト
- MD5計算ライブラリ:mitsunari@cybozu labs
- PHPでパスワードの強度を調べる:ぱふぅ家のホームページ
- C++でパスワードの強度を調べる:ぱふぅ家のホームページ
- プログラミング入門 - 7.2 jQueryによる実装:ぱふぅ家のホームページ
そこで、強度3の判定方法として、Wikipediaの見出しに存在するかどうかではなく、漏洩したことがあるパスワードを集めたブラックリストとのマッチングを行うことにする。
また、平文のパスワードがネットに流れないようにするため、平文を扱う処理はクライアントサイド(JavaScript)で完結し、ブラックリスト辞書の探索だけをサーバサイド(PHP)に処理させることにする。
(2024年5月4日)DECODE_TIMEの値を改訂
(2023年11月23日)ブラックリスト辞書を強化