关于lua :(安全)随机字符串?


(Secure) Random string?

在Lua中,通常使用math.randommath.randomseed生成随机值和/或字符串,其中os.time用于math.randomseed

但是,这种方法有一个主要缺点。返回的数字始终与当前时间一样随机,并且每个随机数的间隔为一秒,如果在很短的时间内需要多个随机值,则该时间太长。

Lua用户Wiki:http://lua-users.org/wiki/MathLibraryTutorial以及相应的RandomStringS记录甚至指出了此问题:http://lua-users.org/wiki/RandomStrings。

因此,我坐下来编写了另一种算法(如果可以称之为),该算法通过(错误地)使用表的内存地址来生成随机数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
math.randomseed(os.time())
function realrandom(maxlen)
    local tbl = {}
    local num = tonumber(string.sub(tostring(tbl), 8))
    if maxlen ~= nil then
        num = num % maxlen
    end
    return num
end

function string.random(length,pattern)
    local length = length or 11
    local pattern = pattern or '%a%d'
    local rand =""
    local allchars =""
    for loop=0, 255 do
        allchars = allchars .. string.char(loop)
    end
    local str=string.gsub(allchars, '[^'..pattern..']','')
    while string.len(rand) ~= length do
        local randidx = realrandom(string.len(str))
        local randbyte = string.byte(str, randidx)
        rand = rand .. string.char(randbyte)
    end

    return rand
end

一开始,一切似乎都是随机的,而且我敢肯定,至少对于当前程序而言。

所以我的问题是,realrandom返回的这些数字真的有多随机?

还是有一种更好的方法可以在比一秒钟更短的时间间隔内生成随机数(如上所述,这意味着不应该使用os.time),而无需依赖外部库,并且,如果可能,在完全跨平台的方式?

编辑:
RNG的播种方式似乎存在重大误解。在生产代码中,对math.randomseed()的调用仅发生一次,这只是此处的错误选择示例。

我所说的随机值是每秒仅随机一次,此粘贴很容易说明:http://codepad.org/4cDsTpcD

