パスワードに繰り返し文字を使用させない

hayasaka

2024.02.20

42

パスワードに 11111111 とか 12121212 と設定するのはセキュリティ上良くありません

そこで繰り返し文字列の使用についてスコアを算出し、一定スコアを下回ったら警告を出す仕組みを考えます

例えばスコア 0.6 以下の値であった場合には「パスワードには連続した文字列を使用しないでください」と警告を出すような使い方です

最後にソースコードが記載してあります

パスワードに繰り返し文字を使用させない

サンプル

今回紹介するロジックを使用して作成したパスワードのスコアです

2文字分割、3文字分割 については後に説明します


文字列 = 算出したスコア

11111111 = 0.0037725407623366803

12121212 = 0.030919312169312166

12341234 = 0.33333333333333337

65872934 = 1

74829653 = 1

aaaaaaaaaa = 0.0016624335562414267

abcabcabc = 0.07316468253968254

zxpzxpzxpzxpzxpzxp = 0.0061785130718954254

abcabcabcxyzxyzxyz = 0.15788398692810457

xkskefstpwzyjszp = 1

hcksvibdvwgruusb = 1


同じ文字が連続した場合に低いスコアとなっており意図通りに動作していることが分かります

ロジック概要

与えられた文字列(パスワード)を2~3文字単位に分割し、同じ文字列が出てくる回数をカウントします

何度も出てくる単語があった場合にはスコアを低くし

単語が少ない回数しか出ていない場合はスコアを高くします

ロジック解説

1.与えられた文字列(パスワード)を先頭から2文字抽出して辞書に登録します

 1-1 抽出した2文字が辞書に存在しないのならその単語のカウンターに1を設定し登録処理を終了します

 1-2 抽出した2文字が辞書に登録済みであったらその単語のカウンターを1加算します

2.検索位置を1文字進めて「1」の処理に戻ります

 2-1 文字列の末尾の文字を辞書に取り込んだら処理を終了します

スコア算出部分の実装です

function repititionScore(text, wordLength) {
    var dicionary = {};
    var size = text.length;
    var procSize = size - (wordLength - 1);
    for(var i = 0; i < procSize; i += 1) {
        var word = text.substring(i, i + wordLength);
        if(word in dicionary) {
            dicionary[word] += 1;
        } else {
            dicionary[word] = 1;
        }
    }
    prt("text", text);
    prt(dicionary);
    var score = 0;
    
    for(k in dicionary) {
        score += 1 / dicionary[k] ** 2;
    }
    score = score / (procSize);
    return score;
}


文字列 abcabcabcxyzxyzxyz に対して処理を行うと以下のような辞書が作られます


{
  "ab": 3,
  "bc": 3,
  "ca": 2,
  "cx": 1,
  "xy": 3,
  "yz": 3,
  "zx": 2
}


これらの 2乗の逆数 の 平均値 をスコアとします


>"bc": 3,

>"ca": 2,

この2つについて計算すると

(1/(3*3) + 1/(2*2)) / 2 = 0.361

となりスコアは 0.361 となります

辞書全てを計算すると 0.11437908496732027 となります(計算省略)



文字列 11111111 に対して実施した場合は以下のような辞書が作られます

{
  "11": 7
}

0.0204081632653061 / 7 = 0.0029154518950437

となります


ソースコードは最後に全て掲載していますので興味があれば御覧ください

本ロジックの使い方

例えばですが

・最低8文字とする

・8文字区切り(最低文字数区切り)ですべての箇所でスコアを算出し、一番スコアの高い箇所を採用する

という使い方になります


32文字パスワードだった場合に全部の文字列を一発で判定してしまえば良いのでは?

と思うかもしれませんがその場合に納得いかないような結果になります

例えばですが

!!!!!!!!!!!!!!!!!!!!xkskefstpwzyjszp!!!!!!!!!!!!!!!!!!!!

このようなパスワード文字列を指定した場合にスコアが 0.32 と低くなってしまいます

真ん中のランダム文字列がかなりセキュアだとは思いますし、桁数が多く桁数予測もしづらいです

ですが !!! の繰り返しが低スコア判定となり強固なパスワードを指定したにも関わらず低スコアとなってしまいます

2文字分割と3文字分割について

先の説明で

先頭から2文字抽出して辞書に登録します

とありました、これはパラメータで変更でき辞書を作成する際に辞書単語を「2文字」にするか「3文字」にするかでスコアが微妙に異なります

4文字以上の指定も可能です

実装部分は

function repititionScore(text, wordLength)

になります

3文字のほうがスコアがやや高くなる傾向にあります

「3文字は繰り返しに入らない、2文字で充分」とかいう人間の感覚的な問題に対応するためにある可変パラメータです

