深入了解:随机数简介

[InDepth#22] Random 通过 PHP 的一个简单帮助包生成多种不同格式的加密安全随机值。

深入了解:随机数简介
💡
作为 2023 年年底的特别礼物,并且由于我们正在深入研究我的新开源包,因此我删除了付费墙,因此本文可供所有人免费阅读。

如果你喜欢这篇文章,或者我的包裹, 请考虑成为付费订阅者 每月收到这些深入文章并支持我在 Laravel 和 PHP 社区内的安全工作。

我在安全审计期间(尤其是在较旧的代码库上)经常遇到的问题是不安全的随机性,通常是在需要安全性的地方。它通常使用某种形式 rand() ,经常注入内部 md5() 生成随机散列,结合 str_shuffle() 生成新密码,或用于制作一次性密码 (OTP) rand(100_000, 999_999)

问题是 rand() 不是 加密安全 ,而且都不是 mt_rand() , mt_srand() , str_shuffle() , array_rand() 或其他 不安全的功能 可用在 PHP 中。这是我讨论过的一个话题 之前很多次 - 所以我希望你已经意识到这些方法并不安全 - 但是,我们不能简单地宣布这些方法不安全,然后放下麦克风走开。相反,我们需要提供安全的替代方案 - 因此,与其简单地说“不要使用 rand() 这样”,我们可以说“这是一个您可以使用的安全方法”!

这就是我创造的原因 随机的

什么是 随机的

随机的 是我构建的一个新的 Composer 包,旨在提供安全且易于使用的常见随机函数实现。它完全与框架无关,可在 PHP 7.1 及更高版本上运行,唯一的依赖项是优秀的 php-随机-polyfill 经过 安东·斯米尔诺夫

您可以在以下位置找到它:

可以按照常规方式安装:

composer require valorin/random

我希望它能像嵌入式工具包一样易于使用,因此所有方法都可以通过 `\Valorin\Random\Random` 班级 1 ,这使其具有简单且易读的语法:

$number = Random::number(1, 100);
$password = Random::password();

目的是任何想要摆脱不安全实现的人都可以要求该包并放入一行:

// Original Insecure Version
$otp = rand(100_000, 999_999);

// Secure Version
$otp = Random::otp();
// Original Insecure Version
function generatePassword($length = 10) {
    $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
    $charactersLength = strlen($characters);
    $password = '';
    for ($i = 0; $i < $length; $i++) {
        $password .= $characters[rand(0, $charactersLength - 1)];
    }
    return str_shuffle($password);
}

// Secure Version
function generatePassword($length = 16) {
    return Random::password($length);
}

另一个目标是支持旧版本的 PHP,因为即使旧版本不再受支持 2 ,团队升级仍然需要时间。拥有一个可以立即投入使用的工具包来快速修复不安全的随机性是一个巨大的好处,并且使这些人不必尝试自己的安全实现或匆忙升级。我选择了 PHP 7.1,因为这是 polyfill 支持的最早版本,而 7.0 被证明太难让一切正常工作。

介绍得已经足够了,让我们来探索每种方法!

随机整数

从基础开始,我们可以使用以下方法生成随机数:

$number = Random::number(int $min, int $max): int;
> $number = Random::number(1, 1000);
= 384

因为我们已经拥有了奇妙的 `random_int()` 可用的功能,这是您想要使用的唯一实际原因 `Random::number()` 3 如果您使用自定义引擎(例如用于播种随机数),我们将在下面介绍这些内容。

我们讨论过 random_int() 在过去 密码安全随机性

随机一次性密码(数字固定长度的 OTP)

一次性密码 (OTP)、Passcode、Nonce……发送给用户的固定长度数字字符串有多个名称 4 用于验证目的。

我们可以使用以下方法生成它们:

$otp = Random::otp(int $length): string;
> $otp = Random::otp(6);
= "001421"

经常会碰到用类似这样的代码生成的 OTP rand(100_000, 999_999) 然而,这种方法存在双重缺陷,因为它使用了不安全的随机性,并且损失了约 10% 的熵( 000,000 - 099,999 范围)。

我建议的解决办法,正如 魔法电子邮件 , 特征 random_int()左垫 但是我想让它更简单,并在一个助手中完成所有的工作,这样人们就不需要自己实现任何工作了。

该方法添加零( 0 ) 作为前缀并返回一个字符串,以防止零从前面被删除。

我最初将零前缀设为可自定义,但考虑到该方法的用途,使用非零前缀毫无意义。该方法还支持比整数限制更长的数字 - 不确定为什么需要它,但如果您需要它,它就在那里。

关于这个命名有一个问题—— otp() 准确地表示它的功能而不暗示额外的行为?我很难想出一个让我感到舒服的。 如果您有任何建议,请在评论中告诉我!

随机字符串

现在我们已经介绍了数字,我们还需要生成具有不同字符组合的随机字符串。

我们可以使用主 string() 方法:

$string = Random::string(
    int $length = 32,
    bool $lower = true,      // toggle lower case letters
    bool $upper = true,      // toggle upper case letters
    bool $numbers = true,    // toggle numbers
    bool $symbols = true,    // toggle special symbols
    bool $requireAll = false // require at least one character from each type
): string;

还有针对常见用例的包装器:

// Random letters only
$string = Random::letters(int $length = 32): string;

// Random letters and numbers (i.e. a random token)
$string = Random::token(int $length = 32): string;

// Random letters, numbers, and symbols (i.e. a random password).
$string = Random::password(int $length = 32, bool $requireAll = false): string;

// Random alphanumeric token string with chunks separated by dashes, making it easy to read and type.
$password = Random::dashed(int $length = 25, string $delimiter = '-', int $chunkLength = 5): string;
> $string = Random::string();
= "QS`#z&/kP4x/R*gc9MomOMD]Q"&Ry62Z"

> $letters = Random::letters();
= "bDIZrdAOdMgxXnnLTrobaHVLMGaWeDgj"

> $token = Random::token();
= "Jz5QSwuUW7cF7J5flYqyrhSQEfZrvWdV"

> $password = Random::password();
= "gB#'JhYc$1YWMOlN"

> $password = Random::dashed();
= "91m3K-TttUb-tBwdV-C5Llm-IngAC"

这种方法是我构建此包的原因之一。我经常看到不安全的密码生成函数,做各种令人担忧的事情(例如我上面使用的示例)。

我试图将它构建得尽可能灵活 - 允许您打开和关闭每种字符类型,并且可选地要求每种类型至少有一个字符(使用 $requireAll )。我还想支持自定义字符集(稍后会详细介绍!)。这些都是我在 Laravel 的密码生成器 ,所以我想支持他们。

注意:我特意在密码字符集中避免使用空格字符。我知道有些人喜欢添加它,但我个人认为它增加了不必要的复杂性(不能是第一个或最后一个)和混乱(不完整的复制粘贴、自动换行等),没有真正的好处(它只是另一个字符)。

我添加了辅助包装器来涵盖常见用例,因此您不需要记住主要的所有参数 string() 方法,并使代码在使用时读起来更美观。

破折号密码

对于需要生成随机密码供用户阅读和输入的情况,使用分隔符将较长的字符串拆分为较小的块会很有帮助。这就是 dashed() 助手会做:

// Random::dashed(int $length = 25, string $delimiter = '-', int $chunkLength = 5): string;

> $password = Random::dashed();
= "91m3K-TttUb-tBwdV-C5Llm-IngAC"

您可以调整长度、分隔符和块长度以生成正确格式的字符串:

> $password = Random::dashed($length = 12, $delimiter = '.', $chunkLength 3);
= "6Jl.6sV.iFA.Hd3"

$requireAll

如果你需要每种类型至少一个角色,你可以切换 $requireAll 范围。

例如:

// Only uppercase and symbols were randomly picked
> $password = Random::string(length: 5, requireAll: false);
= ")OR`{"

// At least one of each: lower, upper, number, symbols
> $password = Random::string(length: 5, requireAll: true);
= "d4)T-"

尽管现代密码建议不再需要通过字符类型来提高复杂性,但我仍将其作为一个选项。 5 ,因为一些进展较慢的合规性和公司规则可能仍要求在生成密码时将其作为选项。默认情况下,它是禁用的,但如果您需要,可以使用它。

自定义字符集

如果需要覆盖使用的特定字符集 string() ,您可以这样做:

// Override just symbols
$generator = Random::useSymbols(['!', '@', '#', '$', '%', '^', '&', '*', '(', ')'])->string();
= "UZWS2KYiK)(XECWLQbs9yYveH#@gwVpo"

// Override everything
$generator = Random::useLower(range('a', 'f'))
    ->useUpper(range('G', 'L'))
    ->useNumbers(range(2, 6))
    ->useSymbols(['!', '@', '#', '$', '%', '^', '&', '*', '(', ')']);

$string = $generator->string(
    $length = 32,
    $lower = true,
    $upper = true,
    $numbers = true,
    $symbols = true,
    $requireAll = true
);
= "fG22aIG@%fad25b264)fe(b5G3JKe46("

这些 use*() 方法返回 \Valorin\Random\Generator ,它具有所有的随机方法,但会遵守自定义字符集。

这使您可以完全控制生成的字符串中包含哪些字符 - 如果您的系统支持有限的符号,这将非常有用 6 ,或者你想生成一个十六进制字符串?

> $string = Random::useUpper(range('A', 'F'))->string(
    $length = 32,
    $lower = false,
    $upper = true,
    $numbers = true,
    $symbols = false,
    $requireAll = true
);
= "5C65DF598AD08CF94D129040F2668025"

随机排列数组、字符串或集合

除了生成随机性之外,您还需要安全地随机排列数组、字符串和集合:

$shuffled = Random::shuffle(
    array|string|\Illuminate\Support\Collection $values,
    bool $preserveKeys = false
): array|string|\Illuminate\Support\Collection;
> $shuffled = Random::shuffle(['a', 'b', 'c', 'd', 'e']);
= [
    "e",
    "b",
    "a",
    "d",
    "c",
  ]

> $shuffled = Random::shuffle(['a', 'b', 'c', 'd', 'e'], $preserveKeys = true);
= [
    3 => "d",
    2 => "c",
    1 => "b",
    4 => "e",
    0 => "a",
  ]

> $string = Random::shuffle('abcde');
= "bdcae"

> $collection = new Collection(['a', 'b', 'c', 'd', 'e']);
> $shuffled = Random::shuffle($collection);
> $shuffled->toArray();
= [
    "a",
    "c",
    "e",
    "d",
    "b",
  ]

这种方法是我想要构建的另一个原因 随机的 在我审计期间(以及审计之外的许多审计)看到的几乎每一次改组实施 7 ) 在某种程度上是不安全的。大多数情况下,只需使用 PHP 的原始 `shuffle()` 方法 - 但所有这些都使用不安全的随机性。人们不知道如何安全地打乱值,我想改变这一点。

