python

Pythonでパスワードを生成する関数を作成する

inty98-admin

はじめに

Pythonを使ってパスワードを生成する方法を詳しく解説していきます。

本ページに記載のコードは常識の範囲内であれば、商用利用問わず利用(改変)していただいて問題ありません。
NG(常識の範囲外)の例として、本ページのコードをそのまま or 概ね同じ状態でパッケージ(ライブラリ)として公開するなどはご遠慮願います。(会社内、組織内の閉じられた環境であれば問題ありません)

環境

  • Python: 3.13

実装

作成したコードは下記になります。
生成するパスワードに小文字、大文字、数字、記号を含めるかどうかを引数で設定できるようにしています。

  • 生成対象に含めない場合
    • NOT_INCLUDE
  • 含める場合
    • INCLUDE
  • 必ず1文字以上を必要とする
    • REQUIRED

パスワード生成関数(password.py)

大文字・小文字、数字などの文字列はstringモジュールの定数から取得します。
パスワードの生成にはsecretsモジュールは暗号学的に強い乱数を生成することができるとのことですので、randomモジュールではなく、こちらを使用します。
処理の流れとしては、REQUIREDに指定されている種類の文字を先に生成し、その後指定した文字数になるまで、INCLUDE or REQUIREDに設定されている文字を生成します。
REQUIREDをまとめて先に生成しているため、先頭に固まっていますので、最後にシャッフルします。

import string
from enum import Enum, auto
import secrets
import random


class Policy(Enum):
    REQUIRED = auto()
    INCLUDE = auto()
    NOT_INCLUDE = auto()


def create_password(
    len: int,
    lowercase: Policy = Policy.REQUIRED,
    uppercase: Policy = Policy.REQUIRED,
    digits: Policy = Policy.REQUIRED,
    symbol: Policy = Policy.INCLUDE,
) -> str:
    chars: list[str] = []
    password: list[str] = []
    created_len: int = 0

    for policy, target_string in [
        (lowercase, string.ascii_lowercase),
        (uppercase, string.ascii_uppercase),
        (digits, string.digits),
        (symbol, string.punctuation),
    ]:
        if policy != Policy.NOT_INCLUDE:
            target_list: list[str] = list(target_string)
            chars.extend(target_list)

            if policy == Policy.REQUIRED:
                password.append(secrets.choice(target_list))
                created_len += 1

    # 必須としている種類が生成する桁数より大きい場合はエラー
    if len < created_len:
        raise ValueError("桁数が不足しています")

    for _ in range(len - created_len):
        password.append(secrets.choice(chars))

    # 必須を先に生成しているため、シャッフルする
    random.shuffle(password)

    return "".join(password)


# 確認用のコード ※削除して問題ありません
if __name__ == "__main__":
    # 10桁 (小文字・大文字・数字は各種1つ以上を含める必要あり)
    print(create_password(10, symbol=Policy.REQUIRED))

    # 16桁 (小文字・大文字・数字は各種1つ以上を含める必要あり)
    print(create_password(16))

ユニットテスト(test_password.py)

作成したパスワード生成関数を検証するためにユニットテストを実施します。
パスワードの文字数が指定した通りになっているか、使用する文字種が想定範囲をカバーされているかをチェックします。

※ 下記のファイルはpassword.pyと同じ階層に置いてください。

from password import create_password, Policy
import string
import unittest

TEST_INCLUDE_PASSWORD_LEN: int = 32
TEST_REQUIRED_PASSWORD_LEN: int = 2