実際にNGパターンパスワードを作成し納得いくスコアの出る方を採用すれば良いと思います

ソースコード

以下のソースを html ファイルとして保存しブラウザで開けば動くと思います


<html lang="ja">
<head>
    <meta charset="utf-8">
</head>
<body>
    <div id="sample_area">
    </div>
    <div id="operation_area">
        <button type="button" id="operation_area_reset_button">
            Reset
        </button>
        <button type="button" id="operation_area_submit_button">
            判定する
        </button>
        <br>
    </div>
    <div id="log_area">
        <textarea id="log_arrea_view" style="width: 200px; height: 600px; background-color: #DDDDDD;">
        </textarea>
    </div>


    <div id="templates" style="display: none;">
        <div id="template_password_view" name="o_password_text">
            <input type="text" id="set_up_id_you_want_1" name="password_value">
                <span>score: </span>
                <span>2char= </span><span name="password_score2"></span>
                <span>, 3char= </span><span name="password_score3"></span>
                <span>, average= </span><span name="password_score_avr"></span>
        </div>
    </div>


    <script type="text/javascript">


function prt() {
    console.log.apply(null, arguments);
}


function onReady(fn) {
    setTimeout(function() {
        fn();
    }, 1);
}


function repititionScore(text, wordLength) {
    var dicionary = {};
    var size = text.length;
    var procSize = size - (wordLength - 1);
    for(var i = 0; i < procSize; i += 1) {
        var word = text.substring(i, i + wordLength);
        if(word in dicionary) {
            dicionary[word] += 1;
        } else {
            dicionary[word] = 1;
        }
    }
    prt("text", text);
    prt(dicionary);
    var score = 0;
    
    for(k in dicionary) {
        score += 1 / dicionary[k] ** 2;
    }
    score = score / (procSize);
    return score;
}


function repititionScore2(text) {
    return repititionScore(text, 2);
}


function repititionScore3(text) {
    return repititionScore(text, 3);
}


////////////////////////////////////////
// メインクラス
////////////////////////////////////////
function PasswordCheck() {
    var self = this;
    self.samples = [
        "11111111",
        "12121212",
        "12341234",
        "65872934",
        "74829653",
        "aaaaaaaaaa",
        "abcabcabc",
        "zxpzxpzxpzxpzxpzxp",
        "abcabcabcxyzxyzxyz",
        "xkskefstpwzyjszp",
        "hcksvibdvwgruusb"
    ];
    self.initElements = function() {
        var me = this;
        return new Promise(function(resolve) {
            var area = document.getElementById("sample_area");
            var tpl = document.getElementById("template_password_view");
            var samples = me.samples;
            area.querySelectorAll("*").forEach(function(element) {
                element.remove();
                //prt(element);
            });
            for(var i = 0; i < samples.length; i += 1) {
                var cln = tpl.cloneNode(true);
                cln.setAttribute("id", cln.getAttribute("id") + "_" + String(i));
                var inp = cln.querySelectorAll("input")[0];
                //var inp = cln.getElementsByTagName("input")[0];
                inp.setAttribute("id", "sample-input-" + String(i));
                inp.value = samples[i];
                area.appendChild(cln);
            }
            onReady(function() {
                resolve(true);
            });
        });
    }
    self.setupEvents = function() {
        var me = this;
        document.getElementById("operation_area_reset_button").addEventListener("click", function() {
            me.initElements();
        });
        document.getElementById("operation_area_submit_button").addEventListener("click", function() {
            me.scoreThePassword();
        });
    }
    self.scoreThePassword = function() {
        var me = this;
        var sample_area = document.getElementById("sample_area");
        var items = sample_area.querySelectorAll('[name="o_password_text"]');
        for(var i = 0; i < items.length; i += 1) {
            var oinp = items[i];
            var inp = oinp.querySelectorAll('[name="password_value"]')[0];
            var score2 = repititionScore2(inp.value);
            var score3 = repititionScore3(inp.value);
            var score_avr = (score2 + score3) / 2;
            oinp.querySelectorAll('[name="password_score2"]')[0].innerHTML = String(score2);
            oinp.querySelectorAll('[name="password_score3"]')[0].innerHTML = String(score3);
            oinp.querySelectorAll('[name="password_score_avr"]')[0].innerHTML = String(score_avr);
        }
    }
    self.start = function() {
        var me = this;
        return me.initElements()
        .then(function() {
            me.setupEvents();
            return true;
        });
    }
}


window.addEventListener('load', function(){
    var x = new PasswordCheck();
    x.start();
});
    </script>


</body>
</html>


この記事をシェアする