entropy-string
Version:
Efficiently generate cryptographically strong random strings of specified entropy from various character sets.
335 lines (279 loc) • 10 kB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.EntropyString = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
const { csprngBytes } = require('./lib/csprng-bytes')
const { prngBytes } = require('./lib/prng-bytes')
const BITS_PER_BYTE = 8
const {
abs, ceil, floor, log2, round
} = Math
const gcd = (a, b) => {
let la = a
let lb = b
while (lb !== 0) {
[la, lb] = [lb, la % lb]
}
return abs(la)
}
const lcm = (a, b) => (a / gcd(a, b)) * b
const genNdxFn = (bitsPerChar) => {
// If BITS_PER_BYTEs is a multiple of bitsPerChar, we can slice off an integer number
// of chars per byte.
if (lcm(bitsPerChar, BITS_PER_BYTE) === BITS_PER_BYTE) {
return (chunk, slice, bytes) => {
const lShift = bitsPerChar
const rShift = BITS_PER_BYTE - bitsPerChar
return ((bytes[chunk] << (lShift * slice)) & 0xff) >> rShift
}
}
// Otherwise, while slicing off bits per char, we can possibly straddle two
// of bytes, so a more work is involved
const slicesPerChunk = lcm(bitsPerChar, BITS_PER_BYTE) / BITS_PER_BYTE
return (chunk, slice, bytes) => {
const bNum = chunk * slicesPerChunk
const offset = (slice * bitsPerChar) / BITS_PER_BYTE
const lOffset = floor(offset)
const rOffset = ceil(offset)
const rShift = BITS_PER_BYTE - bitsPerChar
const lShift = (slice * bitsPerChar) % BITS_PER_BYTE
let ndx = ((bytes[bNum + lOffset] << lShift) & 0xff) >> rShift
const r1Bits = (rOffset + 1) * BITS_PER_BYTE
const s1Bits = (slice + 1) * bitsPerChar
const rShiftIt = (r1Bits - s1Bits) % BITS_PER_BYTE
if (rShift < rShiftIt) {
ndx += bytes[bNum + rOffset] >> rShiftIt
}
return ndx
}
}
class CharSet {
constructor(chars) {
if (!(typeof chars === 'string' || chars instanceof String)) {
throw new Error('Invalid chars: Must be string')
}
const { length } = chars
if (![2, 4, 8, 16, 32, 64].includes(length)) {
throw new Error('Invalid char count: must be one of 2,4,8,16,32,64')
}
// Ensure no repeated characters
for (let i = 0; i < length; i += 1) {
const c = chars.charAt(i)
for (let j = i + 1; j < length; j += 1) {
if (c === chars.charAt(j)) {
throw new Error('Characters not unique')
}
}
}
this.chars = chars
this.bitsPerChar = floor(log2(length))
this.length = length
this.ndxFn = genNdxFn(this.bitsPerChar)
this.charsPerChunk = lcm(this.bitsPerChar, BITS_PER_BYTE) / this.bitsPerChar
}
bytesNeeded(bitLen) {
const count = ceil(bitLen / this.bitsPerChar)
return ceil((count * this.bitsPerChar) / BITS_PER_BYTE)
}
}
const charset64 = new CharSet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_')
const charset32 = new CharSet('2346789bdfghjmnpqrtBDFGHJLMNPQRT')
const charset16 = new CharSet('0123456789abcdef')
const charset8 = new CharSet('01234567')
const charset4 = new CharSet('ATCG')
const charset2 = new CharSet('01')
const stringWithBytes = (bytes, bitLen, charset) => {
if (bitLen <= 0) { return '' }
const { bitsPerChar } = charset
const count = ceil(bitLen / bitsPerChar)
if (count <= 0) { return '' }
const need = ceil(count * (bitsPerChar / BITS_PER_BYTE))
if (bytes.length < need) {
throw new Error(`Insufficient bytes: need ${need} and got ${bytes.length}`)
}
const { ndxFn, charsPerChunk, chars } = charset
const chunks = floor(count / charsPerChunk)
const partials = count % charsPerChunk
let string = ''
for (let chunk = 0; chunk < chunks; chunk += 1) {
for (let slice = 0; slice < charsPerChunk; slice += 1) {
const ndx = ndxFn(chunk, slice, bytes)
string += chars[ndx]
}
}
for (let slice = 0; slice < partials; slice += 1) {
const ndx = ndxFn(chunks, slice, bytes)
string += chars[ndx]
}
return string
}
const entropyBits = (total, risk) => {
if (total === 0) { return 0 }
let N
if (total < 1000) {
N = log2(total) + log2(total - 1)
} else {
N = 2 * log2(total)
}
return (N + log2(risk)) - 1
}
class Entropy {
constructor(params = { bits: 128, charset: charset32 }) {
if (params !== undefined) {
if (!(params instanceof Object)) {
throw new Error('Invalid argument for Entropy constructor: Expect params object')
}
if (params.bits === undefined &&
params.charset === undefined &&
params.total === undefined &&
params.risk === undefined &&
params.prng === undefined) {
throw new Error('Invalid Entropy params')
}
if (params.bits !== undefined) {
if (typeof params.bits !== 'number') {
throw new Error('Invalid Entropy params: non-numeric bits')
}
if (params.bits < 0) {
throw new Error('Invalid Entropy params: negative bits')
}
}
if (params.total !== undefined) {
if (typeof params.total !== 'number') {
throw new Error('Invalid Entropy params: non-numeric total')
}
if (params.total < 1) {
throw new Error('Invalid Entropy params: non-positive total')
}
}
if (params.risk !== undefined) {
if (typeof params.risk !== 'number') {
throw new Error('Invalid Entropy params: non-numeric risk')
}
if (params.risk < 1) {
throw new Error('Invalid Entropy params: non-positive risk')
}
}
if (params.risk !== undefined && typeof params.risk !== 'number') {
throw new Error('Invalid Entropy params: non-numeric risk')
}
if ((params.total !== undefined && params.risk === undefined) ||
(params.total === undefined && params.risk !== undefined)) {
throw new Error('Invalid Entropy params: total and risk must be paired')
}
if (params.bits !== undefined &&
(params.total !== undefined || params.risk !== undefined)) {
throw new Error('Invalid Entropy params: bits with total and/or risk')
}
}
let bitLen
if (params.bits) {
bitLen = round(params.bits)
} else if (params.total && params.risk) {
bitLen = round(entropyBits(params.total, params.risk))
} else {
bitLen = 128
}
let charset
if (params.charset instanceof CharSet) {
const { charset: cs } = params
charset = cs
} else if ((typeof params.charset === 'string' || params.charset instanceof String)) {
charset = new CharSet(params.charset)
} else {
charset = charset32
}
this.charset = charset
this.bitLen = bitLen
this.prng = params.prng || false
}
static bits(total, risk) { return entropyBits(total, risk) }
smallID(charset = this.charset) {
return this.string(29, charset)
}
mediumID(charset = this.charset) {
return this.string(69, charset)
}
largeID(charset = this.charset) {
return this.string(99, charset)
}
sessionID(charset = this.charset) {
return this.string(128, charset)
}
token(charset = this.charset) {
return this.string(256, charset)
}
string(bitLen = this.bitLen, charset = this.charset) {
const bytesNeeded = charset.bytesNeeded(bitLen)
const bytes = this.prng ? prngBytes(bytesNeeded) : csprngBytes(bytesNeeded)
return this.stringWithBytes(bytes, bitLen, charset)
}
stringWithBytes(
bytes,
bitLen = this.bitLen, charset = this.charset
) {
return stringWithBytes(bytes, bitLen, charset)
}
bytesNeeded(bitLen = this.bitLen, charset = this.charset) {
return charset.bytesNeeded(bitLen)
}
chars() {
return this.charset.chars
}
bits() {
return this.bitLen
}
use(charset) {
if (!(charset instanceof CharSet)) { throw new Error('Invalid CharSet') }
this.charset = charset
}
useChars(chars) {
if (!(typeof chars === 'string' || chars instanceof String)) {
throw new Error('Invalid chars: Must be string')
}
this.use(new CharSet(chars))
}
}
module.exports = {
CharSet,
Entropy,
charset2,
charset4,
charset8,
charset16,
charset32,
charset64
}
},{"./lib/csprng-bytes":2,"./lib/prng-bytes":3}],2:[function(require,module,exports){
const csprngBytes = count => window.crypto.getRandomValues(new Uint8Array(count))
module.exports = {
csprngBytes
}
},{}],3:[function(require,module,exports){
const { ceil, random } = Math
const BITS_PER_BYTE = 8
const endianByteNum = (() => {
const buf32 = new Uint32Array(1)
const buf8 = new Uint8Array(buf32.buffer)
buf32[0] = 0xff
return (buf8[0] === 0xff) ? [2, 3, 4, 5, 6, 7] : [0, 1, 2, 3, 6, 7]
})()
const prngBytes = (count) => {
const BYTES_USED_PER_RANDOM_CALL = 6
const randCount = ceil(count / BYTES_USED_PER_RANDOM_CALL)
const buffer = new ArrayBuffer(count)
const dataView = new DataView(new ArrayBuffer(BITS_PER_BYTE))
for (let rNum = 0; rNum < randCount; rNum += 1) {
dataView.setFloat64(0, random())
for (let n = 0; n < BYTES_USED_PER_RANDOM_CALL; n += 1) {
const fByteNum = endianByteNum[n]
const bByteNum = (rNum * BYTES_USED_PER_RANDOM_CALL) + n
if (bByteNum < count) {
buffer[bByteNum] = dataView.getUint8(fByteNum)
}
}
}
return buffer
}
module.exports = {
prngBytes
}
},{}]},{},[1])(1)
});