うたカモ技術ブログ

Linux その他

Linux   乱数ジェネレーター(/dev/random、/dev/urandom、getrandom)

post:     update: 

今回はLinuxの/devに存在する疑似デバイスファイルのrandomとurandomについて紹介します。

これらはその名の通り、乱数ジェネレーターを指す疑似デバイスファイルです。

この記事ではLinuxシステム上の乱数の生成方法として提供される/dev/randomと/dev/urandomの特徴と使い方を説明します。

加えて等価の処理として、Linux 3.17 (glibc 2.25.)で導入されたgetrandomシステムコールの使い方についてもサンプルプログラム を掲載して紹介します。

それでは行ってみます。

#実行デバイス
Raspberry Pi 3B(64bit)
#実行環境OS
Raspbian GNU/Linux 11 (bullseye)
(Kernel Ver: Linux raspberrypi 6.1.21-v7+)

目次

エントロピープールとは?

Linuxの/dev/randomや/dev/urandomはエントロピープールを利用した乱数ジェネレーターです。

エントロピープールとは、ハードウェア(キーボード・マウス・CPUなどの)動作から得られる環境ノイズを蓄積した データ領域のことを指します。

ハードウェアから発生する物理特性に基づいた不規則な環境ノイズを蓄積し、それを元に乱数を生成することで 推測が困難な真の乱数(※)の生成を実現しています。

/dev/randomや/dev/urandomは共に乱数ジェネレーターとして機能しますが、それぞれは異なる特性を持ちます。

/dev/randomは乱数の生成に使用した(中身を開示した)エントロピープールを再利用しないようにロックを掛けて 新しい環境ノイズが蓄積するのを待つ特性を持ちます。そのため、連続した乱数生成を実施できない場合があります。

対して、/dev/urandomは一度使用したエントロピープールを再利用してそれから乱数を生成することができる特性を持ちます。 そのため、いつでも使用することが可能代わりに疑似的な乱数を生成します。これにより、乱数の推測は/dev/randomよりも 容易になりますが、連続した乱数生成における制約はありません。

このように、/dev/randomと/dev/urandomは異なる特性(メリット・デメリット)を持った乱数ジェネレーターです。

次節では両者のメリット・デメリットをもう少しまとめて掲載した上で、その使い方を紹介します。

真の乱数と言ってもハードウェアから発生する環境ノイズを利用したものなので、外部から推測が可能な場合が常にあります。 そのため最大限、外部からの推測を困難にするような乱数の生成規則を適用する工夫が施されています。

/dev/randomの使い方

エントロピープールを元に乱数を生成する疑似デバイスファイルです。

以下のメリットとデメリットを持ちます。

  1. メリット:真の乱数を生成できる
  2. デメリット:エントロピープールが空になるとロックされ、新たに蓄積されるまで使用できない
  3. 用途:セキュリティや暗号強度が求められる暗号鍵や長期利用のパスワード生成など

使い方は以下のようにして、/dev/randomから生成される乱数列をheadやodで利用したい分切り取ります。

headコマンドを使用してランダムな文字列を取得する(12文字)

kamo@kamo:~$ head -c 8 /dev/random | base64
kp9bxBY6osU=

odコマンドを使用してランダムな数字列を取得する(10文字)

kamo@kamo:~$ od -An -tu4  -N4 /dev/random | sed 's/^ *//'
6249257105

先に説明したように/dev/randomは一度使用すると、エントロピープールが再蓄積されるまでロックされます。

乱数列(バイナリデータ)のBase64エンコードについて

/dev/randomや/dev/urandomによって生成される乱数列はバイナリデータです。 そのため、英数字などのテキストデータに変換するためにBase64エンコードを実施する必要があります。

これによって、パスワードとして使用できる文字列(文字種:64個)を得ることができます。

※エンコードデータを元のバイナリデータに戻したい場合はBase64デコードをする必要があります。

/dev/urandomの使い方

エントロピープールを再利用する仕組みを持つため、/dev/randomのようにロックされることはありません。 常に開放(アンロック)されているのでurandomという名前が付けられています。

  1. メリット:エントロピープールを再利用するため、いつでも使用できる
  2. デメリット:疑似的な乱数を生成するため真の乱数よりも推測が容易
  3. 用途:テスト用のベンチマークデータ、セキュリティ上の懸念事項が少ない状況でのパスワード生成など

headコマンドを使用してランダムな文字列を取得する(12文字)

kamo@kamo:~$ head -c 8 /dev/urandom | base64
CXz43Q7aaUSK

odコマンドを使用してランダムな数字列を取得する(10文字)

