安全な乱数

通常の乱数は簡単な規則で作られているので、パスワードの生成には使えない。暗号学的に安全な乱数(cryptographically secure random numbers)を使うべきである。Python の標準ライブラリには secrets というモジュールがこのために存在する(PEP 506 参照)。ただし、完全な物理乱数ではないので、これを使って暗号化のためのワンタイムパッドを作ることは想定されていない。

ランダムなパスワードを生成したいなら、例えば次のようにすれば、9バイトのランダムなビット列を Base64 にしたもの(12文字)を出力してくれる:

import secrets

secrets.token_urlsafe(9)
'c31jnRMYaQ3j'

なお、RFC 4648 のBase64は英大小数字と + / だが、こちらはURLセーフにするため英大小数字と - _ になっている。例えば次のようにして確認できる。

set(list(secrets.token_urlsafe(10000)))

ちなみに、URLセーフとは、URI(RFC 3986)の unreserved characters(ALPHA / DIGIT / "-" / "." / "_" / "~")に相当すると思われるが、この中でも ~ はやや特別な意味を持ちうるし、. がURLの最後に来るとURLの一部と解釈されないことがあるので、- _ が妥当なのであろう。RFC 4648のBase64を使ったパスワード生成コマンドに openssl rand -base64 n がある。これは n が3の倍数でない場合は = によるパディングが最後に追加される(Generate a random password from the command lineHow to generate a random string?)。

より一般的に、任意の文字集合のパスワードを生成するには、次のようにする:

n = 12  # 長さ
a = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"  # 文字集合
print(''.join([secrets.choice(a) for _ in range(n)]))

サイトによっては、秘密のことばを全角で入れろなどと言ってくるところがある:

n = 12  # 長さ
a = "あいうえおかきくけこさしすせそたちつてとなにぬねの" \
    "はひふへほまみむめもやゆよらりるれろわん"
print(''.join([secrets.choice(a) for _ in range(n)]))

よく使うなら次のような mkpasswd.py を作ってパスの通ったところに置いておく:

#! /usr/bin/env python3

import sys, secrets

n = int(sys.argv[1]) if sys.argv[1:] else 12
a = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
print(''.join([secrets.choice(a) for _ in range(n)]))

別の例として、長さ10(5バイト)の相異なる16進文字列を100個生成する:

import secrets

s = set()
while len(s) < 100:
    s.add(secrets.token_hex(5))
for x in s:
    print(x)

このような乱数を使った簡易電子投票のやりかたを上原哲太郎先生がGoogleフォームを使った簡易電子投票システムとして紹介されている。


Last modified: