VBAで要件(ポリシー)に沿ったランダムパスワードを生成

VBAでのランダムパスワード生成なんて出尽くされていると思ったけれど、意外とスマートな実装がなかったので自分で書いてみた。

単純に使用可能な文字種全体からランダムに1文字ずつ選択して連結するだけでは、「英小文字、英大文字、数字を必ず1文字以上含む」などのポリシーに沿っていない文字列が生成されてしまう可能性があるが、 1回の処理で必ずポリシーに沿った文字列を生成する関数を作成した。

実装

とりあえず作ったものはこちら。

' パスワード用のランダム文字列を生成する
Public Function GeneratePassword(iLength As Integer, Optional bAllowLower As Boolean = True, Optional bAllowUpper As Boolean = True, _
  Optional bAllowNumber As Boolean = True, Optional bAllowSymbol As Boolean = False, Optional bRequireLower As Boolean = True, _
  Optional bRequireUpper As Boolean = True, Optional bRequireNumber As Boolean = True, Optional bRequireSymbol As Boolean = False, _
  Optional sSymbols As String = "!#$%&()=^-|@[]{}:;,.") As String
    Dim sPassword As String
    Dim i As Long
    Dim sTotalChars As String
   
    Const S_LOWER As String = "abcdefghijklmnopqrstuvwxyz"
    Const S_UPPER As String = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    Const S_NUMBER As String = "0123456789"
   
    ' 使用可能文字種
    If bAllowLower Then
        sTotalChars = sTotalChars & S_LOWER
    End If
    If bAllowUpper Then
        sTotalChars = sTotalChars & S_UPPER
    End If
    If bAllowNumber Then
        sTotalChars = sTotalChars & S_NUMBER
    End If
    If bAllowSymbol Then
        sTotalChars = sTotalChars & sSymbols
    End If
   
    ' 任意の文字種が可能な部分を生成
    For i = 1 To iLength - Abs(bRequireLower + bRequireUpper + bRequireNumber + bRequireSymbol)
        sPassword = sPassword & Mid(sTotalChars, Int(Len(sTotalChars) * Rnd + 1), 1)
    Next i
   
    ' 必須とされる文字種の文字を1文字ずつ挿入
    If bRequireLower Then
        i = Int((Len(sPassword) + 1) * Rnd + 1)
        sPassword = Mid$(sPassword, 1, i - 1) & Mid(S_LOWER, Int(Len(S_LOWER) * Rnd + 1), 1) & Mid$(sPassword, i)
    End If
    If bRequireUpper Then
        i = Int((Len(sPassword) + 1) * Rnd + 1)
        sPassword = Mid$(sPassword, 1, i - 1) & Mid(S_UPPER, Int(Len(S_UPPER) * Rnd + 1), 1) & Mid$(sPassword, i)
    End If
    If bRequireNumber Then
        i = Int((Len(sPassword) + 1) * Rnd + 1)
        sPassword = Mid$(sPassword, 1, i - 1) & Mid(S_NUMBER, Int(Len(S_NUMBER) * Rnd + 1), 1) & Mid$(sPassword, i)
    End If
    If bRequireSymbol Then
        i = Int((Len(sPassword) + 1) * Rnd + 1)
        sPassword = Mid$(sPassword, 1, i - 1) & Mid(sSymbols, Int(Len(sSymbols) * Rnd + 1), 1) & Mid$(sPassword, i)
    End If
   
    GeneratePassword = sPassword

End Function

説明

各引数の説明は以下。

  • iLength: 生成するパスワードの長さ
  • bAllowLower: 省略可。英小文字を使用するか。デフォルトTrue
  • bAllowUpper: 省略可。英大文字を使用するか。デフォルトTrue
  • bAllowNumber: 省略可。数字を使用するか。デフォルトTrue
  • bAllowSymbol: 省略可。記号を使用するか。デフォルトFalse
  • bRequireLower: 省略可。英小文字を1文字以上使用することを要求するか。デフォルトTrue
  • bRequireUpper: 省略可。英大文字を1文字以上使用することを要求するか。デフォルトTrue
  • bRequireNumber: 省略可。数字を1文字以上使用することを要求するか。デフォルトTrue
  • bRequireSymbol: 省略可。記号を1文字以上使用することを要求するか。デフォルトFalse
  • sSymbols: 省略可。記号として使用する文字。デフォルト!#$%&()=^-|@[]{}:;,.

「英小文字、英大文字、数字をそれぞれ1文字以上含む」といったポリシーを満たすパスワード生成する単純なやり方としては、 全文字種から1文字ずつランダムに選択して結合し、その結果が各文字種が含まれているかをチェックして合格するまでやり直す、 という方法は考えられるが、あまりスマートとは言えない。

そこで必ず1度(正確には毎回同じ計算量)で要件に沿ったパスワードが生成できるよう、以下の手順で生成している。

例として「英小文字、英大文字、数字を必ず1文字以上含む、12文字のパスワード」を生成する場合、

  1. 12文字中の9文字については英小文字、英大文字、数字はそれぞれ0文字以上でよいので、単純に[a-zA-Z0-9]から1文字ずつランダムに取り出す方法が使って長さ9の文字列を生成。
  2. 必ず1文字以上含む文字種については、対象の文字種から1文字をランダムで選んで、それまでに生成した文字列のどこかに挿入する。この例では以下の処理を行う。
    1. 英小文字([a-z])から1つ選び、長さ9の文字列の先頭、末尾、および各文字間にある合計10個の挿入可能位置から1つを選んで挿入。
    2. 英大文字([A-Z])から1つ選び、長さ10の文字列の先頭、末尾、および各文字間にある合計11個の挿入可能位置から1つを選んで挿入。
    3. 数字([0-9])から1つ選び、長さ11の文字列の先頭、末尾、および各文字間にある合計12個の挿入可能位置から1つを選んで挿入。

という順序で生成すれば、必ず要件に沿ったパスワードを1回の処理で生成できる。

実行例

GeneratePassword(10): 英小文字、英大文字、数字を1文字以上含む、10文字。

B9ZJQVrseM
wYkkAE9cU0
feWc79QQ1S
jq1bu3ocVR
9MeCMCg6ls
7TAFCkC1zR
ZudA9RE6ZN
xv7xsXn3V3
vHZjXA9HeA
hbxE3k0ZOM

GeneratePassword(10, True, True, True, True, True, True, True, True): 英小文字、英大文字、数字に加えて記号を1文字以上含む、10文字。

b:t8r@KG^n
&Wp}vjT1bA
q(5u,o1[Kw
7W,^y2MUtb
eXO-NKb4)$
[3rW48O3.=
i73zhvZS)F
=#%8FpVEIQ
nS0troQ:G(
31.w.FDLM6

なお、引数のTrueFalseの組み合わせチェックはしていないので悪しからず。

例えばbAllowSymbolFalsebRequireSymbolTrueを指定した場合、必ず記号が1文字だけ含まれた文字列が生成される。