class TestPassword(unittest.TestCase):

    def test_create_password_include_lowercase(self):
        """小文字 (含めない)."""
        for _ in range(100):
            password: str = create_password(
                TEST_INCLUDE_PASSWORD_LEN,
                lowercase=Policy.NOT_INCLUDE,
                uppercase=Policy.INCLUDE,
                digits=Policy.INCLUDE,
                symbol=Policy.INCLUDE,
            )

            assert not self._check_include(
                password=password, check_chars=string.ascii_lowercase
            )
            assert len(password) == TEST_INCLUDE_PASSWORD_LEN

    def test_create_password_required_lowercase(self):
        """小文字 (必須)."""
        password: str = create_password(
            TEST_REQUIRED_PASSWORD_LEN,
            lowercase=Policy.REQUIRED,
            uppercase=Policy.NOT_INCLUDE,
            digits=Policy.NOT_INCLUDE,
            symbol=Policy.NOT_INCLUDE,
        )

        assert self._check_include(
            password=password, check_chars=string.ascii_lowercase
        )
        assert len(password) == TEST_REQUIRED_PASSWORD_LEN

    def test_create_password_include_uppercase(self):
        """大文字 (含めない)."""
        for _ in range(100):
            password: str = create_password(
                TEST_INCLUDE_PASSWORD_LEN,
                lowercase=Policy.INCLUDE,
                uppercase=Policy.NOT_INCLUDE,
                digits=Policy.INCLUDE,
                symbol=Policy.INCLUDE,
            )

            assert not self._check_include(
                password=password, check_chars=string.ascii_uppercase
            )
            assert len(password) == TEST_INCLUDE_PASSWORD_LEN

    def test_create_password_required_uppercase(self):
        """大文字 (必須)."""
        password: str = create_password(
            TEST_REQUIRED_PASSWORD_LEN,
            lowercase=Policy.NOT_INCLUDE,
            uppercase=Policy.REQUIRED,
            digits=Policy.NOT_INCLUDE,
            symbol=Policy.NOT_INCLUDE,
        )

        assert self._check_include(
            password=password, check_chars=string.ascii_uppercase
        )
        assert len(password) == TEST_REQUIRED_PASSWORD_LEN

    def test_create_password_include_digits(self):
        """数字 (含めない)."""
        for _ in range(100):
            password: str = create_password(
                TEST_INCLUDE_PASSWORD_LEN,
                lowercase=Policy.INCLUDE,
                uppercase=Policy.INCLUDE,
                digits=Policy.NOT_INCLUDE,
                symbol=Policy.INCLUDE,
            )

            assert not self._check_include(password=password, check_chars=string.digits)
            assert len(password) == TEST_INCLUDE_PASSWORD_LEN

    def test_create_password_required_digits(self):
        """数字 (必須)."""
        password: str = create_password(
            TEST_REQUIRED_PASSWORD_LEN,
            lowercase=Policy.NOT_INCLUDE,
            uppercase=Policy.NOT_INCLUDE,
            digits=Policy.REQUIRED,
            symbol=Policy.NOT_INCLUDE,
        )

        assert self._check_include(password=password, check_chars=string.digits)
        assert len(password) == TEST_REQUIRED_PASSWORD_LEN

    def test_create_password_include_symbol(self):
        """記号 (含めない)."""
        for _ in range(100):
            password: str = create_password(
                TEST_INCLUDE_PASSWORD_LEN,
                lowercase=Policy.INCLUDE,
                uppercase=Policy.INCLUDE,
                digits=Policy.INCLUDE,
                symbol=Policy.NOT_INCLUDE,
            )

            assert not self._check_include(
                password=password, check_chars=string.punctuation
            )
            assert len(password) == TEST_INCLUDE_PASSWORD_LEN

    def test_create_password_required_symbol(self):
        """記号 (必須)."""
        password: str = create_password(
            TEST_REQUIRED_PASSWORD_LEN,
            lowercase=Policy.NOT_INCLUDE,
            uppercase=Policy.NOT_INCLUDE,
            digits=Policy.NOT_INCLUDE,
            symbol=Policy.REQUIRED,
        )

        assert self._check_include(password=password, check_chars=string.punctuation)
        assert len(password) == TEST_REQUIRED_PASSWORD_LEN

    def test_create_password_len_error(self):
        pass

    @staticmethod
    def _check_include(password: str, check_chars: str) -> bool:
        return any([check_char in password for check_char in list(check_chars)])


if __name__ == "__main__":
    unittest.main()
ABOUT ME
金ノコ
金ノコ
プロフィールの画像は現在検討中です。 金色のキノコというと某ゲームのきのこが頭に浮かんでしまうので、どうしようか..と思っている次第です。
記事URLをコピーしました