[Ruby] パスワードジェネレータを実装する
Rubyでパスワードジェネレータを実装します。
パスワードの仕様を決める
生成するパスワードの仕様は、以下とします:
-
英字(大文字・小文字)、数字、記号の混在とする
-
英字は、数字との混同を防ぐため、以下の5文字を除外した47文字を使う:
-
大文字:
I
(アイ)とO
(オー)とZ
(ゼット) -
小文字:
b
(ビー)l
(エル)
-
-
数字は、次の10文字を使う:
1
、2
、3
、4
、5
、6
、7
、8
、9
、0
-
記号は、次の11文字を使う:
-
、_
、!
、#
、$
、%
、&
、(
、)
、~
、/
-
-
文字の重複を許す
また、当然ですが、パスワードジェネレータは、毎回異なるパスワードを生成するものとします。
パスワードの文字種数とパスワードの強度の関係
パスワードの強度を「総組み合わせの数」と定義した場合、その違いは文字種数に対してパスワード文字数のべき乗で効いてくるので、文字種数を削ってよいかどうかは慎重に検討しましょう。
例えば、文字種数を英字小文字26文字にした場合と、b
とl
を抜いた24文字の場合を考えてみましょう。
これらの文字種数の違いは、
小さい違いに見えますが、パスワード文字数が8の場合、強度に約2倍の違いが出てきます:
- 文字種数が26:
26 ^ 8 = 208827064576 - 文字種数が24:
24 ^ 8 = 110075314176
一般に、強度(ここでは総組み合わせの数)の違いは、以下の式で求めることができます:
実装する
PasswordGenerator
というモジュールを定義し、そこに、呼び出すたびにランダムにパスワードを生成するクラスメソッドPasswordGenerator.generate
を定義します。このメソッドは、名前付き引数length
で生成するパスワードの文字数を指定するようにします。
コード
# Generates a random password.
#
# @example
# PasswordGenerator.generate(length: 16) #=> "rjyu4&8&M#T~AV0k"
# PasswordGenerator.generate(length: 16) #=> "gaC6t_5JWCj80_QN"
#
# The generated password;
# * Uses alphabets (uppercase/lowercase), numbers, and some symbols:
# * Uppercase alphabets: without number-like characters 'I', 'O', and 'Z'
# * Lowercase alphabets: without number-like 'b' and 'l'
# * Numbers: 1 2 3 4 5 6 7 8 9 0
# * Symbols: - _ ! # $ % & ( ) ~ /
# * Allows character duplications
module PasswordGenerator
ALPHABET_UPPERCASE_CHARS = %w(
A B C D E F G H J K L M N P Q R S T U V W X Y
).map(&:freeze).freeze # without 'I', 'O', and 'Z'
ALPHABET_LOWERCASE_CHARS = %w(
a c d e f g h i j k m n 0 p q r s t u v w x y z
).map(&:freeze).freeze # without 'b' and 'l'
NUMBER_CHARS = %w( 1 2 3 4 5 6 7 8 9 0 ).map(&:freeze).freeze
SYMBOL_CHARS = %w{ - _ ! # $ % & ( ) ~ / }.map(&:freeze).freeze
private_constant :ALPHABET_UPPERCASE_CHARS, :ALPHABET_LOWERCASE_CHARS,
:NUMBER_CHARS, :SYMBOL_CHARS
class << self
# Generates a password String.
#
# @param length [Integer] Specify a positive Integer.
# @return [String] A password String.
# @raise [ArgumentError] If the argument length is not a positive Integer.
def generate(length:)
if length.class != Integer || length <= 0
raise ArgumentError.new("Specify a positive Integer for length; got #{length}")
end
Array.new(length) { |_| password_chars.sample }.join
end
private
# Returns an Array of characters used for passwords.
def password_chars
@@password_chars ||=
ALPHABET_UPPERCASE_CHARS + ALPHABET_LOWERCASE_CHARS \
+ NUMBER_CHARS + SYMBOL_CHARS
end
# Returns the number of characters used for passwords.
def password_chars_count
@@password_chars_count ||= password_chars.count
end
end
end
短いコードですが、一応説明します:
PasswordGenerator.generate(length:)
クラスメソッド名前付き引数length
で指定した長さで指定した文字数のパスワード文字列を、呼び出すたびにランダムに生成して返します。
引数length
としては「1以上の正の整数」を想定していて、それ以外の値が指定された場合はArgumentError
を発生させます。
パスワード生成は、以下の部分です:
Array.new(length) { |_| password_chars.sample }.join
例えば["4", "&", "a"]
というような、パスワード文字列の元となる「ランダムな文字の配列」を作ったあと、それをArray#join
メソッド[1]で結合してパスワード文字列としています。
ここで、「ランダムな文字の配列」は、Array.new
メソッドで以下のようにして作成しています:
-
Array.new
メソッドの引数にパスワード文字長length
を指定し、パスワード文字長と同じ要素数の配列にする -
Array.new
メソッドにブロックをとり、配列の各要素を「ランダムな1文字」とする。
ここでは、パスワード文字候補の配列PasswordGenerator.password_chars
から、Array#sample
メソッド[2]でランダムに1文字ピックアップしている
(ブロック引数は使わないので、_
という変数名にしている)
PasswordGenerator.password_chars
プライベートクラスメソッドパスワードに使う文字を、1文字1要素として、1つの配列にして返します。
何度も計算をするのは無駄なので、メモ化しています。
PasswordGenerator.password_chars_count
プライベートクラスメソッドパスワードに使う文字種の数を返します。
こちらも何度も計算をするのは無駄なので、メモ化しています。
モジュール定数
モジュール定数ALPHABET_UPPERCASE_CHARS
、ALPHABET_LOWERCASE_CHARS
、NUMBER_CHARS
、SYMBOL_CHARS
の定義で使っている%w( foo bar )
は、RubyのArrayを定義する %記法 です[3]。
%w
の後の、配列の要素を囲む記号(%w( foo bar )
の(
と)
)には、任意の非英数字を使うことができます[4]。SYMBOL_CHARS
の定義では、配列の要素に(
と)
の記号を使っているので、わかりやすさのために、配列の要素に使われていない記号{
と}
で囲んでいます:
SYMBOL_CHARS = %w{ - _ ! # $ % & ( ) ~ / }.map(&:freeze).freeze
また、これら定数の値の中身は変更されたくないので、Object#freeze
[5]メソッドを使って配列の各要素と配列自身を凍結し、Module#private_constant
[6]でモジュール外から参照できないようにしています。
使い方
コード節で示したモジュールPasswordGenerator
を「lib/password_generator.rb」に定義しているとします。
require_relative "lib/password_generator"
puts PasswordGenerator.generate(length: 16) #=> "d0z1g4)RvJk&ux2m"
puts PasswordGenerator.generate(length: 16) #=> "5-Xr4ttwBw&C(E4G"
使い方(丁寧版)
-
まず、「sample.rb」を以下のように作成します:
vim sample.rb
sample.rbrequire_relative "lib/password_generator" puts PasswordGenerator.generate(length: 16) puts PasswordGenerator.generate(length: 16)
-
コード節で示したモジュール
PasswordGenerator
を「lib/password_generator.rb」に定義します。(lib/ディレクトリの配下に配置していることに注意!) -
以下のようなファイル構成になります:
ファイル構成$ tree -F ./ ├── lib/ │ └── password_generator.rb └── sample.rb 1 directory, 2 files
-
実行します:
ruby sample.rb
実行例$ ruby sample.rb d0z1g4)RvJk&ux2m 5-Xr4ttwBw&C(E4G
-
インスタンスメソッド
Array#join
(Arrayクラス < Ruby 3.3 リファレンスマニュアル):
https://docs.ruby-lang.org/ja/latest/method/Array/i/join.html ↩︎ -
インスタンスメソッド
Array#sample
(Arrayクラス < Ruby 3.3 リファレンスマニュアル):
https://docs.ruby-lang.org/ja/latest/method/Array/i/sample.html ↩︎ -
%記法による配列の定義:
- 配列式(リテラル < Ruby 3.3 リファレンスマニュアル):
https://docs.ruby-lang.org/ja/latest/doc/spec=2fliteral.html#array - %記法(リテラル < Ruby 3.3 リファレンスマニュアル):
https://docs.ruby-lang.org/ja/latest/doc/spec=2fliteral.html#percent
- 配列式(リテラル < Ruby 3.3 リファレンスマニュアル):
-
%記法の囲みに使う文字について:
%記法(リテラル < Ruby 3.3 リファレンスマニュアル):
https://docs.ruby-lang.org/ja/latest/doc/spec=2fliteral.html#percent...
%w!STRING! : 要素が文字列の配列(空白区切り)
...
!の部分には改行を含めた任意の非英数字を使うことができます (%w、%W、%i、%I は区切りに空白、改行を用いるため、!の部分には使うことができません)。始まりの区切り文字が括弧((',
[',{',
<')である時には、終りの区切り文字は対応する括弧になります。 -
インスタンスメソッド
Object#freeze
(Objectクラス < Ruby 3.3 リファレンスマニュアル):
https://docs.ruby-lang.org/ja/latest/method/Object/i/freeze.html ↩︎ -
インスタンスメソッド
Module#private_constant
(Moduleクラス < Ruby 3.3 リファレンスマニュアル)
https://docs.ruby-lang.org/ja/latest/method/Module/i/private_constant.html ↩︎
Discussion