equit / totp
PHP TOTP 生成器
Requires
- php: >=8.0
Requires (Dev)
- ext-mbstring: *
- phpunit/phpunit: ^9.5
- dev-main
- 0.2.0
- 0.1.0
- 0.0.6
- 0.0.5
- 0.0.4
- 0.0.3
- 0.0.2
- dev-PT-18-Totp-Contract
- dev-PT-12-finalise-readme-md
- dev-PT-16-scrub-members-in-base-32-and-base-64-destructors
- dev-PT-15-scrub-secrets-in-totp-secret-destructor
- dev-PT-10-review-symbol-names-and-argument-types-for-consistency
- dev-PT-3-alternative-sources-of-random-data-for-totp-random-secret
- dev-PT-11-rename-totp-interval-as-totp-time-step-for-consistency-with-rfc
- dev-PT-2-steam-renderer
- dev-PT-9-tests-for-exception-classes
- dev-PT-7-copyright-notice-for-all-source-files
- dev-PT-8-f-ix-incorrect-argument-order-for-totp-integer-totp-in-random-totp-php-dev-tool
- dev-PT-5-use-declare-strict-types-1-for-all-source-files
- dev-PT-1-ability-to-generate-provisioning-ur-ls
This package is auto-updated.
Last update: 2024-09-10 16:51:27 UTC
README
PHP 的时间基础一次性密码生成器。
使用符合 RFC 6238 标准的 TOTP,为您的应用程序添加双因素认证,兼容常见的身份验证器应用程序,例如 Google Authenticator、KeePassXC、Microsoft Authenticator 等。
快速入门
-
为您的用户生成一个安全、随机的密钥
$user->totpSecret = Totp::randomSecret()
-
通知用户其 TOTP 的详细信息,以便将其导入其身份验证器应用程序
UrlGenerator::for($user->username)->urlFor(new Totp($user->totpSecret))
-
当用户登录时,要求他们提供当前的 TOTP 并进行验证
(new Totp($user->totpSecret))->verify($inputOtp)
内容
另请参阅
简介
TOTP 由 RFC 6238 规定,并基于 基于 HMAC 的一次性密码 (HOTP, RFC4226),通过计算基于自特定时间点以来经过的时间步数和由授权服务器(您的应用程序)和安全的客户端应用程序(您的用户的身份验证器应用程序)所知的随机密钥的哈希消息认证码(HMAC,RFC 2104)来构建。然后,从 HMAC 中导出一个 31 位的整数,并使用最右侧的(通常是 6 位)十进制数字作为密码(如有必要,用 0 填充)。只要服务器和应用程序就当前时间、参考时间、时间步长的大小和密钥达成一致,它们就会同时计算出相同的密码序列。
php-totp 由三个主要组件组成:一个 Totp
类,它执行计算 TOTP 的大部分工作;一个 UrlGenerator
类,它有助于生成用户设置其身份验证器应用程序所需的信息;以及一组 OTP Renderer
类,它们将 Totp
执行的计算结果转换为实际的一次性密码。(后一类的对象由 Totp
内部使用,除非您正在发明自己的密码创建方案——这被强烈反对——否则您不太可能需要了解它们。)
以下示例使用概念性函数、类和方法来填补 php-totp 库之外的函数。例如,encrypt()
函数用作您应用程序用于加密数据的任何机制的占位符。它们还假设一个标准的 TOTP 配置,如 RFC 6238 中所述——即参考时间为 1970 年 1 月 1 日 00:00:00,时间步长为 30 秒,SHA1 哈希算法生成 6 位密码。有关自定义 TOTP 配置的可能性将在稍后描述。
为用户配置 TOTP
为用户配置 TOTP 包括三个步骤
- 为用户生成、加密并存储密钥。
- 向用户发送一个包含URL、密钥和/或二维码的通知,以便他们可以将其导入到他们的认证应用中。
- 通过要求用户输入当前的OTP来验证配置是否成功。
生成密钥
TOTP规范要求密钥随机生成(即不由用户选择)。您可以生成自己的密钥,但Totp::randomSecret()
,可以为您生成一个随机密钥,保证其密码学安全性,并且足以支持TOTP支持的所有哈希算法。或者,您也可以不提供密钥而直接实例化一个
一旦生成密钥,就必须安全存储。它必须始终以加密形式存储。
$user->totpSecret = encrypt(Totp::randomSecret()); $user->save();
通常,Base32编码与TOTP密钥一起使用,尤其是在将它们添加到认证应用时。如果您需要Base32格式的密钥,php-totp提供了一个Base32
编解码器类来执行转换。
$user->totpSecret = encrypt(Base32::encode(Totp::randomSecret())); $user->save();
有时也会使用Base64。PHP提供了内置的Base64编码和解码,但为了保持一致性,php-totp也提供了一个Base64
编解码器类,其操作方式与Base32
类相同,只是使用Base64。
最小化密钥未加密的可用性
您应努力减少共享密钥在RAM中未加密的时间。无论您是用它进行配置还是进行验证,您都应该只在准备好使用它之前才检索它,一旦不再需要,就应立即丢弃它,并确保在丢弃之前将包含密钥的变量安全擦除。如果不这样做,未加密的密钥可能会在不再由您的应用使用的内存中“可见”。\Equit\Totp
命名空间中的scrubString()
函数可用于实现此功能 - 将包含密钥的字符串变量传递给它,它将用随机字节覆盖该字符串。
php-totp库中所有旨在与TOTP密钥一起使用的代码都以这种方式清理其数据,以帮助防止TOTP密钥意外可见。这包括TotpSecret
类以及Base32
和Base64
类。您应该一旦不再需要它们就取消设置这些类的实例,并确保您没有保留不必要的引用。
通知用户
用户获取其TOTP密钥详情通常有三种常见方式,并且大多数认证应用至少支持其中一种——许多支持所有三种。
1. 仅密钥
第一种是直接向他们提供密钥。由于密钥是二进制字符串,它需要转换为某种文本安全格式,通常使用Base32。此方法通知用户只有在使用标准TOTP配置的情况下才可行——即6位OTP、SHA1哈希、Unix纪元作为参考时间和30秒作为时间步长。如果您使用的是自定义TOTP配置,您需要向用户提供更多信息,并且他们需要执行更多步骤来配置他们的认证应用。
$user->notify(Base32::encode(decrypt($user->totpSecret)));
请注意,在此示例中,用于编码TOTP密钥的临时Base32
对象在使用后立即超出作用域,因此其属性被安全擦除。
otpauth
URL
第二种方法是提供一个专门构造的URL,让用户的认证器应用能够读取。URL格式在此描述。 php-totp 提供了一个 UrlGenerator
类来创建这些URL。
$user->notify(UrlGenerator::from("MyWebApp")->for($user->username)->urlFor(new Totp(decrypt($user->totpSecret)));
同样,Totp
对象是临时的,使用后立即超出范围,因此其密钥是安全擦除的。
默认情况下,UrlGenerator 会将尽可能多的信息插入到生成的URL中,以表示您的TOTP设置。因此,如果您使用SHA512哈希算法,生成的URL将包含 algorithm
URL参数;如果您使用默认的SHA1算法,则省略 algorithm
URL参数。《UrlGenerator》类提供了一个流畅的接口来配置如何构建URL(例如,您可以通过在 urlFor
方法之前链式调用 withAlgorithm
方法来强制生成 algorithm
URL参数,即使您使用的是非默认算法)。
此通知方法支持所有自定义设置,除了那些使用非标准参考时间的设置(因为没有URL参数可以指定它)。许多具有TOTP功能的认证器应用支持此类型的URL,尽管您需要检查您针对用户的目标应用中的支持级别——例如,Google Authenticator 支持URL,但不识别 algorithm
参数,始终使用SHA1算法。
3. 二维码
第三种方法是为用户提供一个认证器应用可以扫描的二维码。这实际上与上述使用URL的方法相同——二维码只是生成URL的表示。
php-totp 目前还没有二维码生成器,但应该很容易使用现有的二维码生成器以及 UrlGenerator
来创建发送给用户的二维码。 bacon/bacon-qr-code 是这样的外部库之一。
验证配置成功
一旦为用户配置了,您需要要求他们从认证器应用中获取OTP以确认它已成功设置。收到用户输入后,验证很简单。
$isVerified = (new Totp(decrypt($user->totpSecret))->verify($inputOtp);
为了避免用户在时间步长快结束时输入OTP时出现的问题,您可以选择接受一小部分以前的密码以及当前密码。向 Totp::verify()
方法的 window
参数提供一个参数,该参数标识验证将回退的最大时间步数以检查匹配的OTP。
$isVerified = (new Totp(decrypt($user->totpSecret))->verify(password: $inputOtp, window: 1);
默认情况下,Totp::verify()
只接受当前的OTP。强烈建议您使用最多1个窗口进行验证。
批量配置用户
您可以使用一个UrlGenerator实例为多个用户配置TOTP,并通知每个用户他们自己的唯一URL。
$generator = UrlGenerator::from("Equit"); foreach ($users as $user) { $totp = new Totp(algorithm: Totp::Sha512Algorithm); $user->totpSecret = $totp->secret(); $user->save(); $user->notify($generator->for($user->username)->urlFor($totp)); } unset($totp);
身份验证
验证用户的TOTP主要是一个简单的过程,即要求用户提供当前的OTP并验证它。这与验证他们的TOTP应用的初始设置相同。
$isVerified = (new Totp(decrypt($user->totpSecret))->verify($userInput);
或者,使用验证窗口
$isVerified = (new Totp(decrypt($user->totpSecret))->verify(password: $inputOtp, window: 1);
如果 Totp::verify()
返回 false
,则用户未提供正确的OTP,不应使用您的应用进行认证;如果它返回 true
,则用户已提供了有效的OTP,可以进行认证。
确保 OTP 只使用一次
RFC规定,每个生成的OTP只能使用一次以成功认证——一旦OTP已成功用于认证,该OTP就不应再次使用。
确保每个一次性密码(OTP)不会被重复使用的一种方法是在每次成功认证后记录TOTP计数器。计数器是一个递增整数,表示自参考时间以来经过了多少个时间步。通过记录最高使用过的计数器值,并拒绝验证在相应时间步或之前生成的任何OTP,可以确保没有OTP可以被重复使用。
$totp = new Totp(decrypt($user->totpSecret)); if ($user->highestUsedTotpCounter < $totp->counter()) { if ($totp->verify($inputOtp)) { $user->highestUsedTotpCounter = $totp->counter(); $user->save(); // user is authenticated } else { // incorrect OTP } } else { // OTP has already been used } // ensure the secret is shredded scrubString($inputOtp); unset($totp);
您还可以在调用Totp::verify()
时使用验证窗口,但别忘了调整窗口以避免接受之前使用过的OTP
$totp = new Totp(decrypt($user->totpSecret)); $window = min(1, $totp->counter() - $user->highestUsedTotpCounter - 1); if (0 <= $window) { if ($totp->verify(password: $inputOtp, window: $window)) { ... } } // ensure the secret is shredded scrubString($inputOtp); unset($totp);
您需要确保所有使用TOTP密钥的认证路径都受到OTP重复使用保护——例如,如果您有一个移动应用程序和一个Web应用程序,您必须确保用于Web应用程序认证的OTP不能随后用于移动应用程序的认证。《RFC 4226》对这一理由有很好的讨论。
自定义TOTP配置
您可以对TOTP设置进行四项自定义
- 哈希算法
- 参考时间戳
- 时间步长的大小
- OTP的位数
自定义TOTP设置应被视为一次性选项。一旦确定设置,就很难更改(您需要重新配置所有用户,并且他们都需要重新配置他们的认证应用程序),因此通常最好在开始之前仔细选择设置。
Totp
构造函数和便利的工厂方法Totp::sixDigits()
、Totp::eightDigits()
和Totp::integer()
都接受参数来自定义TOTP的所有四个方面。所有这些参数都使用TOTP RFC中指定的默认值,除非您显式提供值,这意味着您可以使用PHP的命名参数仅自定义那些非默认的TOTP实例方面。
哈希算法
TOTP支持三种哈希算法——SHA1、SHA256和SHA512。最强的是SHA512,而RFC中指定的默认值是SHA1(与HOTP兼容)。如上所述,在自定义之前,您应检查针对用户的目标认证应用程序是否支持您打算使用的算法。
Totp
类提供表示所有支持的哈希算法的常量,您强烈建议使用这些常量以避免在应用程序中抛出异常。使用这些常量可以使您的应用程序对未来可能更新为使用PHP8.1枚举来指定哈希算法的php-totp具有前瞻性。
要使用SHA256创建Totp
实例如下
// when provisioning $totp = new Totp(hashAlgorithm: Totp::Sha256Algorithm); // when verifying $totp = new Totp(secret: decrypt($user->totpSecret), hashAlgorithm: Totp::Sha256Algorithm);
类似地,要使用SHA512
// when provisioning $totp = new Totp(hashAlgorithm: Totp::Sha512Algorithm); // when verifying $totp = new Totp(secret: decrypt($user->totpSecret), hashAlgorithm: Totp::Sha512Algorithm);
参考时间戳和时间步长
TOTP使用的计数器是从参考时间以来经过的时间步数。默认情况下,参考时间是00:00:00 01/01/1970(又称Unix纪元或Unix时间戳0
)。默认时间步长大小是30秒。除非您有充分的理由更改它们,否则这些默认值是合理的。如果您选择自定义时间步长,请记住,非常短的时间间隔会让用户更难,因为他们有更少的时间输入正确的OTP。同样,使间隔太大也可能让用户更难,因为如果您在他们只有短暂会话后注销,您可能会有效地将他们锁定一段时间。
要使用60秒的时间步长而不是30秒,创建Totp
实例如下
// when provisioning $totp = new Totp(timeStep: 60); // when verifying $totp = new Totp(secret: decrypt($user->totpSecret), timeStep: 60);
您可以使用Unix时间戳来自定义参考时间
// when provisioning $totp = new Totp(referenceTime: 86400); // when verifying $totp = new Totp(secret: decrypt($user->totpSecret), referenceTime: 86400);
或DateTime
对象
// when provisioning $totp = new Totp(referenceTime: new DateTime("1970-01-02 00:00:00", new DateTimeZone("UTC"))); // when verifying $totp = new Totp(secret: decrypt($user->totpSecret), referenceTime: new DateTime("1970-01-02 00:00:00", new DateTimeZone("UTC")));
这两个示例都创建了一个将参考时间设置为1970年1月2日午夜UTC的TOTP。强烈建议在创建DateTime
对象时使用UTC时区以避免任何混淆。TOTP算法与始终从00:00:00开始测量的Unix时间戳一起工作,即1970年1月1日UTC。
您可以自定义时间步长和参考时间
// when provisioning $totp = new Totp(timeStep: 60, referenceTime: new DateTime("1970-01-02 00:00:00", new DateTimeZone("UTC"))); // when verifying $totp = new Totp(secret: decrypt($user->totpSecret), timeStep: 60, referenceTime: new DateTime("1970-01-02 00:00:00", new DateTimeZone("UTC")));
以及哈希算法
// when provisioning $totp = new Totp( timeStep: 60, referenceTime: new DateTime("1970-01-02 00:00:00", new DateTimeZone("UTC")), hashAlgorithm: Totp::Sha512Algorithm ); // when verifying $totp = new Totp( secret: decrypt($user->totpSecret), timeStep: 60, referenceTime: new DateTime("1970-01-02 00:00:00", new DateTimeZone("UTC")), hashAlgorithm: Totp::Sha512Algorithm );
密码位数
OTP的数字位数默认为6位,但范围可以从6位到9位(含9位)。从技术上讲,使用更多数字位数的理由并不是没有,但除了在OTP左边填充0以外,没有任何好处。
创建8位Totp最简单的方法是使用Totp::eightDigits()
便捷工厂方法
// when provisioning $totp = Totp::eightDigits(); // when verifying $totp = Totp::eightDigits(decrypt($user->totpSecret));
当然,您仍然可以自定义Totp
的其他方面
// when provisioning $totp = Totp::eightDigits( timeStep: 60, referenceTime: new DateTime("1970-01-02 00:00:00", new DateTimeZone("UTC")), hashAlgorithm: Totp::Sha512Algorithm ); // when verifying $totp = Totp::eightDigits( secret: decrypt($user->totpSecret), timeStep: 60, referenceTime: new DateTime("1970-01-02 00:00:00", new DateTimeZone("UTC")), hashAlgorithm: Totp::Sha512Algorithm );
如果您想使用不常见的数字位数,请使用Totp::integer()
方法
// when provisioning $totp = Totp::integer(9); // when verifying $totp = Totp::integer(digits: 9, secret: decrypt($user->totpSecret));
并且,还有更多的自定义选项
// when provisioning $totp = Totp::integer( digits: 9, timeStep: 60, referenceTime: new DateTime("1970-01-02 00:00:00", new DateTimeZone("UTC")), hashAlgorithm: Totp::Sha512Algorithm ); // when verifying $totp = Totp::integer( digits: 9, secret: decrypt($user->totpSecret), timeStep: 60, referenceTime: new DateTime("1970-01-02 00:00:00", new DateTimeZone("UTC")), hashAlgorithm: Totp::Sha512Algorithm );
要控制密码的属性,而不仅仅是数字位数,您可以在构造函数中提供renderer
参数。例如,为了使Totp
生成与Steam验证器兼容的5位OTP,
// when provisioning $totp = new Totp(renderer: new Steam()); // when verifying $totp = new Totp(secret: decrypt($user->totpSecret), renderer: new Steam());
以及其他自定义设置
// when provisioning $totp = new Totp( renderer: new Steam(), timeStep: 60, referenceTime: new DateTime("1970-01-02 00:00:00", new DateTimeZone("UTC")), hashAlgorithm: Totp::Sha512Algorithm ); // when verifying $totp = new Totp( renderer: new Steam(), secret: decrypt($user->totpSecret), timeStep: 60, referenceTime: new DateTime("1970-01-02 00:00:00", new DateTimeZone("UTC")), hashAlgorithm: Totp::Sha512Algorithm );
Base32/Base64 密钥
如前所述,TOTP通常与以Base32或Base64编码的机密信息一起使用,以便于将其输入到验证器应用程序中。如果您使用这些编码之一存储您的机密信息(例如在数据库中的文本字段),在将它们传递给Totp
实例之前,需要对其进行解码(以及解密)。
您可以选择自己进行
$totp = new Totp(Base32::decode(decrypt($user->totpSecret))); $totp = new Totp(Base64::decode(decrypt($user->totpSecret)));
或者使用TotpSecret
实用类
$totp = new Totp(TotpSecret::fromBase32(decrypt($user->totpSecret))); $totp = new Totp(TotpSecret::fromBase64(decrypt($user->totpSecret)));
Base32/Base64和TotpSecret类都负责清除机密信息的细节,因此机密信息将仅在Totp
实例中保留一个副本。如果您使用其他Base32/Base64解码器(例如PHP的base64_decode()
函数),您可能无法确保在释放之前正确地从内存中清除机密信息。
RFCs
- H. Krawczyk、M. Bellare和R. Canetti,《RFC2104: HMAC: 消息认证的密钥散列》,https://www.ietf.org/rfc/rfc2104.txt,2022年4月17日检索。
- D. M'Raihi、M. Bellare、F. Hoornaert、D. Naccache和O. Ranen,2005,《RFC4226: HOTP: 基于HMAC的一次性密码算法》,https://www.ietf.org/rfc/rfc4226.txt,2022年4月17日检索。
- D. M'Raihi、S. Machani、M. Pei和J. Rydell,2011,《RFC6238: TOTP: 基于时间的一次性密码算法》,https://www.ietf.org/rfc/rfc6238.txt,2022年4月17日检索。