TrueRNG v3を使って[0,1)の一様乱数を生成する

2021-08-20

TrueRNG v3を使って0から1の一様乱数を生成します。

TrueRNG v3

TrueRNG v3

TrueRNGはハードウェア乱数生成器です。

半導体接合のアバランシェ効果を使用して、真の乱数を生成します。 生成速度が400 kbits/secondを超える低コストのUSBハードウェア乱数ジェネレーターです。

TrueRNG v3 | ubld.it

動作確認

公式ページのコード1とTrueRNG Utilities2を参考に動作を確認します。

外部モジュールとして pySerial を使います。

from serial import Serial
from serial.tools import list_ports

デバイスの接続確認

シリアルポート一覧からデバイスが認識しているかを確認します。

ports_avaiable = list_ports.comports()

戻り値はListPortInfoオブジェクトを含むリストです。

このオブジェクトはシリアルポートに関する情報を持っています。[*1]


デバイスの確認には.hwidを使います。TrueRNGはVID:PID=04D8:F5FEらしいです。

また、ポートの確認には.deviceを使います。(.portは存在しません。)

devices = [p.device for p in ports_avaiable if '04D8:F5FE' in p.hwid]
port = devices[0] if devices else None

複数のデバイスを検知した場合に備えdevice[0]でアクセスしています。

list_ports.comports()による返り値は特定の順番で返ってくる訳ではないので注意が必要です。


動作確認

serial.Serialを使ってTrueRNGとシリアル通信を行っていきます。

with Serial(port=port, timeout=10, dsrdtr=True) as ser:

    # Open the serial port if it isn't open
    if not ser.isOpen():
        ser.open()

    ser.reset_input_buffer()  # ノイズデータがある場合があるのでバッファをクリア

    try:
        before = time.perf_counter()
        x = ser.read(102400)  # <- (1) データの取得
        after = time.perf_counter()

    except:
        print('Read Failed!')

    else:  # then
        kbps = 8*len(x) / ((after - before) * 1000)
        print(f'{round(kbps, 2)} kbit/s')

(1) TrueRNGからread()を使って、値を取得します。bytesが返ってきます。

b'\xf2\xb62$\x94y .... \x88'

100回の試行結果をまとめました。

funckbit/s
mean403.106
max403.180
min403.032

生成速度が400kbit/sを超えると書いてあるので、読み取り自体は問題なさそうです。

乱数の生成

0から1の一様乱数を生成します。

TrueRNGから得られる値はbytesであり、これを変換していく必要があります。

今回はrandom.SystemRandomを参考にしていきます。


SystemRandom

random.SystemRandom.randomは0から1の一様乱数を返します。

print(SystemRandom().random())

>>> 0.6643365002539736

random.SystemRandomの実装を確認します。

# random.py

...

from os import urandom as _urandom

...

BPF = 53        # Number of bits in a float
RECIP_BPF = 2**-BPF

...

class SystemRandom(Random):
    def random(self):
        return (int.from_bytes(_urandom(7), 'big') >> 3) * RECIP_BPF  # <- (1)

(1) os.urandomを使用し、乱数の生成を行っていることがわかります。


実装

os.urandom(size)はsize バイトからなるランダムな文字列(bytes)を返します。

つまり、os.urandom(7)の代わりにser.read(7)することで、0から1の一様乱数を生成できます。

with Serial(port=self.port,timeout=1, dsrdtr=True) as ser:
    ...

    data = ser.read(7)

    r = (int.from_bytes(data, 'big') >> 3) * 2**-53

    print(r)


>>> 0.23952311969869522

解説

(int.from_bytes(_urandom(7), 'big') >> 3) * 2**-53

これを少し書き換えます。(見やすくするために無駄なスペースを使用しています。)

1  value_raw   = _urandom(7)
2  value_int   = int.from_bytes(value_raw, 'big')
3  value_shift = value_int >> 3
4  value_rand  = value_shift * 2**-53
  1. _urandom(7)を使って、7byte(=56bit)の値を回収します。

  2. int.from_bytesを使って、バイト列(value_raw)の整数表現を回収します。

  3. >>3を使って、3bit分右にシフトします。これで53bitになっています。

    (53bitなので 0~9007199254740991 の整数になるはずです。)

  4. 2**-53[*2]を掛ける(2**53で割る)ことで、乱数の範囲を[0,1)にします。

value_raw   ->  b'\xdaL@\xf5dM\xff'
value_int   ->  61445386801532415  -> 11011010010011000100000011110101011001000100110111111111
value_shift ->  7680673350191551   -> 11011010010011000100000011110101011001000100110111111
value_rand  ->  0.8527260398007498

doubleにおける仮数部の精度は53ビットであるため、53bit全てを乱数で埋め、シフト>>3と掛け算*2**-53をおこなっていると考えられます。[*2]


おまけ

ListPortInfoのインデックス

下位互換性のためにインデックスを使って、3個(port, desc, hwid)だけにアクセスできます。

# serial/tools/list_ports_common.py

def __getitem__(self, index):
    """Item access: backwards compatible -> (port, desc, hwid)"""
    if index == 0:
        return self.device
    elif index == 1:
        return self.description
    elif index == 2:
        return self.hwid
    else:
        raise IndexError('{} > 2'.format(index))

2**53

2**53     -> 100000000000000000000000000000000000000000000000000000
2**53 - 1 -> 11111111111111111111111111111111111111111111111111111

倍精度浮動小数点数 | wiki

15. 浮動小数点演算、その問題と制限 | Python ドキュメント


os.urandom

os.urandomはOSによって動作が異なります。

random.SystemRandom.randomには以下のような説明があります。

such as /dev/urandom on Unix or CryptGenRandom on Windows

cpython/Lib/random.py | GitHub


この動作については、stackoverflowの回答に分かりやすい説明がありました。

How exactly does random.random() work in python? | stackoverflow


os.urandomrandom.randomについては次の記事に書きます。(多分)


ホワイトニング

TrueRNG v3ではホワイトニングが改善されているそうです。

...解析の手がかりとなるパターンをそのデータから取り除いておきたい。パターンを取り除く処理はホワイトニングと呼ばれる。

C/C++セキュアプログラミングクックブック VOLUME 2


v2ではXORが使用されていたようです。

What’s the whitening algorithm? | ubld.it


v3で改善されたと書かれていますが、実際に何が使用されているかを発見できませんでした。

フォーラムにv3のホワイトニングについて質問がありましたが、回答はありませんでした。


Supported Modes

TrueRNG pro系はモード変更が可能ですが、TrueRNGは変更できないようです。

TrueRNGproの商品ページにサポートしているモードが記載されています。

TrueRNGpro – USB Random Number Generator | ubld.it


RAW Binaryモードを使用することでホワイトニングされていない値を取得できるようです。


参考文献

1

2016年にPython2系で書かれたコードです。TrueRNGpro用のコードです。 How-to use TrueRNGpro with Python in Windows 10 and Linux | ubld.it

2

Python3系で書かれたコードです。公式ページにGitHubへのリンクがありました。TrueRNG Utilities | GitHub