【JavaScript】ランダムな数値を取得する方法

 JavaScriptには、ビルトインオブジェクトのMathクラスが持つ静的関数のrandom()がありますから、通常はそれを利用すれば事足ります。

 しかし、よりセキュアな方法があることはあまり知られていません。今回はそれも含めて解説してみます。

Math.random()を使う

 最もポピュラーで手軽な方法です。実際のところ、ただランダムな数値が欲しい場合はこれで十分です。

 Math.random()は、0以上で1未満のランダムな浮動小数点を返します。

let randomFloat = Math.random();
console.log(randomFloat); // -> 0.13213566616186645

 「下限値および上限値の間のintを取得する」というのがおそらく最も多い使い方です。

const MIN = 5;
const MAX = 10;
let randomFloat = Math.random();
let randomInt = Math.floor(randomFloat * (MAX - MIN)) + MIN;
console.log(randomInt); // -> 6

 このコードで、5以上10未満の値を取得できます。

Math.random()の問題点

 MDNのMath.random()の記述によると、以下のように警告されています。

Math.random() の提供する乱数は、暗号に使用可能な安全性を備えていません。セキュリティに関連する目的では使用しないで下さい。代わりにWeb Crypto API(より正確にはwindow.crypto.getRandomValues()メソッド)を使用して下さい。

 Hackernoonに興味深い記事があります。この記事によると、Math.random()は疑似乱数を生成しているだけで、真の乱数でないことに言及されています。

 JavaScript(ECMAScript)は、Math.random()に対するインターフェースを定義してはいますが、その実装はブラウザに一任されています。これはMath.random()に限ったことではありません。

 分かりやすく言えば、ECMAScriptは、「基本設計」を担い、ブラウザは「詳細設計」以降のフェーズを担う、という役割分担によってJavaScriptは実現されていると言えます。

 話がそれましたが、これは「ブラウザによって、Math.random()の乱数生成強度が全く異なる」ということを意味しています。その事情によって、過去にいくつかの問題を生じさせた経緯もあります。

crypto.getRandomValues()を使う

 getRandomValues()メソッドは、WebCryptoAPIの仕様書で、次のように定義されています。

The getRandomValues method generates cryptographically random values.

 暗号的にランダムな値を返す、ということですから、少なくともMath.random()よりも暗号強度の高い乱数が返ってくることが期待できます。試しに、5つのランダムな値を取得してみましょう。

let arr = new Uint32Array(5);
crypto.getRandomValues(arr);

console.log(arr); // -> Uint32Array(5) [836164506, 2650582686, 3220344852, 2279112908, 4196113461]

 Uint32Arrayは、unsigned intの32bitの値の配列です。つまり、この例でのgetRandomValues()は、[0 ~ 4,294,967,295]までの乱数を生成します。

 8bitでも試してみましょう。以下のコードでは0~255までの整数が出力されます。

let arr = new Uint8Array(5);
crypto.getRandomValues(arr);

console.log(arr); // -> Uint8Array(5) [3, 153, 100, 105, 101]

 ところで、getRandomValuesは取得する配列のサイズが65536byteを超えると、QuotaExceededErrorをthrowします。実際に試してみます。

let arr = new Uint32Array(16385);
crypto.getRandomValues(arr); // -> Uncaught DOMException: Failed to execute 'getRandomValues' on 'Crypto': The ArrayBufferView's byte length (65540) exceeds the number of bytes of entropy available via this API (65536).

 この仕様には少しの注意が必要でしょう。

MINからMAXまでのランダム整数を取得する

 さて、上記のような配列では実用的に問題があるかもしれません。少しのロジックを追加して、MIN~MAXまでのランダムな整数を出力するプログラムを組んでみます。

const MIN = 5;
const MAX = 10;
const ARRAY_LENGTH = 5;

let diff = MAX - MIN;

let IntArray = ((maxSize)=>{
  if(maxSize < 255) {
    return Uint8Array;
  }
  if(maxSize < 65535 ){
    return Uint16Array;
  }
  if(maxSize < 4294967295){
    return Uint32Array;
  }
  throw new RangeError("MAX must be less than 4294967295");
})(MAX);

let arr = new IntArray(ARRAY_LENGTH);
crypto.getRandomValues(arr);
let randomArray = Array.from(arr, v => (v % diff) + MIN);
console.log(randomArray); // -> [9, 7, 7, 6, 7]

 クラスとして実用化するには、Generator関数を使うなどもう少し工夫が必要です。以下の記事も参考にしてみてください。

 ES2015から実装されたGeneratorという機能について実際に試しながら解説します。 Generator関数とは  ES2015...

使いどころ

 フロントエンドにおいては、Math.random()で事足りることが多く、セキュリティ面で使うにしてもSubtleCrypto.generateKey()などがありますから、実際にはあまり使う機会のない関数かもしれません。

 あえて使うなら、UUIDの生成器を作成するときくらいでしょうか。

まとめ

 今回のように、ランダムな値を1つ得るためにも複数の方法があることがわかります。

 プログラムを記述するときは、その場に合ったやり方を考えていきましょう。