【问题标题】:safe enough 8-character short unique random string足够安全的 8 字符短唯一随机字符串
【发布时间】:2012-11-09 04:48:30
【问题描述】:

我正在尝试为数以千计的文件计算 8 个字符的短唯一随机文件名,而不会发生可能的名称冲突。这种方法够安全吗?

base64.urlsafe_b64encode(hashlib.md5(os.urandom(128)).digest())[:8]

编辑

为了清楚起见,我正在尝试对上传到存储的文件名进行最简单的混淆。

我发现 8 个字符的字符串,足够随机,如果实施得当,将是一种非常有效和简单的方式来存储数万个文件而不会发生冲突。我不需要保证唯一性,只需要足够高的名称冲突可能性(仅谈论数千个名称)。

文件存储在并发环境中,因此增加共享计数器是可以实现的,但很复杂。将计数器存储在数据库中会效率低下。

我还面临这样一个事实,即 random() 在某些情况下会在 不同 进程中返回 same 伪随机序列。

【问题讨论】:

  • “足够安全”是指“我根本不需要处理碰撞”,还是“碰撞非常少见,即使我处理效率低下也没关系”?
  • 停止使用 md5!它是一个损坏的散列函数,并使得输出 LESS RANDOM,因为它的 PRNG 输出存在问题。
  • @Rook:+1。人们通常使用它是因为“它比库中的其他所有东西都快得多,而且我真的不需要安全哈希......”但是当然,如​​果你真的不需要安全哈希,你不需要' 也不需要 MD5,那为什么不首先做 urlsafe_b64encode(os.urandom(6)) 呢?
  • @Rook 在这里使用 MD5,虽然没有意义,但不会增加碰撞率。
  • @PhilGan:你用什么语言写的?

标签: python hash random cryptography


【解决方案1】:

您当前的方法应该足够安全,但您也可以查看uuid 模块。例如

import uuid

print str(uuid.uuid4())[:8]

输出:

ef21b9ad

【讨论】:

  • 注意uuid4的“4”部分非常重要。例如,“uuid1”会根据主机 ID 生成带有固定前缀的字符串。
  • 这给了我 100k 个名字的 3 次冲突。这可能是一种不好的方法,因为您只使用了 16 个字符
  • 我在别处读到截断 uuid 不是生成短随机字符串的好方法。
  • @zahory:部分原因是 BoppreH 提出的观点。您必须先了解每种类型的 UUID 的工作原理,然后才能知道以不同方式截断它们的安全性。
  • 43981 次在百万列表中尝试时发生冲突。
【解决方案2】:

哪种方法冲突少、速度更快、更易阅读?

TLDR

random_choice 最快碰撞次数较少,但 IMO 稍微难以阅读

可读性最强的shortuuid_random,但它是一个外部依赖项,速度稍慢,并且有 6 倍的冲突。

方法


alphabet = string.ascii_lowercase + string.digits
su = shortuuid.ShortUUID(alphabet=alphabet)

def random_choice():
    return ''.join(random.choices(alphabet, k=8))

def truncated_uuid4():
    return str(uuid.uuid4())[:8]

def shortuuid_random():
    return su.random(length=8)

def secrets_random_choice():
    return ''.join(secrets.choice(alphabet) for _ in range(8))

结果

所有方法都从 abcdefghijklmnopqrstuvwxyz0123456789 字母表中生成 8 个字符的 UUID。碰撞是从单次运行中计算的,有 1000 万次平局。时间以秒为单位报告为平均函数执行 ± 标准偏差,均计算超过 100 次 1,000 次绘制。 Total time 是碰撞测试的总执行时间。

random_choice: collisions 22 - time (s) 0.00229 ± 0.00016 - total (s) 29.70518
truncated_uuid4: collisions 11711 - time (s) 0.00439 ± 0.00021 - total (s) 54.03649
shortuuid_random: collisions 124 - time (s) 0.00482 ± 0.00029 - total (s) 51.19624
secrets_random_choice: collisions 15 - time (s) 0.02113 ± 0.00072 - total (s) 228.23106

注意事项

  • 默认的shortuuid 字母具有大写字符,因此产生的冲突更少。为了公平比较,我们需要选择与其他方法相同的字母表。
  • secrets 方法 token_hextoken_urlsafe 虽然可能更快,但具有不同的字母表,因此不符合比较条件。
  • alphabet 和基于类的shortuuid 方法被分解为模块变量,从而加快了方法的执行速度。这不应影响 TLDR。

完整的测试细节

import random
import secrets
from statistics import mean
from statistics import stdev
import string
import time
import timeit
import uuid

import shortuuid


alphabet = string.ascii_lowercase + string.digits
su = shortuuid.ShortUUID(alphabet=alphabet)


