[Ruby] パスワードジェネレータを実装する

2024/04/06に公開

Rubyでパスワードジェネレータを実装します。

パスワードの仕様を決める

生成するパスワードの仕様は、以下とします:

  • 英字(大文字・小文字)、数字、記号の混在とする

    • 英字は、数字との混同を防ぐため、以下の5文字を除外した47文字を使う:

      • 大文字:I(アイ)とO(オー)とZ(ゼット)

      • 小文字:b(ビー)l(エル)

    • 数字は、次の10文字を使う:1234567890

    • 記号は、次の11文字を使う:-_!#$%&()~/

  • 文字の重複を許す

また、当然ですが、パスワードジェネレータは、毎回異なるパスワードを生成するものとします。

パスワードの文字種数とパスワードの強度の関係

パスワードの強度を「総組み合わせの数」と定義した場合、その違いは文字種数に対してパスワード文字数のべき乗で効いてくるので、文字種数を削ってよいかどうかは慎重に検討しましょう。

例えば、文字種数を英字小文字26文字にした場合と、blを抜いた24文字の場合を考えてみましょう。
これらの文字種数の違いは、26/24 = 1.08\dot{3}ということで、約8.3%程度です。
小さい違いに見えますが、パスワード文字数が8の場合、強度に約2倍の違いが出てきます:

  • 文字種数が26:26 ^ 8 = 208827064576
  • 文字種数が24:24 ^ 8 = 110075314176

一般に、強度(ここでは総組み合わせの数)の違いは、以下の式で求めることができます:

\bigg( \dfrac{\text{文字種数1}}{\text{文字種数2}} \bigg) ^ \text{パスワード文字数}

実装する

PasswordGeneratorというモジュールを定義し、そこに、呼び出すたびにランダムにパスワードを生成するクラスメソッドPasswordGenerator.generateを定義します。このメソッドは、名前付き引数lengthで生成するパスワードの文字数を指定するようにします。

コード

password_generator.rb
# 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_CHARSALPHABET_LOWERCASE_CHARSNUMBER_CHARSSYMBOL_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"
使い方(丁寧版)
  1. まず、「sample.rb」を以下のように作成します:

    vim sample.rb
    
    sample.rb
    require_relative "lib/password_generator"
    
    puts PasswordGenerator.generate(length: 16)
    puts PasswordGenerator.generate(length: 16)
    
  2. コード節で示したモジュールPasswordGeneratorを「lib/password_generator.rb」に定義します。(lib/ディレクトリの配下に配置していることに注意!)

  3. 以下のようなファイル構成になります:

    ファイル構成
    $ tree -F
    ./
    ├── lib/
    │   └── password_generator.rb
    └── sample.rb
    
    1 directory, 2 files
    
  4. 実行します:

    ruby sample.rb
    
    実行例
    $ ruby sample.rb
    d0z1g4)RvJk&ux2m
    5-Xr4ttwBw&C(E4G
    
脚注
  1. インスタンスメソッドArray#join(Arrayクラス < Ruby 3.3 リファレンスマニュアル):
    https://docs.ruby-lang.org/ja/latest/method/Array/i/join.html ↩︎

  2. インスタンスメソッドArray#sample(Arrayクラス < Ruby 3.3 リファレンスマニュアル):
    https://docs.ruby-lang.org/ja/latest/method/Array/i/sample.html ↩︎

  3. %記法による配列の定義:

    ↩︎
  4. %記法の囲みに使う文字について:

    %記法(リテラル < Ruby 3.3 リファレンスマニュアル):
    https://docs.ruby-lang.org/ja/latest/doc/spec=2fliteral.html#percent

    ...
    %w!STRING! : 要素が文字列の配列(空白区切り)
    ...
    !の部分には改行を含めた任意の非英数字を使うことができます (%w、%W、%i、%I は区切りに空白、改行を用いるため、!の部分には使うことができません)。始まりの区切り文字が括弧((',[',{',<')である時には、終りの区切り文字は対応する括弧になります。

    ↩︎
  5. インスタンスメソッドObject#freeze(Objectクラス < Ruby 3.3 リファレンスマニュアル):
    https://docs.ruby-lang.org/ja/latest/method/Object/i/freeze.html ↩︎

  6. インスタンスメソッドModule#private_constant(Moduleクラス < Ruby 3.3 リファレンスマニュアル)
    https://docs.ruby-lang.org/ja/latest/method/Module/i/private_constant.html ↩︎

Discussion