目次
- サンプル・プログラム
- 使用ライブラリ
- リソースの準備
- プログラム製作の背景
- 解説:指定した文字列が数字だけかどうか
- 解説:指定した文字列が英数n文字以下かどうか
- 解説:指定した文字列が辞書に存在するかどうか
- 解説:指定した文字列が記号を含まないかどうか
- 解説:指定した文字列に連続した文字を含むかどうか
- 解説:総当たりで解読するときの時間
- 解説:パスワードの強度を求める
- 解説:指定した文字列が数字だけかどうか
- 解説:辞書ファイルを空にする
- 解説:辞書ファイルを指定したメモリに読み込む
- 解説:指定したハッシュ値が指定したメモリになければ追加する
- 解説:指定したメモリの内容を辞書ファイルに書き込む
- 解説:ブラックリスト・ファイルを読み込んで辞書ファイルに追加
- 解説:辞書ファイルに登録されているパスワード数
- 同梱のブラックリスト辞書について
- 参考サイト
サンプル・プログラム
| passwordStrength.msi | インストーラ |
| bin/passwordStrength.exe | 実行プログラム本体(GUI版) |
| bin/pswst.exe | 実行プログラム本体(CUI版) |
| bin/etc/help.chm | ヘルプ・ファイル |
| sour/passwordStrength.cpp | ソース・プログラム |
| sour/resource.h | リソース・ヘッダ |
| sour/resource.rc | リソース・ファイル(GUI版) |
| sour/resource2.rc | リソース・ファイル(CUI版) |
| sour/application.ico | アプリケーション・アイコン(GUI版) |
| sour/application2.ico | アプリケーション・アイコン(CUI版) |
| sour/passwords.txt | ブラックリスト辞書(サンプル) |
| sour/makefile | GUI版ビルド |
| sour/makefile_cmd | CUI版ビルド |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 1.1.0 | 2026/06/14 | --levbel,--fullオプション追加, 使用ライブラリ更新 |
| 1.0.9 | 2026/03/28 | ブラックリスト辞書の強化,使用ライブラリ更新, optional名前衝突回避 |
| 1.0.8 | 2025/11/30 | 使用ライブラリ更新 |
| 1.0.7 | 2025/08/02 | 使用ライブラリ更新 |
| 1.0.6 | 2025/03/29 | ブラックリスト辞書の強化,使用ライブラリ更新 |
使用ライブラリ
リソースの準備
ResEdit を起動し、resource.rc を用意する。
Eclipse に戻り、ソース・プログラム "passwordStrength.cpp" を追加する。
リンカー・フラグを -s -mwindows -static -lstdc++ -lgcc -lole32 -lcrypto -lboost_program_options-mt -lpthread -lwinpthread" に設定する。
また、CUI版をビルドするために、構成 CMD を追加し、リンカー・フラグを -s -static -lstdc++ -lgcc -lole32 -lcrypto -lboost_program_options-mt -lpthread に設定する。
MSYS2 コマンドラインからビルドするのであれば、"makefile" と "makefile_cmd" を利用してほしい。
プログラム製作の背景
NIST(National Institute of Standards and Technology;米国立標準技術研究所)は、セキュリティ文書「NIST Special Publication 800-63B」の「5.1.1 記憶シークレット」に、パスワードの要件として次の項目を挙げている。
- 長いパスワード(8文字以上、最長64文字)
- 表示可能文字のASCII,Unicodeや空白の使用を許可
- Password1やqwerty123などの違反したパスワード、辞書単語をブラックリストに設定
- aaaa1234や123456などの連続した同じ文字の使用を制限
- 強固なパスワードチェッカーを使用
- 連続で認証失敗した場合に強制的にアカウントをロック
- パスワードの入力でペースト機能の使用を許可
- パスワードの他、別の種類による二要素認証を強制
この要件の1~4を実装した「5.パスワードチェッカー」を作ろうと考えた。
解説:指定した文字列が数字だけかどうか
passwordStrength.cpp
546: /**
547: * 指定した文字列が数字だけかどうかを求める.
548: * @param string str 文字列
549: * @return bool TRUE:数字だけである/FALSE:ではない
550: */
551: bool isNumbersOnly(string str) {
552: regex re("^[0-9]+$");
553: return regex_match(str, re);
554: }
解説:指定した文字列が英数字だけかどうか
passwordStrength.cpp
556: /**
557: * 指定した文字列が英数字だけかどうかを求める.
558: * @param string str 文字列
559: * @return bool TRUE:英数字だけである/FALSE:ではない
560: */
561: bool isAlphanumeric(string str) {
562: regex re("^[A-Z|a-z|0-9]+$");
563: return regex_match(str, re);
564: }
解説:指定した文字列が辞書に存在するかどうか
passwordStrength.cpp
566: /**
567: * 指定した文字列が辞書に存在するかどうかを求める.
568: * 指定した文字列を小文字に統一して辞書ファイルと比較する.
569: * @param string str 文字列
570: * @return bool TRUE:存在する/FALSE:存在しない、またはエラー
571: */
572: bool inDictionary(string str) {
573: unsigned char s1[SIZE_BUFF + 1];
574: unsigned char s2[SIZE_BUFF + 1];
575:
576: string s0 = str;
577: // 小文字に統一する
578: transform(s0.begin(), s0.end(), s0.begin(), ::tolower);
579: size_t len = s0.length();
580: // MD5ハッシュ値をs1に代入する.
581: MD5((const unsigned char *)s0.c_str(), len, (unsigned char *)s1);
582:
583: // printDictionary((Digest *)s1);
584: // cout << endl;
585:
586: // カーソルを砂時計に
587: HCURSOR cur = SetCursor(LoadCursor(NULL, IDC_WAIT));
588:
589: // 辞書ファイルに含まれているかどうか探す.
590: bool ret = FALSE;
591: const char *dicname = getDictionaryName();
592: FILE *infp = fopen(dicname, "rb");
593: if (infp == NULL) {
594: ErrorMessage = "辞書ファイルが見当たりません";
595: cout << ErrorMessage << endl;
596: return FALSE;
597: }
598: for (size_t i = 0; i < MAX_WORDS; i++) {
599: if (feof(infp)) break;
600: fread(s2, sizeof(Digest), 1, infp);
601:
602: // printDictionary((Digest *)s2);
603: // cout << endl;
604:
605: if (memcmp(s1, s2, sizeof(Digest)) == 0) {
606: ret = TRUE;
607: break;
608: }
609: }
610: fclose(infp);
611:
612: // カーソルを元に戻す
613: SetCursor(cur);
614:
615: return ret;
616: }
入力されたパスワードを、MD5でハッシュ化し、ブラックリスト辞書にあるかどうかを突合する。ブラックリスト辞書は固定長バイナリファイルなので、forループで回して合致するハッシュ値があるかどうかを調べている。
解説:指定した文字列が記号を含まないかどうか
passwordStrength.cpp
618: /**
619: * 指定した文字列が記号を含むかどうかを返す.
620: * @param string str 文字列
621: * @return bool TRUE:記号を含んでいない/FALSE:含んでいる
622: */
623: bool isContainNoSymbol(string str) {
624: regex re("^[A-Z|a-z|0-9]+$");
625: return regex_match(str, re);
626: }
解説:指定した文字列に連続した文字を含むかどうか
passwordStrength.cpp
628: /**
629: * 指定した文字列に連続した文字を含むかどうかを求める.
630: * @param string str 文字列
631: * @return bool true:連続した文字を含む/false:含まない
632: */
633: bool isSeqCharacters(string str) {
634: const char *ptr = str.c_str();
635: bool ret = FALSE;
636: size_t len = str.length();
637: if (len > 1) {
638: for (size_t i = 1; i < len; i++) {
639: if (ptr[i] == ptr[i - 1]) {
640: ret = TRUE;
641: break;
642: }
643: }
644: }
645: return ret;
646: }
解説:総当たりで解読するときの時間
passwordStrength.cpp
648: /**
649: * 指定したパスワードを総当たりで解読するときの時間を求める.
650: * @param string psw パスワード
651: * @return string 解読時間
652: */
653: string calcDecodeTime(string psw) {
654: // 文字種と桁数を求める.
655: regex re1("^[0-9]+$");
656: regex re2("^[A-Z]+$");
657: regex re3("^[0-9|A-Z]+$");
658: regex re4("^[a-z]+$");
659: regex re5("^[0-9|a-z]+$");
660: regex re6("^[A-Z|a-z]+$");
661: regex re7("^[0-9|A-Z|a-z]+$");
662: size_t num, len;
663: if (regex_match(psw, re1)) {
664: num = 10;
665: } else if (regex_match(psw, re2)) {
666: num = 26;
667: } else if (regex_match(psw, re3)) {
668: num = 36;
669: } else if (regex_match(psw, re4)) {
670: num = 26;
671: } else if (regex_match(psw, re5)) {
672: num = 36;
673: } else if (regex_match(psw, re6)) {
674: num = 52;
675: } else if (regex_match(psw, re7)) {
676: num = 62;
677: } else {
678: num = 86;
679: }
680: len = psw.length();
681:
682: // 解読時間を計算する.
683: static char buff[SIZE_BUFF + 1];
684: double sec = (double)pow(num, len) * DECODE_TIME;
685: if (sec <= 1) {
686: snprintf(buff, sizeof(buff), "1秒以下で解読できる.");
687: } else if (sec < 60) {
688: snprintf(buff, sizeof(buff), "解読に約%.0f秒かかる.", sec);
689: } else if (sec < (double)60 * 60) {
690: snprintf(buff, sizeof(buff), "解読に約%.0f分かかる.", sec / 60);
691: } else if (sec < (double)60 * 60 * 24) {
692: snprintf(buff, sizeof(buff), "解読に約%.0f時間かかる.", sec / (60 * 60));
693: } else if (sec < (double)60 * 60 * 24 * 30) {
694: snprintf(buff, sizeof(buff), "解読に約%.0f日かかる.", sec / (60 * 60 * 24));
695: } else if (sec < (double)60 * 60 * 24 * 30 * 12) {
696: snprintf(buff, sizeof(buff), "解読に約%.0fヶ月かかる.", sec / (60 * 60 * 24 * 30));
697: } else if (sec < (double)60 * 60 * 24 * 30 * 12 * 1000) {
698: snprintf(buff, sizeof(buff), "解読に約%.0f年かかる.", sec / ((double)60 * 60 * 24 * 30 * 12));
699: } else {
700: snprintf(buff, sizeof(buff), "解読に1000年以上かかる.");
701: }
702:
703: return (string)buff;
704: }
使用されている文字種の数を変数 num に、パスワード長を変数 len に格納し、num の len 乗がが総当たりパターン数となる。これに1パターンあたりの解読時間 DECODE_TIME を乗ずることで解読時間(秒)を求める。NVIDIA製GPU「RTX 4090」1基で、英小文字8文字からなるパスワードを5分で解読できるという記事(NVIDIAの高性能グラボは複雑なパスワードも短時間で突破可能, GIGAZINE, 2024年5月2日)より、$$ \displaystyle DECODE\_TIME = \frac{5 \times 60}{(26 \times 2) ^ 8} $$ と算出した。
この関数は、結果を読みやすいように日本語文字列に変換して戻す。
解説:パスワードの強度を求める
passwordStrength.cpp
706: /**
707: * 指定したパスワードの強度を求める.
708: * 強度は1〜5の整数で,数字が大きいほど強度が強い.
709: * @param string psw パスワード
710: * @param size_t min パスワードの最小長;省略時はPASSWORD_MINIMUM_LENGTH
711: * @param size_t max パスワードの最大長;省略時はPASSWORD_MAXIMUM_LENGTH
712: * @return int 強度
713: * 1:数字のみ
714: * 2:英数n文字以下
715: * 3:ブラックリスト辞書に存在する
716: * 4:1〜3をクリアし,記号が含まれていない
717: * 5:1〜3をクリアし,記号が含まれている
718: * 6:1〜5をクリアし,連続した文字がない
719: */
720: int getPasswordStrength(string psw, size_t min=PASSWORD_MINIMUM_LENGTH, size_t max=PASSWORD_MAXIMUM_LENGTH) {
721: // 空白を除く
722: string str = regex_replace(psw, regex("[ \\t]+"), "");
723:
724: // パスワードstrの強度を算出する.
725: int ret = 0;
726: if ((psw.length() < min) || (psw.length() > max)) {
727: ret = 0;
728: } else if (isNumbersOnly(str)) {
729: ret = 1;
730: } else if (isAlphanumeric(str)) {
731: ret = 2;
732: } else if (inDictionary(str)) {
733: ret = 3;
734: } else if (isContainNoSymbol(str)) {
735: ret = 4;
736: } else if (isSeqCharacters(str)) {
737: ret = 5;
738: } else {
739: ret = 6;
740: }
741: return ret;
742: }
解説:辞書ファイル名を求める
passwordStrength.cpp
296: /**
297: * 辞書ファイル名を求める.
298: * @param なし
299: * @return string 辞書ファイル名
300: */
301: const char *getDictionaryName(void) {
302: static char buff[SIZE_BUFF + 1];
303: strncpy(buff, (getMyPath(APPNAME) + FILENAME_DIC).c_str(), SIZE_BUFF);
304: return (const char *)buff;
305: }
まず、ユーザー関数 getDictionaryName は、ブラックリスト辞書ファイルへのフルパスを求める。ユーザーのAppDataの下にある。
解説:辞書ファイルを空にする
passwordStrength.cpp
321: /**
322: * 辞書ファイルを空にする.
323: * @param なし
324: * @return bool TRUE:成功/FALSE:失敗
325: */
326: bool emptyDictionary() {
327: const char *outfname = getDictionaryName();
328: FILE *outfp;
329: outfp = fopen(outfname, "wb");
330: fclose(outfp);
331:
332: return TRUE;
333: }
解説:辞書ファイルを指定したメモリに読み込む
passwordStrength.cpp
335: /**
336: * 辞書ファイルを指定したメモリに読み込む.
337: * @param unsigned char *dic 読み込むメモリ
338: * @return size_t 読み込んだ見出語の数
339: */
340: size_t readDictionary(unsigned char *dic) {
341: size_t cnt;
342: const char *infname = getDictionaryName();
343: FILE *infp;
344: infp = fopen(infname, "rb");
345: if (infp == NULL) {
346: ErrorMessage = "辞書ファイルが見つかりません";
347: return 0;
348: }
349:
350: // 辞書カウンタを初期化してメモリに読み込む.
351: DictionaryCounter = 0;
352: for (cnt = 0; cnt < MAX_WORDS; cnt++) {
353: if (feof(infp)) break;
354: fread(dic + cnt * sizeof(Digest), sizeof(Digest), 1, infp);
355: DictionaryCounter++;
356: }
357: fclose(infp);
358:
359: return cnt;
360: }
解説:指定したハッシュ値が指定したメモリになければ追加する
passwordStrength.cpp
380: /**
381: * 指定したハッシュ値が指定したメモリになければ追加する.
382: * 直近に登録した50万件に重複がなければ追加する.
383: * @param unsigned char *dic ハッシュ値格納メモリ
384: * @param Digest *md5 ハッシュ値
385: * @return bool TRUE:追加成功/FALSE:失敗
386: */
387: bool addDictionary(unsigned char *dic, Digest *md5) {
388: static Digest zero;
389: memset((void *)&zero, 0, sizeof(Digest));
390: size_t i;
391: static unsigned char *pointer;
392: static size_t ll = sizeof(Digest);
393: size_t cnt;
394:
395: // BACK_WORDS語だけ遡って重複チェックする.
396: if (DictionaryCounter > BACK_WORDS) {
397: cnt = DictionaryCounter - BACK_WORDS;
398: pointer = dic + cnt * ll;
399: // それ以外
400: } else {
401: cnt = 0;
402: pointer = dic;
403: }
404:
405: // 指定したハッシュ値が指定したメモリになければ追加する.
406: for (i = cnt; i < MAX_WORDS; i++) {
407: if (memcmp(pointer, (void *)&zero, ll) == 0) {
408: break;
409: } else if (memcmp(pointer, (void *)md5, ll) == 0) {
410: return FALSE;
411: }
412: pointer += ll;
413: }
414:
415: // 登録可能かどうかを検査する.
416: if (i >= MAX_WORDS) {
417: ErrorMessage = "辞書に登録できない";
418: return FALSE;
419: } else {
420: memcpy(dic + i * sizeof(Digest), (void *)md5, sizeof(Digest));
421: DictionaryCounter++;
422: return TRUE;
423: }
424: }
このとき、BACK_WORDS語だけ遡って重複チェックし、同じハッシュ値を見つけたら追加しないようにした。本当は銭湯に戻って重複チェックをしたかったのだが、Wikipedia見出しのように1千万語超になると重複チェックがボトルネックとなり、ブラックリスト辞書の追加に1日以上かかることから、このような処理にした。
解説:指定したメモリの内容を辞書ファイルに書き込む
passwordStrength.cpp
362: /**
363: * 指定したメモリの内容を辞書ファイルに書き込む.
364: * @param unsigned char *dic 書き込むメモリ
365: * @param size_t cnt 見出し語の数
366: * @return bool TRUE:書き込み成功/FALSE:失敗
367: */
368: bool writeDictionary(unsigned char *dic, size_t cnt) {
369: const char *outfname = getDictionaryName();
370: FILE *outfp;
371: outfp = fopen(outfname, "wb");
372: for (size_t i = 0; i < cnt; i++) {
373: fwrite(dic + i * sizeof(Digest), sizeof(Digest), 1, outfp);
374: }
375: fclose(outfp);
376:
377: return TRUE;
378: }
解説:ブラックリスト・ファイルを読み込んで辞書ファイルに追加
passwordStrength.cpp
426: /**
427: * ブラックリスト・ファイルを読み込んで,辞書ファイルに追加する.
428: * @param const char* infname パスワード・ファイル名
429: * @param unsigned column 読み込むカラム番号
430: * @return size_t 読み込んだ見出語の数
431: */
432: size_t readKeywords2Dictionary(const char* infname, unsigned column) {
433: // カーソルを砂時計に
434: HCURSOR cur = SetCursor(LoadCursor(NULL, IDC_WAIT));
435:
436: // 辞書読み込み用のメモリを確保する。
437: unsigned char *dic;
438: dic = (unsigned char *)malloc((MAX_WORDS + 1) * sizeof(Digest));
439: memset(dic, 0, (MAX_WORDS + 1) * sizeof(Digest));
440:
441: // 辞書ファイルをメモリに読み込む.
442: if (readDictionary(dic) == FALSE) {
443: return FALSE;
444: }
445:
446: // ブラックリスト・ファイルを読み込みオープンする.
447: static char s1[SIZE_BUFF + 1];
448: static char s2[SIZE_BUFF + 1];
449: FILE *infp;
450: infp = fopen(infname, "rb");
451: if (infp == NULL) {
452: ErrorMessage = (string)infname + " の読み込みに失敗しました";
453: fclose(infp);
454: return FALSE;
455: }
456:
457: // パスワードを1つずつ読み込み,ハッシュ値に変換して辞書ファイルへ追加する.
458: size_t cnt = 0;
459: size_t cnt2 = 0;
460: size_t len = 0;
461: while (cnt2 < MAX_WORDS) {
462: if (feof(infp)) break;
463: if (fgets(s1, SIZE_BUFF, infp) == NULL) break;
464: len = strlen(s1);
465: int k = 0;
466: unsigned col = 1; // カラム番号
467: for (size_t j = 0; j < len; j++) {
468: // カラム区切り文字を見つけたら,カラム番号を1つ増やす.
469: // 読み込むカラム番号を超えたら読み込みを打ち切る.
470: if ((s1[j] == '\t') || (s1[j] == ' ') || (s1[j] == ',')) {
471: col++;
472: if (col > column) {
473: break;
474: }
475: k = 0;
476: // 英数記号を取り込む.
477: } else if (col == column) {
478: if ((s1[j] >= '!') && (s1[j] <= '~')) {
479: // 小文字に統一する.
480: s2[k] = tolower(s1[j]);
481: k++;
482: }
483: }
484: }
485: s2[k] = '\0';
486:
487: // 最小長より長ければ辞書に登録する.
488: static Digest dd;
489: if (k >= PASSWORD_MINIMUM_LENGTH) {
490: // MD5ハッシュ値をddに代入する.
491: MD5((const unsigned char *)s2, k, (unsigned char *)&dd);
492:
493: // cout << s2 << endl;
494: // printDictionary((Digest *)dd);
495: // cout << endl;
496:
497: // 読み込みカウンタ
498: if (cnt % 10000 == 0) {
499: cout << cnt << "=>" << cnt2 << '\r';
500: }
501: if (addDictionary(dic, &dd) == TRUE) {
502: cnt2++;
503: }
504: cnt++;
505: }
506: }
507: fclose(infp);
508:
509: // 辞書ファイルへ書き込む.
510: writeDictionary(dic, DictionaryCounter);
511: cout << "\nDictionaryCounter = " << DictionaryCounter << endl;
512: // メモリを解放する.
513: free(dic);
514:
515: // カーソルを元に戻す
516: SetCursor(cur);
517:
518: return cnt2;
519: }
ブラックリスト・ファイルはテキストファイルで、1行に1パスワードが記載されているものとする。タブ、空白、カンマでカラムが区切られていてもよく、その場合は、columnにパスワードがあるカラム番号(左端の先頭が1)を指定してやる。
解説:辞書ファイルに登録されているパスワード数
passwordStrength.cpp
521: /**
522: * 辞書ファイルに登録されているパスワード数を求める.
523: * パスワード数は"1234","約10万"のような文字列で返す.
524: * @param なし
525: * @return string パスワード数
526: */
527: string countDictionary(void) {
528: const char *infname = getDictionaryName();
529: double fsize = filesystem::file_size((string)infname) / sizeof(Digest);
530: char buff[SIZE_BUFF + 1];
531:
532: if (fsize < 10000) {
533: snprintf(buff, SIZE_BUFF, " (登録語数:%.0f)", fsize);
534: } else if (fsize < 100000000) {
535: snprintf(buff, SIZE_BUFF, " (登録語数:約%.0f万)", fsize / 10000);
536: } else if (fsize < 1000000000000) {
537: snprintf(buff, SIZE_BUFF, " (登録語数:約%.0f億)", fsize / 100000000);
538: } else {
539: snprintf(buff, SIZE_BUFF, " (登録語数:1兆以上)");
540: }
541:
542: return buff;
543: }
ブラックリスト辞書は固定長バイナリファイルなので、ハッシュ値の長さ DIGEST_LENGTH で除算すれば、登録語数を得られる。
読みやすいように、語数に応じて日本語に変換した文字列を戻す。
同梱のブラックリスト辞書について
- Wikipedia英語版 見出し語
- zxcvbn:Dropbox,2015年11月
- 使ってはいけないパスワードトップ10万:英NCSC,2019年4月
- 10-million-password-list-top-1000000.txt:Daniel Miessler,2020年7月
参考サイト
- PHPでパスワードの強度を調べる:ぱふぅ家のホームページ
- PHPでパスワードの強度を調べる(その2):ぱふぅ家のホームページ
- WiX によるWindowsインストーラー作成:ぱふぅ家のホームページ
- C++ 開発環境の準備:ぱふぅ家のホームページ
- NIST Special Publication 800-63B:NIST

「PHPでパスワードの強度を調べる(その2)」で作ったPHPプログラムを移植したものである。
(2026年6月14日)--levbel,--fullオプション追加, 使用ライブラリ更新
(2026年3月28日)ブラックリスト辞書の強化,使用ライブラリ更新, optional名前衝突回避
(2025年11月30日)使用ライブラリ更新
(2025年8月2日)使用ライブラリ更新