oire/iridium

Iridium是一个用于散列密码、加密数据和安全管理安全令牌的安全库。

v2.0 2024-04-20 21:16 UTC

This package is not auto-updated.

Last update: 2024-09-21 23:45:53 UTC


README

Latest Version on Packagist MIT License [Psalm coverage Psalm level

欢迎来到Iridium,一个用于加密数据、散列密码和安全管理安全令牌的安全库!
此库由多个类或模块组成,可用于散列和验证密码、加密和解密数据,以及管理适用于身份验证cookie、密码重置、API访问和其他各种任务的安全令牌。

要求

需要PHP 8.1或更高版本,并启用PDO、Mbstring和OpenSSL。

安装

通过Composer安装Composer

composer require oire/iridium

运行测试

在项目目录中运行./vendor/bin/phpunit

运行Psalm分析

在项目目录中运行./vendor/bin/psalm

🖇 Base64处理,URL安全方式

Base64模块以URL安全方式编码数据并解码编码后的数据。

使用示例

use Oire\Iridium\Base64;
use Oire\Iridium\Exception\Base64Exception;

$text = "The quick brown fox jumps over the lazy dog";
$encoded = Base64::encode($text);
echo $encoded.PHP_EOL;

这将输出

VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw

默认情况下,encode()方法会截断填充=符号,因为PHP的内置解码器可以正确处理。但是,如果提供了第二个参数并设置为true,则=符号将被波浪号(~)替换,即

$encoded = Base64::encode($text, true);
echo $encoded.PHP_EOL;

这将输出

VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw~~

要解码数据,只需调用Base64::decode()

$encoded = "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw";

try {
    $decoded = Base64::decode($encoded);
} catch(Base64Exception $e) {
    // Handle errors
}

echo $decoded.PHP_EOL;

这将输出

The quick brown fox jumps over the lazy dog

方法

Base64类有以下方法

  • static encode(string $data, bool $preservePadding = false): string — 将提供的数据编码为URL安全的Base64。如果preservePadding设置为true,则填充=符号将被波浪号(~)替换。如果设置为false(默认),填充符号将被截断。
  • static decode(string $encodedData): string — 解码提供的Base64数据并返回原始字符串。

🗝 Crypt

Crypt模块用于加密和解密数据。
注意!不要用于管理密码!密码不应被加密,而应使用散列。要管理密码,请使用密码模块(见下文)。
目前Crypt模块仅支持共享密钥加密,即使用单个密钥进行加密和解密。

🔑 共享密钥

此对象包含用于使用Crypt模块加密和解密数据的密钥。首先需要创建一个密钥并将其保存在某个地方(例如,在.env文件中)

use Oire\Iridium\Key\SharedKey;

$sharedKey = new SharedKey();
$key = $sharedKey->getKey();
// Save the key instead
echo $key . PHP_EOL;

这将输出一个可读并可存储的字符串,类似于以下内容

AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8

共享密钥方法

通常,您只需要getKey()方法来安全地存储密钥。您还可以利用__toString()方法将密钥对象作为字符串处理。但是,为了完整性,我们将描述所有方法

  • __construct(string|null $key = null) — 类构造函数。如果提供了密钥,则将其应用于创建一个新的SharedKey实例。如果没有提供,则生成一个随机密钥。
  • getRawKey(): string — 以原始二进制形式返回密钥。主要用于内部使用。
  • getKey(): string — 以可读和可存储的形式返回密钥。使用此方法检索新生成的随机密钥。
  • deriveKeys(string|null $salt = null): DerivedKeys — 使用哈希密钥派生函数来派生加密和身份验证密钥,并返回一个DerivedKeys对象,见下文。仅当你真的知道你在做什么时才使用此方法。这是由Crypt模块内部使用的。如果提供了盐,则基于该盐派生密钥(用于解密)。在99.9%的情况下,你不需要直接使用此方法。
  • __toString(): string — 当对象作为字符串调用时,返回可读和可存储的密钥。

派生密钥

DerivedKeys对象包含由共享密钥的deriveKeys()方法派生的密钥。再次强调,在99.9%的情况下,你不需要使用它,但让我们列举其方法。

  • __construct(string $salt, string $encryptionKey, string $authenticationKey) — 类构造函数。由SharedKey对象的deriveKeys()方法实例化。
  • getSalt(): string — 获取加密盐。
  • getEncryptionKey(): string — 获取派生的加密密钥。
  • getAuthenticationKey(): string — 获取派生的身份验证密钥。
  • areValid(): bool — 检查派生的密钥是否有效。如果密钥有效,则返回true,否则返回false

Crypt用法示例

如果你创建了上述共享密钥,你可以使用此密钥加密你的数据

use Oire\Iridium\Crypt;
use Oire\Iridium\Key\SharedKey;

$data = 'Mischief managed!';
$sharedKey = new SharedKey($key);
$encrypted = Crypt::encrypt($data, $sharedKey);

就这样,你可以将加密的数据存储在数据库中或对其进行其他操作。
要使用相同的密钥解密数据,请使用以下方法

$decrypted = Crypt::decrypt($encrypted, $sharedKey);

异常

Crypt抛出EncryptionExceptionDecryptionException,有时还会抛出一个更通用的CryptException。如果密钥有问题,则抛出SharedKeyException

方法

Crypt类有以下方法

  • static encrypt(string $data, SharedKey $key): string — 使用给定的密钥加密给定的数据。以可读和可存储的形式返回加密数据。
  • static Decrypt(string $encryptedData, SharedKey $key): string — 使用与加密数据相同的密钥解密之前加密的数据,并返回原始字符串。
  • static swapKey(string $data, SharedKey $oldKey, SharedKey $newKey): string — 使用不同的密钥重新加密加密数据,并返回新的加密数据。

🔒 密码

密码类用于哈希密码并验证提供的散列是否有效。

使用示例

要锁定,即哈希密码,请使用以下

use Oire\Iridium\Exception\PasswordException;
use Oire\Iridium\Key\SharedKey;
use Oire\Iridium\Password;

// You should have $key somewhere in an environment variable
$sharedKey = new SharedKey($key);

try {
    $storeMe = Password::lock($_POST['password'], $sharedKey);
} catch (PasswordException $e) {
    // Handle errors
}

然后你可以将你的密码存储在数据库中。
要检查提供的密码是否有效,请使用以下

try {
    $isPasswordValid = Password::check($_POST['password'], $hashFromDatabase, $sharedKey);
} catch (PasswordException $e) {
    // Handle errors. Something went wrong: most often it's a wrong or corrupted key
}

if ($isPasswordValid) {
    // OK
} else {
    // Wrong password
}

你也可以使用Crypt使用另一个密钥重新加密密码,只需使用Crypt::swapKey()并提供你的密码散列即可。
请记住,你不能“解密”密码,显然不能存储未散列的纯文本密码,这会带来巨大的安全风险。

方法

密码类有以下方法

  • static Lock(string $password, SharedKey $key): string — 锁定,即哈希密码并使用给定密钥加密它。以可读和可存储的格式返回加密的散列。散列的密码无法恢复,因此可以安全地存储在数据库中。
  • static Check(string $password, string $encryptedHash, SharedKey $key): bool — 验证给定的密码是否与提供的散列匹配。成功时返回true,失败时返回false

🍪 分割令牌,适用于身份验证Cookies和密码恢复的简单但安全的令牌

SplitToken是Iridium内部的一个类,可以用于生成和验证适合身份验证Cookies、密码恢复、API密钥和各种其他任务的安全令牌。

分割令牌的概念

您可以在 这篇2017年的文章 中了解有关分割令牌认证的详细信息,该文章由 Paragon Initiatives 撰写。Iridium 在 PHP 中实现了该文章中概述的想法。

使用示例

每次使用 SplitToken::create() 生成新令牌或使用 SplitToken::fromString() 从用户提供的令牌创建新的 SplitToken 对象时,您都需要提供一个数据库连接作为 PDO 实例。如果您还没有使用 PDO,考虑使用它,它非常方便。如果您使用 ORM,您很可能有一个 getPDO() 或类似的方法。
计划在未来的版本中支持流行的 ORM。

创建一个表

Iridium 尽可能地实现数据库无关性(已测试 MySQL 和 SQLite,后者实际上支持测试)。
首先,您需要创建 iridium_tokens 表。对于 MySQL,语句如下:

CREATE TABLE `iridium_tokens` (
    `id` INT UNSIGNED NULL AUTO_INCREMENT PRIMARY KEY,
    `user_id` INT UNSIGNED NULL,
    `token_type` TINYINT UNSIGNED NULL ,
    `selector` VARCHAR(25) NOT NULL,
    `verifier` VARCHAR(70) NOT NULL,
    `additional_info` TEXT NULL,
    `expires_at` BIGINT(20) UNSIGNED NULL,
    CONSTRAINT `fk_iridium_token_user`
        FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
        ON UPDATE RESTRICT
        ON DELETE CASCADE
) ENGINE = InnoDB;

您可能需要调整语法以适应您的特定数据库驱动程序(例如,查看测试中的 SQLite 语句),以及您的 users 表的名称。
字段长度是最佳的。但是请记住,您需要调整 FOREIGN KEY 约束中 user_id 字段的长度和符号(UNSIGNED 或非)否则您将从 MySQL 或 MariaDB 收到非常神秘的错误。

创建一个令牌

首先,您需要创建一个令牌。您可以设置一些参数,但数据库连接是必需的,所有其他参数都有默认值。

  • dbConnection — 数据库连接,作为一个 PDO 实例。
  • expirationTime — 令牌过期的时间。存储为时间戳(大整数),但可以设置为整数或字符串。如果您提供字符串,它将被传递给 DateTimeImmutable 构造函数。还有一个特殊值 0(零)。如果将过期时间设置为 0,则将使用默认过期时间,等于当前时间加一小时。如果将 expirationTime 设置为 null,则令牌是永久的,即它永远不会过期。默认值为 0,即过期时间为一个小时。
  • userId — 属于令牌的用户的 ID,作为一个无符号整数。如果它被设置并且是 0 或更小,则会抛出异常。
  • tokenType — 如果您想对令牌执行额外的检查(例如,将密码恢复令牌与电子邮件更改令牌区分开来),您可以设置一个令牌类型作为整数。在文件中的所有示例中,我们将使用纯数字,但我们建议使用枚举。
  • additionalInfo — 您想与令牌一起传达的任何其他信息,作为字符串。例如,您可以将一些 JSON 数据传递到这里。信息可以额外加密。注意再次! 不要使用它来存储密码,即使是过时的密码,这可以被解密。
  • additionalInfoKey — 用于加密附加信息的 Iridium 共享密钥。

要为 ID 为 123 的用户创建一个过期时间为半小时的令牌,并将其存储到数据库中,请执行以下操作。您当然可以使用命名参数

use Oire\Iridium\SplitToken;

// You should have set your $dbConnection first as a PDO instance
$splitToken =  SplitToken::create(
        dbConnection: $dbConnection,
        expirationTime: time() + 1800,
        userId: 123,
        tokenType: 3,
        additionalInfo: '{"some": "data"}
    )
    ->persist();

使用 $splitToken->getToken() 实际上获取新创建的令牌作为字符串。
如果您想创建一个不可过期的令牌,明确地将 expirationTime 设置为 null

设置和验证用户提供的令牌

如果您从用户那里收到了 Iridium 令牌,您还需要实例化 SplitToken 并验证该令牌。为此,请使用 SplitToken::fromString() 而不是 create()。您不需要设置所有属性,因为它们的值是从数据库中获取的。
此方法接受三个参数:作为 PDO 实例的数据库连接、令牌作为字符串,以及可选的附加信息解密密钥作为 Iridium 共享密钥。

use Oire\Iridium\Exception\InvalidTokenException;
use Oire\Iridium\SplitToken;

try {
    $splitToken = SplitToken::fromString($token, $dbConnection);
} catch (InvalidTokenException $e) {
    // Something went wrong with the token: either it is invalid, not found or has been tampered with
}

if ($splitToken->isExpired()) {
    // The token is correct but expired
}

注意!过期的令牌被视为可设置,即,本身并非无效,但正确,因此在这种情况下不会抛出异常,您需要像上面那样手动检查。如果这种行为不符合直觉或不方便,请创建一个 Github 问题

撤销令牌

令牌一旦用于身份验证、密码重置和其他敏感操作,或者过期或受损,就必须撤销,即使其失效。如果您将 Iridium 令牌用作 API 密钥、用于退订电子邮件列表的令牌等,您可以使其令牌永恒,或将过期时间设置得很远,并在第一次使用后不撤销令牌。当然,如果永恒的令牌受损,也必须撤销它。`revokeToken()` 方法返回一个具有令牌相关参数设置为 `null` 的 `SplitToken` 实例。在撤销令牌时,您有两种可能性

  • 将令牌的过期时间设置为过去(默认);
  • 无论何种情况都从数据库中删除令牌。为此,将 `true` 作为参数传递给 `revokeToken()` 方法
// Given that $splitToken contains a valid token
$splitToken = $splitToken->revokeToken(true);

清除过期令牌

有时您需要定期从数据库中删除所有过期令牌以减小表的大小和搜索时间。有一个方法可以做到这一点。它是静态的,因此您必须提供您的 PDO 实例作为其参数。它返回从数据库中删除的令牌数量。

$deletedTokens = SplitToken::clearExpiredTokens($dbConnection);

关于过期时间的说明

  • 所有过期时间都作为 UTC 时间戳内部存储。
  • 过期时间是根据 PHP 服务器的时钟设置的,比较和格式化的,所以即使您的数据库服务器时间因某种原因略微不准确,您也不会遇到麻烦。
  • 值为 0(零)的过期时间设置默认值,即令牌将在一小时后过期。
  • 如果过期时间设置为 null,则令牌是永恒的,永远不会过期。
  • 目前忽略过期时间的微秒,其支持计划在未来版本中实现。

错误处理

SplitToken 抛出两种类型的异常

  • InvalidTokenException 在令牌本身或与令牌相关的 SQL 查询发生真正错误时抛出(例如,令牌找不到,已被篡改,长度无效或 PDO 语句无法执行);
  • SplitTokenException 在大多数情况下,当您做错事时抛出(例如,尝试将空令牌存储到数据库中,尝试设置负的用户 ID 等)。

方法

下面概述了所有 SplitToken 公共方法。

  • static create(PDO $dbConnection, int|string|null $expirationTime = 0, int|null $userId = null, int|null $tokenType = null, string|null $additionalInfo = null, Oire\Iridium\Key\SharedKey|null $additionalInfoKey = null): self — 生成新的令牌。所有参数如上所述,只需数据库连接即可。默认情况下,过期时间设置为 0,这意味着令牌将在一小时后过期。如果 $additionalInfoKey 不为 null,则使用此密钥加密附加信息。如果尝试设置非正的用户 ID,则抛出 SplitTokenException
  • static fromString(string $token, PDO $dbConnection, Oire\Iridium\SharedKey|null $additionalInfoKey): self — 设置并验证用户提供的令牌。如果 $additionalInfoKey 不为 null,则使用此密钥解密数据库中存储的附加信息。
  • getToken(): string — 获取当前 SplitToken 实例的令牌作为字符串。如果令牌之前没有创建或设置,则抛出 SplitTokenException
  • getUserId(): int — 获取令牌所属用户的 ID,作为整数。
  • getExpirationTime(): int — 获取令牌的过期时间作为原始时间戳。返回整数。
  • getExpirationDate(): DateTimeImmutable — 获取令牌的过期时间,以DateTimeImmutable对象的形式返回。返回PHP服务器当前时区的日期。
  • getExpirationDateFormatted(string $format = 'Y-m-d H:i:s'): string — 获取令牌的过期时间,以日期字符串形式返回。默认格式为2020-11-15 12:34:56$format参数必须是一个有效的日期格式
  • isEternal(): bool — 检查令牌是否为永久令牌且永远不会过期。如果令牌是永久的,则返回true,如果设置了未来的过期时间或已经过期,则返回false
  • isExpired(): bool — 检查令牌是否已过期。如果令牌已过期,则返回true,否则返回false
  • getTokenType(): int|null — 获取当前令牌的类型。如果令牌类型之前已设置,则返回整数,如果令牌没有类型,则返回null。
  • getAdditionalInfo(): string|null — 获取令牌的附加信息。返回字符串或null,如果之前未设置附加信息。
  • persist(): self — 将令牌存储到数据库中。返回$this以支持链式调用。
  • revokeToken(bool $deleteToken = false): void — 作废。即使用后使当前令牌失效。如果将$deleteToken参数设置为true,则将从数据库中删除令牌,并且getToken()将返回null。如果设置为false(默认),则更新令牌的过期时间并将其设置为过去的时间值。此方法不返回任何值。
  • static clearExpiredTokens(PDO $dbConnection): int — 从数据库中删除所有已过期的令牌。作为静态方法,它接收数据库连接作为PDO对象。返回已删除的令牌数量,以整数形式。

更改和错误修复

查看变更日志

贡献

欢迎所有贡献。请fork,创建一个功能分支,执行composer install,修改代码,提交,推送您的分支并发送pull请求。

在提交之前,不要忘记运行所有必要的检查,否则CI会在之后抱怨。

./vendor/bin/phpunit
./vendor/bin/psalm
./vendor/bin/php-cs-fixer fix

如果PHPCodeSniffer发现任何代码风格错误,请在您的代码中修复它们。
提交pull请求时,确保CI上的所有检查都通过。

许可

版权所有 © 2021-2024 Andre Polykanine,也称为Menelion Elensúlë,Oirë的神奇王国
本软件根据MIT许可协议许可。