由于无论我进行何种编辑,该问题都会被否决,因此我也取消了以前接受的答案-希望有一个更好的答案,即使只是更好的意见。我知道关于随机值/数字的问题已经讨论过很多次了,但是我没有找到与Lua有关的问题-请记住这一点!


  • 您不应该在每次调用random时都调用种子,而应该在程序初始化时仅调用一次(除非您从某个地方获取种子,例如,复制以前的某些"随机"行为)。

  • 在统计意义上,标准Lua随机生成器的质量很差(事实上,事实上,它是标准C随机生成器),如果需要,请不要使用它。例如,使用lrandom模块(在LuaRocks中可用)。

  • 如果需要更安全的随机性,请在Linux上从/dev/random中读取。 (我认为Windows应该具有相同的含义-但您可能需要使用C编写一些代码才能使用它。)

  • 依赖表指针值是一个坏主意。例如,考虑一下Java中的替代Lua实现-没有告诉他们它们将返回什么。 (此外,指针值可能是可预测的,在某些情况下,每次调用程序时,指针值可能是相同的。)

  • 如果您希望种子具有更高的精度(并且仅在启动程序的频率高于每秒一次的情况下才需要),则应使用分辨率更高的计时器。例如,来自LuaSocket的socket.gettime()。将其乘以某个值,因为math.randomseed仅适用于整数部分,并且socket.gettime()返回的时间为(浮点)秒。

    1
    2
    3
    4
    5
    6
    7
    require 'socket'

    math.randomseed(socket.gettime() * 1e6)

    for i = 1, 1e3 do
      print(math.random())
    end

  • This method however has one major
    weakness; The returned number is
    always just as random as the current
    time, AND the interval for each random
    number is one second, which is way too
    long if one needs many random values
    in a very short time.

    仅当您实施不正确时,它才具有这些缺点。

    应当很少调用math.randomseed-通常在程序开始时仅调用一次,并且通常使用os.time进行种子设置。设置种子后,您可以多次使用math.random,它将产生随机值。

    查看此样本发生了什么:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    > math.randomseed(1)
    > return math.random(), math.random(), math.random()
    0.84018771715471    0.39438292681909    0.78309922375861
    > math.randomseed(2)
    > return math.random(), math.random(), math.random()
    0.70097636929759    0.80967634907443    0.088795455214007
    > math.randomseed(1)
    > return math.random(), math.random(), math.random()
    0.84018771715471    0.39438292681909    0.78309922375861

    将种子从1更改为2时,会得到不同的随机结果。但是当我回到1时,"随机序列"被重置。我获得与以前相同的值。

    os.time()返回一个不断增加的数字。将其用作种子是适当的;那么您可以永久调用math.random,并且每次调用时都有不同的随机数。

    您唯一需要担心非随机性的情况是程序应该每秒执行一次以上。正如其他人所说,在那种情况下,最简单的解决方案是使用具有更高清晰度的时钟。

    换一种说法:

    • 在程序开始时用适当的种子(os.time()可以满足99%的情况)调用math.randomseed
    • 每次需要随机数时调用math.random

    问候!


    对问题的第一部分的一些想法:

    So my question is, how random are these numbers returned by realrandom really?

    您的函数正在尝试通过使用其默认实现tostring()的古怪发现表的地址。我不相信tostring{}返回的字符串具有指定的格式,或者该字符串中包含的值没有任何已记录的含义。实际上,它是从与特定表相关的内容的地址派生的,因此不同的表会转换为不同的字符串。但是,Lua的下一版本可以自由地将其更改为任何方便的内容。更糟糕的是,它采用的格式将高度依赖于平台,因为它似乎使用了sprintf()%p格式说明符,而该说明符仅被指定为明智的指针表示形式。

    还有一个更大的问题。尽管在您的平台上在进程中创建的第n个表的地址似乎是随机的,但tt可能根本不是随机的。或者它可能仅变化几位。例如,在我的win7盒子上,只有几处变化,并且不是非常随机的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    C:...>for /L %i in (1,1,20) do @ lua -e"print{}"
    table: 0042E5D8
    table: 0061E5D8
    table: 0024E5D8
    table: 0049E5D8
    table: 0042E5D8
    table: 0042E5D8
    table: 0042E5D8
    table: 0064E5D8
    table: 0042E5D8
    table: 002FE5D8
    table: 0042E5D8
    table: 0049E5D8
    table: 0042E5D8
    table: 0042E5D8
    table: 0042E5D8
    table: 0024E5D8
    table: 0042E5D8
    table: 0042E5D8
    table: 0061E5D8
    table: 0042E5D8

    当然,其他平台也会有所不同。我什至希望有一个平台,其中第一个分配表的地址是完全确定的,因此在程序的每次运行中都相同。

    简而言之,过程映像中任意对象的地址并不是很好的随机性来源。

    编辑:为了完整起见,我想补充一整夜想到的其他一些想法。

    stock tostring()函数由基础库提供,并由函数luaB_tostring()实现。相关的部分是这个片段:

    1
    2
    3
    4
    5
    switch (lua_type(L, 1)) {
      ...
      default:
        lua_pushfstring(L,"%s: %p", luaL_typename(L, 1), lua_topointer(L, 1));
        break;

    如果您真的在调用此函数,则字符串的末尾将是一个地址,以标准C sprintf()格式%p表示,与特定表密切相关。一种观察是,我已经看到%p的几种不同实现。 Windows MSVCR80.DLL(Lua Windows的当前版本使用的C库的版本)使其等效于%08X。我的Ubuntu Karmic Koala框似乎使它等效于%#x,这明显会删除前导零。如果要解析出字符串的那一部分,那么面对%p含义的变化时,应该采用一种更加灵活的方式。

    还要注意,在库代码中执行类似的操作可能会使您感到惊讶。

    首先,如果传递给tostring()的表具有提供函数__tostring()的元表,则将调用该函数,并且上面引用的片段将根本不会执行。在您的情况下,不会出现此问题,因为表具有单独的元表,并且您没有意外地将元表应用于本地表。

    其次,在模块加载时,某些其他模块或用户提供的代码可能已用其他东西替换了库存tostring()。如果替换是良性的(例如备忘录包装器),则对编写的代码来说可能无关紧要。但是,这将成为攻击的来源,并且完全不在模块的控制范围之内。如果目标是为您的随机种子提供某种改进的安全性,那么这并不是一个好主意。

    第三,您可能根本不会加载到库存的Lua解释器中,并且更大的应用程序(Lightroom,WoW,Wireshark等)可能会选择用其自己的实现替换基本库函数。对于tostring(),这是不太可能出现的问题,但是请注意,在替代实现中,基本库的print()是替换或删除的常见目标,如果print是而不是基础库中的实现。


    我想到了一些重要的事情:

    • 在大多数其他语言中,通常只在程序开始时调用一次随机的"种子"函数,或者在整个执行过程中仅在有限的时间调用一次。通常,您不希望每次生成随机数/序列时都调用它。如果在程序启动时调用一次,则会遇到"每秒一次"的限制。通过每次都调用它,实际上可能会减少结果的随机性。
    • 您的realrandom()函数似乎依赖于Lua的私有实现细节。如果此详细信息更改为始终返回相同的数字,或者仅返回偶数等,则在下一个主要版本中会发生什么。仅仅因为它现在可以正常工作还不能提供足够的保证,尤其是在需要安全的RNG的情况下。
    • 当您说"一切似乎都是随机的"时,您如何衡量这一表现?我们人类很难确定一个序列是否是随机的,仅仅查看一个数字序列实际上是不可能真正分辨出它们是否是随机的。有许多方法可以量化一个系列的"随机性",包括频率分布,自相关,压缩等,还有许多我无法理解的方法。
    • 如果您要为生产编写真正的"安全PRNG",请不要自己编写!由花费了数十年/数十年研究,设计并尝试将其破坏的专家调查和使用一个库或算法。真正安全的随机数生成很难。

    如果您需要更多信息,请从Wikipedia上的PRNG文章开始,并根据需要使用那里的引用/链接。