JavaScriptでお手軽にランダム文字列の生成
JavaScriptでお手軽にランダム文字列の生成:
お手軽さを重視しているため、作り方に偏りがある事が分かっている物もある。
以下の環境で確認
しかし、末端の0がくると省略されるため、任意の桁数が得られるとは限らない。以下のコードでも100万回ぐらい生成を繰り返すと6桁のものがでてきたりする。
一番最初に考えたやり方がこれだった。
なお、
そのため、
「Math.randomはセキュアではないのでは?」という難癖をつける場合、
以下の方法は除算を使うので特定の文字に偏りが起きる。なのでセキュア志向なら使ったらダメ。(以下の例だと
なお、
これよりも任意の文字を偏りなく出す方法を他のAPIで出来ないか考えたけど、すぐに思い浮かばなかったので諦めてます。
文字を用意するのが面倒な場合、
複雑な記号は使えないが、除算を使わない分こっちのほうが偏りも少ない気がする。(未調査)
Nodeにはブラウザとは異なるAPIを持つ
ブラウザの
以下の方法も同じように除算を使ってるので偏ります。
Base64も同じ要領で...と思ったが、btoaがないので、ブラウザのようには行えない。
代わりにNodeには
そして、このBufferがBase64変換に対応している。なので、もう少し短く書ける。
Nodeはこれが一番スマートな気がする。
質の良い乱数生成期を使ったとしても、剰余を求めるやり方を行ってしまうと、偏りが起きる場合がある。
これは、0~255(256個)がランダムで得られたとしても、剰余計算によって0~61(62個)のいずれかになる。
すると、0~61,62~123, 124~185,186~247の間は均等に出現するが、残りの248~255は0~7しか出現しえないため、この分が他と比べて多く出る。これで偏りが起きる。
逆に256を割り切れる数(2,4,8...64,128)で割れば計算による偏りは起きない。もし偏る場合は元の乱数の問題。
また、ランダムで得られる範囲0-255と狭いせいというのもあるので、この範囲を十分に大きくすると剰余計算による偏りは減らすことができる。
上記のコードなら、
お手軽さを重視しているため、作り方に偏りがある事が分かっている物もある。
以下の環境で確認
- Node: v10.15.0
- ブラウザ: Chrome 71.0.3578.98, Firefox 64.0.2
Node/ブラウザ共通
短いコードで
[a-v]
と[0-9]
の32文字で12桁以下なら、これでそれっぽいのが作れる。StackOverflowで知った。しかし、末端の0がくると省略されるため、任意の桁数が得られるとは限らない。以下のコードでも100万回ぐらい生成を繰り返すと6桁のものがでてきたりする。
Math.random().toString(32).substring(2) // 'a6dpgjqlq8g' 等
任意の桁数・任意の文字で
var S="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" var N=16 Array.from(Array(N)).map(()=>S[Math.floor(Math.random()*S.length)]).join('')
なお、
Array(N)
だけでは長さNの配列になっているが要素が無い状態のため、mapが機能しないらしい。そのため、
Array.from(Array(N))
としてやることで、undefinedな要素が入りmapが機能するようになる。(ES6以降なら、Array.from
の代わりに[...Array(N)]
が使える。以降はES6/ES5ごちゃまぜになっている。)
ブラウザ
よりセキュアな乱数生成器で
「Math.randomはセキュアではないのでは?」というcrypto.getRandomValues
があるのでこちらを使う。結構古いブラウザもサポートしてる模様。IE11もmsCrypto
から使える。以下の方法は除算を使うので特定の文字に偏りが起きる。なのでセキュア志向なら使ったらダメ。(以下の例だと
[a-h]
が出やすい)var S="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" var N=16 Array.from(crypto.getRandomValues(new Uint8Array(N))).map((n)=>S[n%S.length]).join('')
Uint8Array
からUint32Array
にすると偏りは少しはマシになる。後述。これよりも任意の文字を偏りなく出す方法を他のAPIで出来ないか考えたけど、すぐに思い浮かばなかったので諦めてます。
Base64で
文字を用意するのが面倒な場合、String.fromCharCode
で変換してからbtoa
でBase64変換を使う手がある。複雑な記号は使えないが、除算を使わない分こっちのほうが偏りも少ない気がする。(未調査)
var N=16 btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(N)))).substring(0,N)
Nodeのみ
よりセキュアな乱数生成器で
Nodeにはブラウザとは異なるAPIを持つcrypto
モジュールがある。ブラウザの
crypto.getRandomValues
と同じ動作をするcrypto.randomFillSync
があるのでこれで。以下の方法も同じように除算を使ってるので偏ります。
const crypto = require('crypto') const S="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" const N=16 Array.from(crypto.randomFillSync(new Uint8Array(N))).map((n)=>S[n%S.length]).join('')
Base64で
Base64も同じ要領で...と思ったが、btoaがないので、ブラウザのようには行えない。代わりにNodeには
crypto.randomBytes
がある。これはN個のランダムなバイト列を作り、Node固有のBufferという型で返す。(crypto.randomFillSync(new Uint8Array(N))
に似ているが、戻り値の型が違う。)そして、このBufferがBase64変換に対応している。なので、もう少し短く書ける。
const crypto = retuire('crypto') const N = 16 crypto.randomBytes(N).toString('base64').substring(0, N)
おまけ:剰余計算による偏り
質の良い乱数生成期を使ったとしても、剰余を求めるやり方を行ってしまうと、偏りが起きる場合がある。const S="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" // 62文字 let p=Array(S.length).fill(0) // 100個づつ乱数を取り出し集計する処理を6200回繰り返す(620000回行う) for(let i=0;i<100*S.length; ++i){ const arr = crypto.randomFillSync(new Uint8Array(100)) for(let j=0; j<arr.length; ++j){ p[arr[j]%S.length]++; } } p // => 均等にばらければ全部が10000前後になるはずだが、あきらかに0-7番目の要素のカウントが大きいことが分かる。
すると、0~61,62~123, 124~185,186~247の間は均等に出現するが、残りの248~255は0~7しか出現しえないため、この分が他と比べて多く出る。これで偏りが起きる。
逆に256を割り切れる数(2,4,8...64,128)で割れば計算による偏りは起きない。もし偏る場合は元の乱数の問題。
また、ランダムで得られる範囲0-255と狭いせいというのもあるので、この範囲を十分に大きくすると剰余計算による偏りは減らすことができる。
上記のコードなら、
Uint8Array
=> Uint32Array
にしてみると、偏りが目立たなくなることがわかる。
コメント
コメントを投稿