比特高效,统一,加密安全的随机数生成

时间:2013-09-22 05:40:21

标签: java random entropy

我记得在一篇关于数学导向网站的文章中有关有效使用随机位的方法,但我似乎无法在谷歌中找到合适的关键字来找到它,而且它不在我的浏览器历史记录中。

问题的要点是在域[domainStartdomainEnd)中采用一系列随机数,并有效地使用随机数序列的位来均匀投影范围[rangeStartrangeEnd)。域和范围都是整数(更准确地说,long s而不是Z)。 执行此操作的算法是什么?

实施方面,我有一个带有此签名的函数:

long doRead(InputStream in, long rangeStart, long rangeEnd);

in基于我需要使用的CSPRNG(由硬件RNG提供,通过SecureRandom调节);返回值必须介于rangeStartrangeEnd之间,但明显的实现方式是浪费:

long doRead(InputStream in, long rangeStart, long rangeEnd) {
    long retVal = 0;
    long range = rangeEnd - rangeStart;

    // Fill until we get to range
    for (int i = 0; (1 << (8 * i)) < range; i++) {
        int in = 0;
        do {
            in = in.read();
        // but be sure we don't exceed range
        } while(retVal + (in << (8 * i)) >= range);
        retVal += in << (8 * i);
     }

    return retVal + rangeStart;
}

我认为这与(rand() * (max - min)) + min实际上是一样的,只有我们丢弃了推动我们过max的位。我们丢弃这些位并重试,而不是使用可能错误地将结果偏向较低值的模运算符。由于点击CSPRNG可能会触发重新播种(可以阻止InputStream),我想避免浪费随机位。 Henry指出此代码偏向于0和257;班塔尔在一个例子中证明了这一点。

第一次编辑:亨利提醒我,求和会调用中心极限定理。我修复了上面的代码来解决这个问题。

第二次编辑:Mechanical蜗牛建议我查看Random.nextInt()的源代码。读了一会儿后,我意识到这个问题类似于基本转换问题。见下面的答案。

2 个答案:

答案 0 :(得分:2)

您的算法会产生偏差结果。我们假设rangeStart=0rangeEnd=257。如果第一个字节大于0,那将是结果。如果是0,结果将是0256,概率为50/50。因此,0256的选择可能性比其他任何数字都低两倍。

我做了一个简单的test来证实这一点:

p(0)=0.001945
p(1)=0.003827
p(2)=0.003818
...
p(254)=0.003941
p(255)=0.003817
p(256)=0.001955

我认为您需要像java.util.Random.nextInt那样做并丢弃整个数字,而不是最后一个字节。

答案 1 :(得分:0)

在读取Random.nextInt()的源代码后,我意识到这个问题类似于基本转换问题。

不是一次转换单个符号,而是通过累加器&#34;缓冲器&#34;一次转换输入符号块更有效。它足够大,可以在域和范围内表示至少一个符号。新代码如下所示:

public int[] fromStream(InputStream input, int length, int rangeLow, int rangeHigh) throws IOException {
    int[] outputBuffer = new int[length];
    // buffer is initially 0, so there is only 1 possible state it can be in
    int numStates = 1;
    long buffer = 0;
    int alphaLength = rangeLow - rangeHigh;
    // Fill outputBuffer from 0 to length
    for (int i = 0; i < length; i++) {
        // Until buffer has sufficient data filled in from input to emit one symbol in the output alphabet, fill buffer.
        fill:
        while(numStates < alphaLength) {
            // Shift buffer by 8 (*256) to mix in new data (of 8 bits)
            buffer = buffer << 8 | input.read();
            // Multiply by 256, as that's the number of states that we have possibly introduced
            numStates = numStates << 8;
        }
        // spits out least significant symbol in alphaLength
        outputBuffer[i] = (int) (rangeLow + (buffer % alphaLength));
        // We have consumed the least significant portion of the input.
        buffer = buffer / alphaLength;
        // Track the number of states we've introduced into buffer
        numStates = numStates / alphaLength;
    }
    return outputBuffer;
}

然而,在基数和这个问题之间转换数字之间存在根本区别;为了在基数之间进行转换,我认为需要有足够的关于数量的信息来执行计算 - 目标基数的连续除法导致剩余部分用于构造目标字母表中的数字。在这个问题上,我并不是真的需要知道所有这些信息,只要我没有偏向数据,这意味着我可以做我在标记为&#34;填充的循环中所做的事情。 #34;