firehed / security
PHP 的安全工具
Requires
- php: ^7.1 || ^8.0
Requires (Dev)
- phpstan/phpstan: ^0.12.98
- phpstan/phpstan-strict-rules: ^0.12.11
- phpunit/phpunit: ^7.0 || ^8.0 || ^9.0
- squizlabs/php_codesniffer: ^3.6
Suggests
- paragonie/constant_time_encoding: Contains a secure base32-encoder, useful for generating QRCodes for TOTP registration
README
秘密:隐藏字符串
Secret 类存在是为了隐藏敏感字符串。这有助于避免在回溯中意外泄露数据,回溯通常写入日志(包括远程日志服务)或 - 如果环境配置为开发模式 - 显示给最终用户。
被隐藏的值不是加密的;它只是与随机生成的密钥进行XOR
操作,并在请求结束时丢弃。这意味着秘密可能以任何方式跨请求持续存在;只有它们的底层值(必须手动揭示)。
API非常简单
$masked_string = new Firehed\Security\Secret('string to mask'); $unmasked_string = $masked_string->reveal();
获取底层值的其他任何方法都将返回"<secret>"
(其中可能检测到导出)或隐藏的值(可能是二进制垃圾)。
require 'vendor/autoload.php'; $x = new Firehed\Security\Secret('asdf'); echo $x; // <secret> print_r($x); // Firehed\Security\Secret Object // ( // [secret] => <secret> // ) var_dump($x); // class Firehed\Security\Secret#2 (1) { // public $secret => // string(8) "<secret>" // } var_export($x); // Firehed\Security\Secret::__set_state(array( // 'value' => '�AZ�', // )) var_dump($x->reveal()); // string(4) "asdf" try { doSomethingThatThrows($x); } catch (Throwable $ex) { echo $ex; } // Stack trace: // #0 /some/file.php(15): doSomethingThatThrows(Object(Firehed\Security\Secret)) // #1 {main}Exception: Some message in /some/file.php:9
常见问题解答
这会泄露隐藏的数据吗?
不会直接泄露,但您必须在某些时候揭示数据以使用它。例如:new PDO(...)
或 new mysqli(...)
。如果消耗揭示的秘密的函数抛出异常,秘密仍然可能被揭示。这无法在用户空间内解决 :(
这是加密的替代品吗?
不是,它不是。这只是一个数据混淆,并没有加密数据。
我应该何时以及如何使用这个?
这是一个用于存储临时敏感数据的优秀包装器;例如,来自 POST 请求的用户密码可以一直包装到实际比较。更具体地说
class User { function isPasswordCorrect(string $password): bool { return password_verify($password, $this->pw_hash); } } // ... if ($user->isPasswordCorrect($_POST['password'])) { ... }
变为
use Firehed\Security\Secret; class User { function isPasswordCorrect(Secret $password): bool { return password_verify($password->reveal(), $this->pw_hash); } } // ... if ($user->isPasswordCorrect(new Secret($_POST['password']))) { ... }
它也适用于存储 API 密钥、连接密码等
$container = new MyDIContainer(); $container['db_username'] = getenv('DB_USER'); $container['db_password'] = new Secret(getenv('DB_PASS')); $container['database'] = function ($c) { return new PDO( 'mysql:host=127.0.0.1;db=test', $c['db_username'], $c['db_password']->reveal(), ); };
(尽管如果您已经正确使用 DI,则其价值会降低)
如果我在 $_SESSION 中放置一个 Secret 会发生什么?
不要这样做。 它将在请求的其余部分按预期工作,但读取它的后续请求将得到垃圾。
为什么使用这个而不是传递加密字符串?
- 无需管理密钥
- 无外部依赖(OpenSSL、libsodium 等)
- 简单的 API
- 简单易懂的实现
- 加密字符串仍然需要在某处存储密钥材料
如果泄露了被隐藏的字符串,能否将其恢复?
是与否。
它容易受到已知明文攻击,因此如果攻击者获得了已知和控制下的字符串以及他们试图从同一请求中捕获的字符串的被隐藏字符串,他们可以确定掩码(直到 N 个字节,其中 N 是已知明文的长度)并将其应用于目标字符串。这意味着如果 strlen($known) >= strlen($target)
,则目标被揭示;如果不,则只揭示前 N 个字节。
请注意,掩码长度为 128 个字符,如果被隐藏的字符串更长,则将重复。因此,如果攻击者确定了掩码的第一个字节,他们将知道掩码字符串的 1、129、257、... 个字节。
实现尽力使被隐藏的值无法泄露,但由于 PHP 的用户空间限制(例如,无法拦截 var_export
),它无法捕捉到每个场景。
OTP:一次性密码
OTP 允许客户端和服务器之间共享秘密,通过散列已知的“计数器”或“移动因子”来进行身份验证。对于 HOTP,计数器通常是一个单调递增的值;TOTP 基于当前时间。
HOTP:基于 HMAC 的一次性密码(RFC 4226)
您可能不需要直接使用此功能,因为大多数面向用户的OTP应用程序都是基于TOTP协议(见下文)。然而,仅供参考,API如下所示
// Preferred: Object-oriented $otp = new \Firehed\Security\OTP(Secret $secret); $code = $otp->getHOTP(int $counter, int $digits = 6, string $algorithm = OTP::ALGORITHM_SHA1); // Legacy: function-based $code = \Firehed\Security\HOTP(Secret $key, int $counter, int $digits = 6, string $algorithm = 'sha1');
详细的参数文档在OTP类中。
TOTP:基于时间的单次密码(RFC 6238)
此协议基于HOTP,通过使用基于时间的计数器生成单次密码。这一协议因Google Authenticator而流行起来,尽管现在存在一些TOTP客户端。
API使用默认值非常简单
// Preferred: Object-oriented $otp = new \Firehed\Security\OTP(Secret $secret); $code = $otp->getTOTP(int $step = 30, int $t0 = 0, int $digits = 6, string $algorithm = OTP::ALGORITHM_SHA1); // Legacy: function-based $code = \Firehed\Security\TOTP(Secret $key, array $options = []): string
详细的参数文档在OTP类中。参数的默认值与典型的Google Authenticator风格的TOTP设置相匹配。
因此,生成一次性密码非常简单
// The string parameter to $secret should be user-specific, and kept protected at rest. $secret = new \Firehed\Security\Secret('some shared secret'); $otp = new \Firehed\Security\OTP($secret); $code = $otp->getTOTP(); // Or: $code = \Firehed\Security\TOTP($secret);
您应该使用hash_equals
函数将预期值与用户提供的值进行验证,以减轻时间攻击
return hash_equals($user_input, $code);
选项允许更改输出数字的位数(默认6位)、散列算法(sha1)或步长(30秒)。由于大多数TOTP客户端应用程序并不完全支持所有选项,建议目前只使用默认值。有关更多信息,请参阅src/OTP.php
和src/TOTP.php
中的docblocks。
注意
提供给TOTP()
函数的密钥必须是原始值 - 用户添加到其应用程序的通常是通过用户Base32编码发送的。如果您向函数提供Base32编码的密钥,您将得到错误的结果。
共享密钥
HOTP和TOTP都基于客户端和服务器之间的共享密钥。此密钥必须以加密方式由服务器生成并加密存储;向客户端提供密钥也必须以安全方式进行(可能使用TLS)且仅应进行一次,以避免密钥克隆。
强烈建议使用PHP中的random_bytes()
函数生成共享密钥。