PHP 8.2 的新特性 \Random\Randomizer::shuffleArray()\Random\Randomizer::shuffleBytes() 助手给我们提供了安全的洗牌,所以我把它们包在里面 随机的 ,并增加了对 Collections 的支持。

我添加了对 Laravel 集合的支持,因为 `shuffle()` Collections 上的方法是 不安全,不应使用 8 。我会尝试在 v11 中修复这个问题,但旧版本仍然包含不安全的 shuffle,因此拥有一个可以轻松处理它们的工具包很有用。

选择 X 个物品或角色

继 shuffle 之后,另一个常见用例是从一个值中选择一个或多个项目或字符。

$picks = Random::pick(
    array|string|\Illuminate\Support\Collection $values,
    int $count
): array|string|\Illuminate\Support\Collection;

$pick = Random::pickOne(
    array|string|\Illuminate\Support\Collection $values
): array|string|\Illuminate\Support\Collection;
// Pick from array
> $picked = Random::pick(['a', 'b', 'c', 'd', 'e'], 1);
= "c"

> $picked = Random::pick(['a', 'b', 'c', 'd', 'e'], 3);
= [
    "b",
    "a",
    "c",
  ]

> $picked = Random::pickOne(['a', 'b', 'c', 'd', 'e']);
= "d"

