🐰
Ruby 任意の文字種を含んだパスワードを自動生成する
以下の要件に沿って一時的なパスワードを自動生成する必要があり、SecureRandom module をそのままでは使えなさそうだったので実現方法を考えてみました。
- 最低 n 文字
- アルファベットの大文字・小文字、0~9 の数字、いくつかの記号をそれぞれ 1 文字ずつ含む
結論
- SecureRandom モジュールでは文字の種類を細かく指定してランダムで文字列を生成することはできない
- 最低 n 文字と文字種を満たすまで各文字種を適当に pick していく方法
- 条件の文字種を含んだ文字列を条件を満たすまで sample する方法
最初に考えた方法
最初に考えたのは、文字種の配列を作成し sample メソッドでピックアップしていく方法です。
こんなイメージで実装しようとしていました。
def generate_password
chars_lower = ('a'..'z').to_a
chars_upper = ('A'..'Z').to_a
chars_number = ('0'..'9').to_a
chars_special = ['~', '-', '|', '!', '?', '_']
chars = [*chars_lower, *chars_upper, *chars_number, *chars_special]
password_length = 8
while true do
tmp_password = ''
is_lower = false
is_upper = false
is_number = false
is_special = false
# パスワード生成
password_length.times do
tmp_password += chars.sample
end
# パスワードの要件を満たしているか確認
tmp_password.split("").each do |c|
is_lower = chars_lower.include?(c) ? true : is_lower
is_upper = chars_upper.include?(c) ? true : is_upper
is_number = chars_number.include?(c) ? true : is_number
is_special = chars_special.include?(c) ? true : is_special
end
if is_lower && is_upper && is_number && is_special
return tmp_password
end
end
end
ただ、sample メソッドで使用している乱数生成器がパスワードで利用するには安全でないとのことから、この方法を使用するのは見送り、SecureRandom モジュールを使用した方法を考えることにしました。
SecureRandom モジュールでは文字の種類を細かく指定してランダムで文字列を生成することはできない
SecureRandom モジュールでは、今回の要件を満たすような文字列を生成することはできません。なぜなら、文字種を指定してランダムに文字列を作成することができないためです。
文字列 + 数値の組み合わせならば alphanumeric
メソッドが使用できますが、記号も入れたいとなると一工夫必要そうです。
最低 n 文字と文字種を満たすまで各文字種を適当に pick していく方法
array.sample
を使用していた部分を SecureRandom の random_number
メソッドを利用してインデックスを抽出する方法を使います。
random_number
メソッドは引数を指定しなければ 0 以上 1 未満の数値を返しますが、引数 n を指定することで 0 以上 n 未満の数値を返してくれます。
def generate_password(password_length=8)
raise ArgumentError if password_length < 4
chars_lower = ('a'..'z').to_a
chars_upper = ('A'..'Z').to_a
chars_number = ('0'..'9').to_a
chars_special = ['~', '-', '|', '!', '?', '_']
chars = [*chars_lower, *chars_upper, *chars_number, *chars_special]
while true do
tmp_password = ''
is_lower = false
is_upper = false
is_number = false
is_special = false
# パスワード生成
password_length.times do
tmp_password += chars[SecureRandom.random_number(chars.size)]
end
# パスワードの要件を満たしているか確認
tmp_password.split("").each do |c|
is_lower = chars_lower.include?(c) ? true : is_lower
is_upper = chars_upper.include?(c) ? true : is_upper
is_number = chars_number.include?(c) ? true : is_number
is_special = chars_special.include?(c) ? true : is_special
end
if is_lower && is_upper && is_number && is_special
return tmp_password
end
end
end
先ほどあげたコードから array.sample
で文字を抽出していた部分を random_number
メソッドで数値を生成し、その数値を配列のインデックスとすることで文字を抽出しています。
さいごに
ご意見やもっといい方法をご存じの方はぜひ教えてください!
参考
Discussion