概要
職場でこんなトラブルに遭遇しました。
「英数字6ケタのパスワードが、5ケタで生成されており、エラーになった」
Pythonを使って、このエラーを再現してみました。
原因
「英数字6ケタ」で大量にランダム生成されたパスワード文字列のうち、ひとつだけ「数字のみ5ケタ」になっていました。
"012345"のように、先頭が0・残りが数字で生成されたため、Excelファイルでやりとりする際に先頭のゼロが省略されて"12345"になったようです。
(なんでmkpasswdのオプションで指定しないの…)
発生確率
この事象が起きる確率を計算してみます。
まず、ある1ケタの取りうる値は、[0-9, a-z]なので、10+26=36種類です。
つまり、0になる確率は、1/36。
一方、数字0-9になる確率は、10/36です。
今回の事象は、先頭1ケタが0・残りの5ケタが数字になる時なので、
(1/36) * (10/36) * (10/36) * (10/36) * (10/36) * (10/36)
= 0.0000459393658… となります。
パーセントにすると、0.0046%。
メガバンクの普通預金金利=0.001%の4倍強ですね。
(パーセントの意味合いが異なりますが)
分数にすると、1/21767.8になります。
再現
このパスワード生成をPythonで再現してみます。
事象が発生するまでパスワード生成を繰り返して、
何回目で発生したかを記録したり、複数回発生させた平均値を出したりしてみます。
コード
import re
import random
from statistics import mean
#可変値
turns = 10
passwd_len = 6
#エラー率の計算(%)
err = 100 * (1 / 36) * (10 / 36) ** (passwd_len - 1)
'''
#ループを使って数字と小文字のリストを作成
#→固定値にしたのでコメントアウト
char_list = []
#数字
for j in range(10):
char_list.append(str(j))
#小文字アルファベット
for i in range(97, 123):
char_list.append(chr(i))
'''
#固定値。数字と小文字のリスト
char_list = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',]
#エラーになった時の試行回数を格納するリスト
challenge_count = []
print('文字列の長さ:' + str(passwd_len))
print(f'エラー率:{err:.8f}%')
print(str(round(100 / err, 1)) + '回に1回はエラーになる計算')
print('--------')
for turn in range(turns):
i = 1
while True:
passwd = ''.join(random.choices(char_list, k=passwd_len))
if passwd[0] == '0' and len(re.findall('[0-9]', passwd)) == passwd_len:
print(passwd, ': ', i, '回目')
challenge_count.append(i)
break
i += 1
print('--------')
print('試行回数 :',turn + 1)
print('最小値 :',min(challenge_count))
print('最大値 :',max(challenge_count))
print('平均値 :',mean(challenge_count))
実行結果
文字列の長さ:6
エラー率:0.00459394%
21767.8回に1回はエラーになる計算
--------
061109 : 63096 回目
085726 : 17000 回目
075952 : 9555 回目
078113 : 28085 回目
061468 : 17485 回目
042766 : 13637 回目
045780 : 9737 回目
023303 : 11550 回目
092385 : 19823 回目
039489 : 31942 回目
--------
試行回数 : 10
最小値 : 9555
最大値 : 63096
平均値 : 22191
文字列の長さを増やしてみる(6→8)
文字列の長さ:8
エラー率:0.00035447%
282111.0回に1回はエラーになる計算
--------
09152768 : 743009 回目
01711507 : 66256 回目
09156634 : 65775 回目
09544840 : 892213 回目
09402244 : 497038 回目
05226558 : 43195 回目
00636774 : 682084 回目
09395383 : 7369 回目
07805687 : 564570 回目
02889286 : 442124 回目
--------
試行回数 : 10
最小値 : 7369
最大値 : 892213
平均値 : 400363.3
試行回数を増やしてみる(10→1000)
※実行結果のprintを一時的にコメントアウト
for turn in range(turns):
i = 1
while True:
passwd = ''.join(random.choices(char_list, k=passwd_len))
if passwd[0] == '0' and len(re.findall('[0-9]', passwd)) == passwd_len:
#print(passwd, ': ', i, '回目')
challenge_count.append(i)
break
i += 1
平均値が理論上の値に近づいたことと、最小値・最大値のブレが大きくなったことが確認できます。
最小値4ってすごいな。
文字列の長さ:6
エラー率:0.00459394%
21767.8回に1回はエラーになる計算
--------
--------
試行回数 : 1000
最小値 : 4
最大値 : 161107
平均値 : 21707.447
所感
- パスワードの長さや試行回数を増やすと、処理時間がだいぶ長くなります。処理時間の視える化や、処理を軽くするための修正が必要。
- 宝くじやガチャのシミュレータなんかがよく公開されていますが、今回のプログラムを応用して作れそうです。
- 結果をグラフで表示して分析とかできればいいんですが、Pythonの知識以上に数学・統計の知識が求められそうです。
- 0.0046%の確率でも、何度も繰り返せばエラーは必ず起きます。そうならないように、mkpasswdのオプションを指定しておきましょう。
mkpasswd -l 6 -d 2 -C 0 -s 0