// Pick from string
> $picked = Random::pick('abcde', 1);
= "a"

> $picked = Random::pick('abcde', 3);
= "dbc"

> $picked = Random::pickOne('abcde');
= "e"

// Pick from Collection
> $collection = new Collection(['a', 'b', 'c', 'd', 'e']);
> $picked = Random::pick($collection, 1);
= "d"

> $picked = Random::pick($collection, 3);
> $picked->toArray();
= [
    "b",
    "c",
    "a",
  ]

> $picked = Random::pickOne($collection, 1);
= "a"

如果你有增量键,随机挑选项目是相当容易的,但我经常看到它使用 rand() , 或者 shuffle() .这些功能使得安全地挑选物品成为一件简单的操作。

我选择在以下情况下从数组和集合中返回单个选定的值: $count = 1 ,因为这样可以避免从数组中提取单个值。此外,如果你还没有弄清楚, pickOne() 方法是 pick($values, $count = 1)

出于同样的原因,这里包含了对集合的支持 shuffle() - 现有的 Laravel 方法并不安全。

使用特定的 \Random\Engine

随机的 使用 PHP 8.2 Random\Randomizer 内部实现所有的随机性,这意味着你可以指定一个自定义的 \Random\Engine 为随机性提供动力。

随机的 支持这一点 use() 方法,构建一个自定义 Generator 围绕随机引擎,允许您使用生成器上的所有方法:

