从加密字节生成 PIN

信息安全 密码 sql服务器
2021-08-26 11:06:33

我们的一个应用程序使用 SQL Server 作为后端,并且该应用程序需要一个 4 位数的数字 PIN 才能创建使用密钥(或多或少)的员工。系统本身默认所有 PIN 为 0000,因为如果您使用的设备没有键盘,这就是“不需要 PIN”的神奇值。

我们遇到了员工没有更新他们的 PIN 的问题(并且厌倦了等待供应商纠正问题),因此我们使用 SQL Server 生成我们自己的 PIN,使用计划作业更新任何 0000 的 PIN。

PIN不需要是安全的(毕竟 4 位数的 PIN 有多安全),它们只需要不重要。但是,没有理由不考虑安全问题,我喜欢花时间了解我正在做的事情,至少是胜任的:

当前的解决方案(不是我!)是:

replace(str(round(rand(checksum(newid()))*10000,0),4), ' ','0')

这有几个问题。当然,密码函数rand()也不newid()是,但实际上需要更改的问题是,****由于str()函数的怪癖,这偶尔会产生一个 pin of。

现在,SQL Server 确实具有crypt_gen_random()返回固定字节数的安全加密生成数字的功能。这让我觉得这样的事情应该有效:

format(cast(crypt_gen_random(2) as int) % 10000,'0000')

我看到的问题crypt_gen_random(2)基本上是“在 0 到 65535 之间选择一个数字”。然而,在该空间中,模 10000 存在偏差。0 到 5535 之间的每个结果有 7 个实例,但 5536 和 9999 之间的每个结果只有 6 个实例。相差 16%。

现在,我可以想到一个可能的简单修复方法,但我不确定是否有首选方法。我不是安全程序员。可以简单地增加生成的字节数吗?

format(cast(crypt_gen_random(7) as bigint) % 10000,'0000')

[注意:我使用 bigint 而不是 int,因为它支持长达 8 个字节的数字。我只生成 7 个字节而不是 8 个字节来防止负数。]

如果我生成 7 个字节而不是 2 个字节,我将生成一个介于 0 和 72057594037927935 (16^14) 之间的数字。问题仍然存在——在 0 和 7935 之间有 7205759403793 个值,但在 7936 和 9999 之间只有 7205759403792——但稀释程度如此之大,这并不重要。我们大约是 10^-12。

不过,就像我说的,我不是安全程序员。我是一名 DBA、分析师和管理员,我对安全编程的所有了解是,我没有资格知道我何时会犯安全错误。

有没有更好的方法来做到这一点?


我的 T-SQL 解决方案基本上看起来像这样:

declare @candidate_pin int = cast(crypt_gen_random(2) as int)
while @candidate_pin not between 1 and 9999
    set @candidate_pin = cast(crypt_gen_random(2) as int)

select format(@candidate_pin, '0000') as [pin]

我希望避免使用过程逻辑而不是更简单的声明性语句,但我不会尝试将其强制为递归 CTE 或其他声明性解决方案。

我并不特别关心这里的性能。如果我们在一个季度内需要 100 个新 PIN,就会发生一些奇怪的事情。

2个回答

是的,您至少需要 14 位才能生成 0 到 9999 之间的数字。

为了避免偏差,您可以使用良好的随机性源生成 14 位随机数据​​。如果结果是 <= 9999,那么您可以将其用作您的 PIN。如果结果更大,请将其丢弃并重试。

这会浪费多少?

在最好的情况下,您的位将与您的答案空间完美对齐,并且您不会浪费任何东西。您可以生成 0 到 255 之间的随机值,您所要做的就是生成 8 个随机位。假设您的随机生成器很好,任何结果都是有效的。

在最坏的情况下,您需要一个介于 0 和 256 之间的随机数,这意味着您需要 9 位,因此有 512 个可能的值。在大约 50% 的情况下,您将不得不丢弃结果。

在我们的示例中,我们的 13 位下限只会给我们 8196 个可能的值,而我们的下一个可能目标是 16384 个可能的值。鉴于您想要 10000 个可能的值,您的可能空间比目标空间大一倍半以上。

但不要担心,您使用的数据量相对较少。如果你查询 7 个随机字节,你会得到 56 个随机位,这意味着 4 次“尝试”以获得“好”值。即使我们假设你有 50% 的机会(最坏的情况)得到一个好的值,在 4 次尝试中你有 93.75% 的机会生成一个好的 PIN。如果运气不好,您将不得不再查询 7 个字节,这将使您有 99.6% 的机会选择一个好的 PIN。

凉爽的!我可以有一个演示吗?

function GeneratePIN()
{
    // The chance to fail after 1000 rounds is 7.5 * 10^-1203
    for(int round = 0; round < 1000; round++)
    {
        // We generate 7 random bytes, which would give us 4 "tries"
        ulong randomness = SecureRandom.GenerateBytes(7);

        // We can iterate this 4 times before our 7 bytes are exhausted
        for (int try = 0; try < 4; try++)
        {
            ushort possiblePIN = randomness & 0x3FFF; //0x3fff is 0011 1111 1111 1111 in binary, so 14 bits set to 1

            // If we succeed, we return the PIN. Else we try again
            if (possiblePIN < 10000)
            {
                return possiblePIN;
            }
            else
            {
                // =>> is the right shift, assign operator
                // This will shift randomness 14 bits to the right
                randomness =>> 14;
            }
        }

        // If we reached this part, we were unlucky and need to try again.
    }

    // If we reach this part, we flipped a coin 1000 times and always got heads.
    // Maybe buy a lottery ticket
    throw new Exception("Buy a lottery ticket!");
}

首先,我完全同意 MechMK1 的正确算法来生成 0 到 10000 之间的加密安全均匀分布随机数。但是:

没有理由不考虑安全问题

让我们。0 到 10000 之间的随机数包含13.29 位的熵。0 到 2^14 % 10000 之间的随机数包含 13.22 位熵,我们损失了 0.067。这相当于将引脚数从 10000 减少到 9546。继续计算:

  • CSPRNG(0, 2^14) % 10000 相当于 CSPRNG(0, 9546),0.067 位熵丢失
  • CSPRNG(0, 2^16) % 10000 相当于 CSPRNG(0, 9971),0.004 位熵丢失
  • CSPRNG(0, 2^24) % 10000 相当于 CSPRNG(0, 9999.99964) [现在差小于 1 个键,所以纯粹是统计性质],丢失了 0.00000005 位熵
  • CSPRNG(0, 2^32) % 10000 相当于 CSPRNG(0, 9999.999999995),熵损失了 0.0000000000008 位 [此处的数量级精度,恐怕浮点舍入可能比计算发挥更大的作用]

换句话说,format(cast(crypt_gen_random(3) as int) % 10000,'0000')通过使用节省的实施时间来检查管理员的密码是否以某种方式留空,从而使用并重新获得丢失的 51 毫微位安全性。

所有这一切都在说:

  • 13.29 位密钥首先提供了可笑的安全性。我会说只是将它们保留为 0000,然后将节省的时间和脑力用于执行 tiiiiny 内部安全审计?我敢打赌,您会在更重要的地方发现不太安全的密码。
  • KISS 确实有效