kamo@kamo:~$ od -An -tu4  -N4 /dev/urandom  | sed 's/^ *//'
1846207633

getrandomシステムコール

/dev/randomや/dev/urandomは疑似デバイスファイルであるため、使用するためにはファイルオープンをして ファイルディスクリプタ(fd)を割り当てないといけません。

つまり、ファイルディスクリプタの枯渇やchroot監獄の状態になるとファイルオープンが失敗しますので、 乱数生成することができなくなります。

そこで、LinuxではLinux 3.17、glibc 2.25からgetrandomシステムコールが導入されました。 getrandomシステムコールを利用することでファイルオープンに起因する乱数生成の失敗を回避することができます。 ※getrandomシステムコールの詳しい仕様はこちらを参照してください。

次にgetrandomシステムコールを使用したサンプルプログラム(random-generator.c)のソースコードを掲載します。

ここで紹介するソースコードは自由に使っていただいて構いません。 アプリケーションの開発や自己学習にお役立て下さい。ただし、当ブログでは掲載するソースコードを流用・利用したことによる損害等につきましては 一切の責任を負いません。自己責任で利用お願いします。

// random-generator.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/random.h>

#define BUFFER_MAX_SIZE 256

void get_random_string(const char*, char*);
char encode_base64(const char*, int);

int main(void) {
    ssize_t result;
    char src[BUFFER_MAX_SIZE];
    char dst[BUFFER_MAX_SIZE];

    memset(src, '\0', BUFFER_MAX_SIZE);
    memset(dst, '\0', BUFFER_MAX_SIZE);

    // [FLAGS]
    // GRND_RANDOM   ... source: /dev/random
    // GRND_NONBLOCK ... source: /dev/urandom (default)
    result = getrandom(src, BUFFER_MAX_SIZE, GRND_NONBLOCK);

    if (result == -1) {
        // check errno: https://man7.org/linux/man-pages/man3/errno.3.html
        perror("getrandom error");
        return -1;
    }

    get_random_string(src, dst);

    fprintf(stdout, "The numbers of bytes that were copied to the \"buffer\": %d[bytes]\n", result);
    fprintf(stdout, "The random string (%d characters) : %s\n", strlen(dst), dst);

    return 0;
}

void get_random_string(const char* buf, char* dst) {

    int offset = sizeof(long);
    int cnt = 0;
    int i = 0;

    for (i = 0; (i < BUFFER_MAX_SIZE) && (cnt < BUFFER_MAX_SIZE); i += offset) {
        if ((i + offset) > BUFFER_MAX_SIZE) {
            return;
        }

        dst[cnt++] = encode_base64(buf, i);
    }
}

char encode_base64(const char* buf, int target_pos) {

    long num = 0L;
    char* encode_character = NULL;
    int long_byte_size = sizeof(long);
    int shiftcnt;

    for (shiftcnt = long_byte_size - 1; shiftcnt > 0; shiftcnt--) {
        num |= (long)buf[target_pos++] >> (shiftcnt * 8);
    }

    num |= (long)buf[target_pos];

    encode_character = l64a(num);

    return *encode_character;
}

【注意】上記サンプルプログラム(random-generator.c)のbase64エンコードについて

今回使用しているbase64エンコード関数はl64aです。

この関数は引数のlong型データをbase64エンコードの一種に変換する処理です。 厳密なbase64エンコードではないため、変換される文字セットの範囲が若干異なります。

l64a関数の変換文字セットの詳細はこちらを参照してください。

ポイント
getrandomは第三引数として、以下のいずれかのフラグ(マクロ定数)を指定することで乱数生成のソースを /dev/randomとするのか/dev/urandomとするのかを選択します。

フラグ 説明
GRND_RANDOM 乱数生成のソースとして/dev/randomを使用する
GRND_NONBLOCK 乱数生成のソースとして/dev/urandomを使用する

上記サンプルプログラムの実行結果は以下のようになります。

kamo@kamo:~$ gcc -o random-generator random-generator.c
kamo@kamo:~$ ./random-generator
The numbers of bytes that were copied to the "buf": 256[bytes]
The random string (64 characters) : MCR7bRWdVFvudPyY12C4dbL9SYgiKIqRFL6iwTSxhwG7DeGtt6qVZZKLJc7SCtah

おわりに

今回はLinuxシステム上で使用できる乱数ジェネレーターとして/dev/randomと/dev/urandom(それとgetrandom)について 紹介しました。

これらはLinux上のアプリケーション開発でデータの暗号化やパスワードを提供する場面などで利用できます。

メリットとデメリットを考慮して利用することをお勧めします。

参考文献