$generator = Random::use(\Random\Engine $engine): \Valorin\Random\Generator; 
> $generatorOne = Random::use(new \Random\Engine\Mt19937($seed = 3791));
> $generatorTwo = Random::use(new \Random\Engine\Mt19937($seed = 3791));

> $number = $generatorOne->number(1, 1000);
= 65

> $number = $generatorTwo->number(1, 1000);
= 65

> $password = $generatorOne->password();
= "MOz:^U/Hc?PsZD[e"

> $password = $generatorTwo->password();
= "MOz:^U/Hc?PsZD[e"

返回的 Generator 对象将使用提供的 Engine,独立于任何其他生成器或主要 Random 助手。这允许您在特定对象中设置种子随机性等操作,而不会影响应用程序的任何其他部分。

这就是为什么 我尝试修复不安全的随机性 2023 年 2 月在 Laravel 中发布的失败。许多人正在使用 srand() 在他们的应用程序中,改变随机性的实现会将输出从预测值改回随机值,从而破坏一切……

这只是一个问题,因为我试图在 v10 发布后添加它,所以这是一个小版本中的重大更改。我计划在 v11 发布之前修复它,这样就可以记录重大更改,使用自定义种子的人可以在升级过程中更新他们的代码。

除非您特别需要某个自定义引擎,或者您必须播种随机值,否则设置自定义引擎可能不是您需要使用的功能。但如果您需要,它就在那里。

未来 随机的

现在我们已经介绍了所有当前功能,接下来 随机的

我不知道……感觉功能齐全,但我认为在添加之前 dashed() 助手 - 我在写这篇文章的时候就这么做了!所以我认为这将是一个改进 API 并添加助手和支持特定随机性用例的案例。

我希望它可以成为您需要做一些随机性的事情(而不仅仅是生成一个随机数)时的首选软件包。

概括

我希望你能从我们的深入研究中学到一些东西 随机的 ,它让你思考如何在你的应用中使用随机性。如果你有任何不安全的东西,为什么不加入进来 随机的 看看它是否能满足你的需要。🙂

请查看以下网址的代码 GitHub ,并留意我可能忽略的任何弱点或错误。另外,如果您对如何改进它有任何建议,请告诉我!


  1. 顺便说一下,我特别想对 `Random` 类,而不是类似的东西 `Factory` 或者 `Generator` ,我知道有些包会用它作为它们的主要类。我发现你最终会给导入设置别名,以从代码中删除通用术语。

  2. 我总是在审计中将它们标记为问题!

  3. 你可能会发现 `Random::number()` 你的代码看起来更好!😉

  4. 通过短信、电子邮件等

  5. 看: https://www.troyhunt.com/passwords-evolved-authentication-guidance-for-the-modern-era /

  6. 许多人仍受困于另一项遗留要求。

  7. 例如 Laravel 的 shuffle 方法! 我试图在 2023 年初解决这个问题 ,我很快会再次尝试 Laravel v11。

  8. 那么 `Arr`