def random_choice():
    return ''.join(random.choices(alphabet, k=8))


def truncated_uuid4():
    return str(uuid.uuid4())[:8]


def shortuuid_random():
    return su.random(length=8)


def secrets_random_choice():
    return ''.join(secrets.choice(alphabet) for _ in range(8))


def test_collisions(fun):
    out = set()
    count = 0
    for _ in range(10_000_000):
        new = fun()
        if new in out:
            count += 1
        else:
            out.add(new)
    return count


def run_and_print_results(fun):
    round_digits = 5
    now = time.time()
    collisions = test_collisions(fun)
    total_time = round(time.time() - now, round_digits)

    trials = 1_000
    runs = 100
    func_time = timeit.repeat(fun, repeat=runs, number=trials)
    avg = round(mean(func_time), round_digits)
    std = round(stdev(func_time), round_digits)

    print(f'{fun.__name__}: collisions {collisions} - '
          f'time (s) {avg} ± {std} - '
          f'total (s) {total_time}')


if __name__ == '__main__':
    run_and_print_results(random_choice)
    run_and_print_results(truncated_uuid4)
    run_and_print_results(shortuuid_random)
    run_and_print_results(secrets_random_choice)

【讨论】:

【解决方案3】:

您是否有不能使用tempfile 生成名称的原因?

mkstempNamedTemporaryFile 等函数绝对保证为您提供唯一的名称;没有任何基于随机字节的东西会给你。

如果由于某种原因您实际上还不想创建文件(例如,您正在生成要在某个远程服务器上使用的文件名或其他东西),那么您就不能完全安全,但mktemp 仍然是比随机名称更安全。

或者只是将一个 48 位计数器存储在某个“足够全局”的位置,这样您就可以保证在发生冲突之前经历完整的名称循环,并且您还可以保证知道何时会发生冲突。

它们都比阅读urandom 和执行md5 更安全、更简单、更高效。

如果您确实想生成随机名称,''.join(random.choice(my_charset) for _ in range(8)) 也将比您所做的更简单,更高效。甚至urlsafe_b64encode(os.urandom(6)) 和 MD5 哈希一样随机,而且更简单、更高效。

加密随机性和/或加密散列函数的唯一好处是避免可预测性。如果这对您来说不是问题,为什么要为此付费?如果您确实需要避免可预测性,您几乎肯定需要避免竞争和其他更简单的攻击,因此避免mkstempNamedTemporaryFile 是一个非常糟糕的主意。

更不用说,正如 Root 在评论中指出的那样,如果您需要安全性,MD5 实际上并没有提供它。

【讨论】:

  • 我正在重命名正在上传到同一个地方的文件,所以没有使用tempfileUuid4 对任何事情都足够独特,但太长了。我很确定 8 个字符应该足以存储数千个名称而不会发生冲突,但我不知道该怎么做。通过散列urandom,我试图获得更好的随机性。
  • @zahory UUID 按什么标准来说“太长”?
  • @NickJohnson 太长了,太漂亮了:) 长文件名在列表中看起来很难看。正如我之前写的那样,8 个字符应该足以存储数以百计的随机唯一名称而不会发生可能的冲突,我只是不知道增加不可能性的最佳方法是什么。
  • @zahory 如果你想缩短,你应该使用计数器。根据生日悖论,在给定相同数量的标识符的情况下,随机生成的 ID 必须至少是顺序 ID 的两倍。
  • 根据我的测试,''.join([random.choice(string.ascii_letters+string.digits+'-_') for ch in range(8)]) 在数百万个字符串中不会发生冲突,并且也比编码 urandom() 更有效。我决定走这条路。
【解决方案4】:

您可以尝试shortuuid 库。

安装:pip install shortuuid

那么就这么简单:

> import shortuuid
> shortuuid.uuid()
'vytxeTZskVKR7C7WgdSP3d'

【讨论】:

  • 测试了shortuuid.uuid()[:8] 1 亿次抽奖并获得了 0 次冲突。但似乎牺牲了速度
  • 只是为了好玩。 10 亿次抽签导致 55 次碰撞
  • 在我看来这应该是选择的答案!
  • 我猜这是一个 Base64 编码的 UUID,去掉了填充 =
  • 我看到他们实际上是在使用 Base57 而没有 Base64 中的任何非字母数字。整洁!
【解决方案5】:

从 Python 3.6 开始,您可能应该使用 secrets 模块。 secrets.token_urlsafe() 似乎很适合您的情况,并且保证使用加密安全的随机源。

