【Python】Pythonで セキュアなランダム文字列生成を考える

■ はじめに

システムのパスワードを決めるために、
より安全な文字列を生成する必要ができたので
Pythonで生成する方法を調べてみた。
ついでに、「トークン」や「UUID」も載せておく。

ちなみに、Java版で同じことを過去にやっていた。

Java】セキュアなランダム文字列生成を考える
https://dk521123.hatenablog.com/entry/2016/10/04/233416

目次

【1】ランダムパスワード
 1)生成方針
 2)サンプル
 3)使用上の注意
【2】トークン
 1)生成方針
 2)サンプル
【3】UUID
 1)生成方針
 2)サンプル

【1】ランダムパスワード

1)生成方針

[1] 生成する文字種(英数字、記号)を決める
[2] 決めた文字種をランダムで選ぶ
[3] [2] を文字数分繰り返す

[1] 生成する文字種(英字、数字、記号)を決める

import string

# abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
print(string.ascii_letters)

# 0123456789
print(string.digits)

# !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
print(string.punctuation)

[2] 決めた文字種をランダムで選ぶ

Pythonでのランダムでの選び方は、色々あるので
それらを使って組み込む

* random.choice
* secrets.choice <= セキュアならこっち

etc...

[3] [2] 文字数分繰り返す

# 以下「【2】サンプル」の↓部分
(secrets.choice(random_source) for i in range(length))

2)サンプル

import secrets
import string


def get_random_string(length):
  random_source = string.ascii_letters + string.digits + string.punctuation
  random_string = ''.join(
    (secrets.choice(random_source) for i in range(length)))
  return random_string


random_string_length = 12
print(f"=== 12345678901234567890")
print(f"[1] {get_random_string(random_string_length)}")
print(f"[2] {get_random_string(random_string_length)}")
print(f"[3] {get_random_string(random_string_length)}")
print(f"[4] {get_random_string(random_string_length)}")
print(f"[5] {get_random_string(random_string_length)}")

3)使用上の注意

* 上記のサンプルだと本当に決められたルールからランダムに生成される。
 => よって、例えば「R:a|bG$acb=Q」のように数字を含まないパターンも
  生成される場合もある
 => パスワード生成にルールがある(例えば、数字は最低1つは含む 等)場合
  そのルールに当てはまらないパスワードが生成されることもあるので注意
 => 運用上、複数回生成して、そこからルール通りのパスワードを
  ピックアップするのもいいが、できる限り頭を使いたくないので、
  パスワード生成ルール考慮したコード例を以下に記す

サンプル・改善版

import secrets
import string


RANDOM_STRING_LENGTH = 12
PASSWORD_NUM_TO_NEED = 10
MAX_EXEC_NUM = 100

# Passwordルールに対する定数(例)
MIN_PASSWORD_LENGTH = 8
MAX_PASSWORD_LENGTH = 32

def get_random_string(length: int) -> str:
  random_source = string.ascii_letters + string.digits + string.punctuation
  random_string = ''.join(
    (secrets.choice(random_source) for i in range(length)))
  return random_string


# ★追加:使用しているパスワードのルールに従い実装する(以下、実装例)
def is_valid_password(target_password: str) -> bool:
  # 1) 最低 8 文字から
  if len(target_password) < MIN_PASSWORD_LENGTH:
    return False

  # 2) 最高 32 文字まで
  if len(target_password) > MAX_PASSWORD_LENGTH:
    return False

  # 3) 最低数字が含まれている
  if not any(map(str.isdigit, target_password)):
    return False

  # 4) 最低大文字および小文字が含まれている
  if not (any(map(str.islower, target_password)) and
    any(map(str.isupper, target_password))):
    return False

  return True

password_num = 0
print(f"==== 12345678901234567890")
for i in range(MAX_EXEC_NUM):
  password = get_random_string(RANDOM_STRING_LENGTH)
  if is_valid_password(password):
    password_num = password_num + 1
    print(f"[{password_num:2}] {password}")
    if PASSWORD_NUM_TO_NEED <= password_num:
      break

print("Done")

【2】トーク

1)生成方針

https://docs.python.org/ja/3/library/secrets.html#generating-tokens

にあるsecretsモジュールの token_xxxx() を使うだけ(簡単!)

[1] secrets.token_bytes([nbytes=None])
 => nbytes バイトを含むバイト文字列を返す

[2] secrets.token_hex([nbytes=None])
 => 十六進数のランダムなテキスト文字列を返す

[3] secrets.token_urlsafe([nbytes=None])
 => nbytes のランダムなバイトを持つ URL 安全なテキスト文字列を返す

補足:トークンは何バイト使うべきか?
https://docs.python.org/ja/3/library/secrets.html#how-many-bytes-should-tokens-use

より抜粋
~~~~~~~~~~~~~~~~~
総当たり攻撃 に耐えるには、トークンは十分にランダムでなければなりません。
残念なことに、コンピュータの性能が向上し、より短時間により多くの推測が
できるようになるにつれ、十分とされるランダムさというのは必然的に増えます。
2015 年の時点で、secrets モジュールに想定される通常の用途では、
32 バイト (256 ビット) のランダムさは十分と考えられています。

注釈 デフォルトはメンテナンスリリースの間を含め、いつでも変更される可能性があります
~~~~~~~~~~~~~~~~~

2)サンプル

import secrets


target_byte = 32
print(f"===== 1234567890123456789012345678901234567890")
print(f"Token {secrets.token_bytes(target_byte)}")
print(f"Token {secrets.token_hex(target_byte)}")
print(f"Token {secrets.token_urlsafe(target_byte)}")

出力結果

===== 1234567890123456789012345678901234567890
Token b'\xbf\xb9\xb5:K\xf7(\xb4yg\xb1H\x93\x0b\x16\x10\xeb\x1e\x1b;\xeb*Z:\xf5\x1d\x8b"\xafL\xb1\xe4'
Token ea917e7d10d7f8edd195568a8c934a476ce5c4c2918492eced6c0582218a0b20
Token OdouCzAUDkjx9ORAN-Wsz1XaDaohn7TbvFZdE1QorOw

【3】UUID

1)生成方針

https://docs.python.org/ja/3/library/uuid.html

にあるuuidモジュールの uuid4() を使うだけ(簡単!)

2)サンプル

import uuid

uuid = uuid.uuid4()
print(f"UUID : {uuid}, is_safe = {uuid.is_safe}")

出力結果

UUID : 44062120-d060-4c21-9cb5-7d0a6819d543, is_safe = SafeUUID.unknown

参考文献

https://leben.mobi/blog/python_ramdom_password/python/

関連記事

Python ~ 基本編 / 文字列 ~
https://dk521123.hatenablog.com/entry/2019/10/12/075251
Python ~ 入門編 ~
https://dk521123.hatenablog.com/entry/2014/08/07/231242
Java】セキュアなランダム文字列生成を考える
https://dk521123.hatenablog.com/entry/2016/10/04/233416