自分でパスワードを作ってみる

# はじめに
会員登録の際にパスワードを作成する機会があります。多くの場合、ブラウザーのパスワード自動生成機能を使って作成しますが、サイトによってはパスワードの文字数や必須の文字種、また使用可能な記号の種類が制限されている場合があり、自動生成されたパスワードがそのまま使えないこともあります。もちろん、その場合は生成後にパスワードを手動で編集すれば済む話ではありますが……。

また、一般的にパスワードは長いほど安全とされているため、文字数も自由に指定でき、安全なパスワードを生成できると便利だと思い、このようなプログラムを作ってみました。

ちなみに、アメリカ国立標準技術研究所（NIST）が発行した「Digital Identity Guidelines」（🔗[NIST Special Publication 800-63B](https://pages.nist.gov/800-63-3/sp800-63b.html)）では、ユーザーが選択するパスワードは少なくとも8文字以上であることが推奨されています。

> “Memorized secrets SHOULD be at least 8 characters in length if chosen by the subscriber.”
>（ユーザーが選択するパスワードは少なくとも8文字であるべきだが、長いほど安全）

# コード
```python:python
import secrets
import string
import random

def generate_password(length, 
                      use_upper=True, require_upper=False,
                      use_lower=True, require_lower=False,
                      use_digits=True, require_digits=False,
                      use_symbols=True, require_symbols=False,
                      custom_symbols=None):
    """
    使用可否と必須条件をサポートしたパスワード生成
    """
    chars = ""
    mandatory_chars = []

    if use_upper:
        chars += string.ascii_uppercase
        if require_upper:
            mandatory_chars.append(secrets.choice(string.ascii_uppercase))

    if use_lower:
        chars += string.ascii_lowercase
        if require_lower:
            mandatory_chars.append(secrets.choice(string.ascii_lowercase))

    if use_digits:
        chars += string.digits
        if require_digits:
            mandatory_chars.append(secrets.choice(string.digits))

    if use_symbols:
        symbols = custom_symbols if custom_symbols else string.punctuation
        chars += symbols
        if require_symbols:
            mandatory_chars.append(secrets.choice(symbols))

    if not chars:
        raise ValueError("少なくとも1種類の文字セットを指定してください。")

    # 残りの長さ分ランダム選択
    remaining_length = length - len(mandatory_chars)
    if remaining_length < 0:
        raise ValueError("長さが必須条件の合計より小さいです。")

    password_chars = mandatory_chars + [secrets.choice(chars) for _ in range(remaining_length)]
    random.shuffle(password_chars)
    password = ''.join(password_chars)
    return password

# ==== 入力部分 ====
length_input = input("パスワードの長さは？（デフォルト: 12）: ")
length = int(length_input) if length_input.strip() else 12

# 英大文字
upper_input = input("英大文字を含めますか？（デフォルト: True, Y/N）: ")
use_upper = upper_input.strip().lower() != 'n' if upper_input.strip() else True
require_upper = False
if use_upper:
    require_upper_input = input("英大文字を必須にしますか？（デフォルト: False, Y/N）: ")
    require_upper = require_upper_input.strip().lower() == 'y'

# 英小文字
lower_input = input("英小文字を含めますか？（デフォルト: True, Y/N）: ")
use_lower = lower_input.strip().lower() != 'n' if lower_input.strip() else True
require_lower = False
if use_lower:
    require_lower_input = input("英小文字を必須にしますか？（デフォルト: False, Y/N）: ")
    require_lower = require_lower_input.strip().lower() == 'y'

# 数字
digits_input = input("数字を含めますか？（デフォルト: True, Y/N）: ")
use_digits = digits_input.strip().lower() != 'n' if digits_input.strip() else True
require_digits = False
if use_digits:
    require_digits_input = input("数字を必須にしますか？（デフォルト: False, Y/N）: ")
    require_digits = require_digits_input.strip().lower() == 'y'

# 記号
symbols_input = input("記号を含めますか？（デフォルト: True, Y/N）: ")
use_symbols = symbols_input.strip().lower() != 'n' if symbols_input.strip() else True
require_symbols = False
if use_symbols:
    require_symbols_input = input("記号を必須にしますか？（デフォルト: False, Y/N）: ")
    require_symbols = require_symbols_input.strip().lower() == 'y'

custom_symbols_input = input("使用する記号セットを入力してください（空欄ならすべての記号を使います）: ")
custom_symbols = custom_symbols_input.strip() if custom_symbols_input.strip() else None

# ==== パスワード生成 ====
password = generate_password(length, use_upper, require_upper,
                             use_lower, require_lower,
                             use_digits, require_digits,
                             use_symbols, require_symbols,
                             custom_symbols)
print("生成されたパスワード:", password)
```

# コードの説明

1️⃣ 基本の流れ
	1.	質問に答える形でユーザーがパスワードの仕様（長さ、文字種の使用可否・必須条件）を決めます
	2.	指定された文字セットからランダムに文字を選んでパスワードを生成します
	3.	必須条件を満たす文字を必ず含むようにします（必須分は先に確保）
	4.	必須文字を含めた後、残りの長さ分ランダムに文字を補充します
	5.	最後にシャッフルして必須文字が先頭に固まらないようにします

2️⃣ 各部分のポイント

🔸 関数部分: generate_password()
```python:python
def generate_password(length, 
                      use_upper=True, require_upper=False,
                      use_lower=True, require_lower=False,
                      use_digits=True, require_digits=False,
                      use_symbols=True, require_symbols=False,
                      custom_symbols=None):
```
パスワードの文字数、文字種等をセットして実行します。
•	use_upper: 英大文字を使うかどうか
•	require_upper: 英大文字を必須にするかどうか
（以下同様に英小文字、数字、記号）

🔸 使用可能な文字セットを集める
```python:python
chars = ""
mandatory_chars = []
```
•	chars: ランダムに選ばれる文字のプール
•	mandatory_chars: 必須条件で絶対に含める文字（1文字ずつ）

例えば：
```python:python
if use_upper:
    chars += string.ascii_uppercase
    if require_upper:
        mandatory_chars.append(secrets.choice(string.ascii_uppercase))
```
✅ 使うだけなら chars に追加
✅ 必須なら mandatory_chars に1文字加える

`random.choce()`を使うと疑似乱数を作れますが、暗号学的安全性が高い`secrets.choice()`を使用します。

🔸 残りの長さ分をランダムに埋める
```python:python
remaining_length = length - len(mandatory_chars)
```
⚠️ もし必須条件の数が長さを超えていたらエラーを出す。

🔸 パスワード生成
```python:python
password_chars = mandatory_chars + [secrets.choice(chars) for _ in range(remaining_length)]
```
•	`mandatory_chars` は「必須文字種（必ず含めたい大文字や小文字、数字、記号など）」としてすでに選ばれている文字のリストです。
•	その後ろに、指定されたパスワードの長さ（`length`）から必須文字種の数（`len(mandatory_chars)`）を引いた数だけ（`remaining_length`）、`secrets.choice(chars)` でランダムに文字を選んでリストを作っています。
•	`chars` には英大文字、小文字、数字、記号などで使用すると指定したすべての文字が詰め込まれています。
•	必須文字が必ず含まれるので、ユーザーが「大文字必須」などの要件を満たしたパスワードが必ず生成されます。

```python:python
random.shuffle(password_chars)
```
•	上で作成した文字リスト（password_chars）の順番をシャッフルしています。
•	これにより、必須文字がパスワードの最初に固まってしまうことを防ぎ、より自然でランダムなパスワードに見えるようにしています。
```python:python
password = ''.join(password_chars)
```

•	password_chars のリストの中の1文字1文字を結合して、最終的にパスワードとして出力する文字列にしています。
•	たとえば、['A', 'b', '3', '!'] なら、'Ab3!' という文字列になります。
    

3️⃣ ユーザー入力部分
•	1行目でパスワード長を決定
•	2行目以降で「使用するかどうか」と「必須にするかどうか」を順に聞いている

例：
```python:python
custom_symbols_input = input("使用する記号セットを入力してください（空欄ならすべての記号を使います）: ")
custom_symbols = custom_symbols_input.strip() if custom_symbols_input.strip() else None
```
ここで：
✅ 空欄なら `string.punctuation`（デフォルト）を使う
✅ 何か入力すればそれだけを使う

⚠️`string.punctuation` は Python の組み込み string モジュール(`import string`が必要）で定義されている文字列で、一般的に使われるすべての記号（句読点や記号類） をまとめて含んだものです。具体的には、以下のような文字が含まれています：
```txt:
!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
```
実行例：
全てデフォルトの場合長さ12文字、全ての文字種が入ったパスワードが作成されます。
```bash:
パスワードの長さは？（デフォルト: 12）: 
英大文字を含めますか？（デフォルト: True, Y/N）: 
英大文字を必須にしますか？（デフォルト: False, Y/N）: 
英小文字を含めますか？（デフォルト: True, Y/N）: 
英小文字を必須にしますか？（デフォルト: False, Y/N）: 
数字を含めますか？（デフォルト: True, Y/N）: 
数字を必須にしますか？（デフォルト: False, Y/N）: 
記号を含めますか？（デフォルト: True, Y/N）: 
記号を必須にしますか？（デフォルト: False, Y/N）: 
使用する記号セットを入力してください（空欄ならすべての記号を使います）: 
生成されたパスワード: 2veBoKysTq\G
```
英数字だけで良い場合は記号を含めますか？で「N」を入れます。
```bash:
パスワードの長さは？（デフォルト: 12）: 
英大文字を含めますか？（デフォルト: True, Y/N）: 
英大文字を必須にしますか？（デフォルト: False, Y/N）: 
英小文字を含めますか？（デフォルト: True, Y/N）: 
英小文字を必須にしますか？（デフォルト: False, Y/N）: 
数字を含めますか？（デフォルト: True, Y/N）: 
数字を必須にしますか？（デフォルト: False, Y/N）: 
記号を含めますか？（デフォルト: True, Y/N）: N
使用する記号セットを入力してください（空欄ならすべての記号を使います）: 
生成されたパスワード: Tq0FE2O1MJOD
```