【讨论】:

    【解决方案6】:

    我正在使用 hashids 将时间戳转换为唯一 ID。 (如果需要,您甚至可以将其转换回时间戳)。

    这样做的缺点是如果你创建的 id 太快,你会得到一个副本。但是,如果您在生成它们之间有时间,那么这是一个选项。

    这是一个例子:

    from hashids import Hashids
    from datetime import datetime
    hashids = Hashids(salt = "lorem ipsum dolor sit amet", alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
    print(hashids.encode(int(datetime.today().timestamp()))) #'QJW60PJ1' when I ran it
    

    【讨论】:

      【解决方案7】:

      你可以试试这个

      import random
      uid_chars = ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u',
                   'v', 'w', 'x', 'y', 'z','1','2','3','4','5','6','7','8','9','0')
      uid_length=8
      def short_uid():
          count=len(uid_chars)-1
          c=''
          for i in range(0,uid_length):
              c+=uid_chars[random.randint(0,count)]
          return c
      

      例如:

      print short_uid()
      nogbomcv
      

      【讨论】:

        【解决方案8】:

        最快的确定性方法

        import random
        import binascii
        e = random.Random(seed)
        binascii.b2a_base64(random.getrandbits(48).to_bytes(6, 'little'), newline=False)
        

        最快系统随机法

        import os
        import binascii
        binascii.b2a_base64(os.urandom(6), newline=False)
        

        网址安全方法

        使用os.urandom

        import os
        import base64
        base64.urlsafe_b64encode(os.urandom(6)).decode()
        

        使用random.Random.choices(缓慢但灵活)

        import random
        import string
        alphabet = string.ascii_letters + string.digits + '-_'
        ''.join(random.choices(alphabet, k=8))
        

        使用random.Random.getrandbits(比random.Random.randbytes快)

        import random
        import base64
        base64.urlsafe_b64encode(random.getrandbits(48).to_bytes(6, 'little')).decode()
        

        使用random.Random.randbytes (python >= 3.9)

        import random
        import base64
        base64.urlsafe_b64encode(random.randbytes(6)).decode()
        

        使用random.SystemRandom.randbytes (python >= 3.9)

        import random
        import base64
        e = random.SystemRandom()
        base64.urlsafe_b64encode(e.randbytes(6)).decode()
        

        如果python >= 3.9,不推荐random.SystemRandom.getrandbits,因为与random.SystemRandom.randbytes相比,它需要2.5倍的时间并且更复杂。

        使用secrets.token_bytes (python >= 3.6)

        import secrets
        import base64
        base64.urlsafe_b64encode(secrets.token_bytes(6)).decode()
        

        使用secrets.token_urlsafe (python >= 3.6)

        import secrets
        secrets.token_urlsafe(6) # 6 byte base64 has 8 char
        

        进一步讨论

        python3.9中的secrets.token_urlsafe实现

        tok = token_bytes(nbytes)
        base64.urlsafe_b64encode(tok).rstrip(b'=').decode('ascii')
        

        由于 ASCII 字节 .decode().decode('ascii') 快, 而.rstrip(b'=')nbytes % 6 == 0 时无用。

        base64.urlsafe_b64encode(secrets.token_bytes(nbytes)).decode() 更快 (~20%)。

        在 Windows10 上,基于字节的方法在 nbytes=6(8 char) 时快 2 倍,在 nbytes=24(32 char) 时快 5 倍。

        在 Windows 10(我的笔记本电脑)上,secrets.token_bytes 花费与 random.Random.randbytes 相似的时间,而base64.urlsafe_b64encode 花费的时间比随机字节生成要多。

        在 Ubuntu 20.04(我的云服务器,可能缺少熵)上,secrets.token_bytes 花费的时间比 random.Random.randbytes 多 15 倍,但与 random.SystemRandom.randbytes 花费的时间相似

        由于secrets.token_bytes 使用random.SystemRandom.randbytes 使用os.urandom(因此它们完全相同),如果性能至关重要,您可以将secrets.token_bytes 替换为os.urandom

        在 Python3.9 中,base64.urlsafe_b64encodebase64.b64encodebytes.translate 的组合,因此要多花约 30% 的时间。

        random.Random.randbytes(n)random.Random.getrandbits(n * 8).to_bytes(n, 'little') 实现,因此慢了 3 倍。 (不过random.SystemRandom.getrandbits是用random.SystemRandom.randbytes实现的)

        base64.b32encodebase64.b64encode 慢得多(6 字节为 5 倍,480 字节为 17 倍),因为base64.b32encode 中有很多 python 代码,但base64.b64encode 只需调用binascii.b2a_base64(C 实现) .

        但是base64.b64encode中有一个python分支语句if altchars is not None:,在处理小数据时会引入不可忽略的开销,binascii.b2a_base64(data, newline=False)可能会更好。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2018-11-26
          • 2017-12-09
          • 1970-01-01
          • 2022-10-19
          • 2013-05-20
          • 1970-01-01
          • 1970-01-01
          • 2020-08-11
          相关资源
          最近更新 更多