编程

现代 PHP:使用 Sodium 扩展对数据进行加密/解密

3095 2023-04-27 00:32:00

多年来,PHP 增加了对几个扩展、库和算法的支持,以加密和解密数据。伴随着几个具有不同维护级别的库和扩展,每个算法都可能有优缺点,有些算法甚至天生不安全,因此很难选择合适的 PHP 扩展、库和加密结构,并平衡安全性和性能。

mcrypt 是为 PHP 带来加密/解密功能的最古老的 PHP 扩展之一。它不再被维护,并且在 PHP 7.2 中,PHP 将它解除了捆绑。

OpenSSL 是另一个被广泛采用并得到积极支持的库。OpenSSL 提供了广泛的密码、密钥交换和身份验证算法,如果在错误的用例中使用,其中一些算法可能是不安全的。例如,OpenSSL 提供的最常见的加密算法是 AES(高级加密标准)。它有几种操作模式和密钥大小,为不安全的使用留下了空间。从一开始,AE S模式(如 ECB(电子代码簿))在语义上是不安全的,而一些模式,如 CBC(Cipher block chaining 密码块链接),需要对加密消息进行身份验证才能完全安全,并且仍然容易受到密文填塞攻击(padding oracle attacks),如 POODLE。

Libsodium 是 NaCl 的一个分支,是一个更现代、更有主见的密码库。它提供了安全和合理的默认值,并省去了从最终用户到库维护人员的大量决策。PHP 中,Libsodium 可以作为 PECL 扩展使用,但从 7.2  开始,PHP 也将该扩展引入到 PHP 核心中。

安装/启用 Sodium 扩展

从 PHP 7.2 开始,Sodium 扩展就包含在 PHP 核心中。Sodium 扩展很可能已经可用并启用,这可以从 phpinfo() 中确认。


此外,你可以在 PHP CLI 中列出 PHP 扩展,并检查输出:

php -m | grep sodium

如果 Sodium 扩展不可用,可以通过添加扩展指令启用它。PHP 7.2 以后在扩展指令中不需要在添加扩展后缀(如 .dll/.so)。

标准的 PHP 安装使用下面的例子即可:

extension=sodium

当用源代码编译 PHP 时,启用 Sodium 扩展需要使用 --with-sodium 标志和 libsodium 库,在 Ubuntu/Debian 上可以简单安装为 libsodium-dev。

对称及非对称加密

使用对称加密和解密,加密和加密使用同一个密钥。就像生活中我们用同一把钥匙去锁门和开门一样。

Symmetric encryption visualized
如果消息在同一个设备上加密和解密,对称加密更加合适。以下是一些使用对称加密的场景:

  • 在发送给用户前加密浏览器 cookie,并解密传入的 cookie。
  • 加密存储驱动器并使用同一个密钥对其进行解密。
  • Zip/Rar 文件加密。

非对称加密涉及一对密钥:公钥私钥

这里的非对称是指有公钥加密的信息,只能由私钥解密。诚如其名,公钥可以自由公开分发。

当生成密钥对时,它的生成方式保证从数学上讲,私钥(并且只有私钥)可以解密用公钥加密的消息。

非对称使得我们可以将公钥和任何感兴趣的

这种不对称性使得我们可以与任何感兴趣的人共享公钥,并让他们发送加密消息,如果没有私钥,其他人都无法读取。

Asymmetric encryption
非对称加密的一些用例包括:

  • 加密服务器日志,并发送到远程服务器,使得只有远程服务器可以能够读取。
  • SSL/TLS 握手
  • 通过接收方的公钥发送加密信息。

使用 Sodium 进行对称加密/解密

PHP Sodium 扩展提供了一些具有最佳默认值和特定密钥大小的算法,以使用密钥加密/解密数据。

Sodium扩展提供的所有算法都提供经过身份验证的加密,这意味着加密文本将经过身份验证,不会被篡改。这可以防止选择密文攻击(Chosen-ciphertext attacks)。对于 mcrypt 或 OpenSSL 中的大多数密码等方法,它们是通过调用者生成HMAC/签名并防止此类攻击。

需要存储身份验证标签(MAC)和加密文本的应用程序可以通过使用 Sodium 提供的 “detached” API 变体来实现。

目前,Sodium 提供了四种密码可供选择:

CipherKey sizeNonce sizeMAC sizeNotes
AES256-GCM256 bits96 bits128 bits在多个库中广泛支持,需要硬件支持
ChaCha20-Poly1305256 bits64 bits128 bits在多个库中广泛支持,且 Libsodium >= 0.6.0
ChaCha20-Poly1305 - IETF256 bits96 bits128 bits在多个库中广泛支持,且 Libsodium >= 1.0.4
XChaCha20-Poly1305 - IETF256 bits192 bits128 bits在多个库中广泛支持,且 Libsodium >= 1.0.12
XSalsa20-Poly1305256 bits192 bits128 bitssecretbox API 的默认选项

默认情况下,所有四种密码都提供身份验证,可以安全使用。选择合适的密码是在与其他库、编程语言和工具的兼容性之间取得平衡。

AES256-GCM 在大部分 CPU(AES-NI 指令集)中得到广泛支持,在其他扩展如 OpenSSL 中也得到支持。

XChaCha20 with Poly1305 MAC

如果无需考虑与其他语言和工具的兼容性,XChaCha20-Poly1305 - IETF' 是最安全的选择。

Sodium扩展为简单地生成密钥、加密和解密消息提供了函数。此外,random_bytes 函数在生成随机随机随机数值时也很方便。

  • sodium_crypto_aeaad_xchacha20poly1305_ietf_keygen:生成所需长度的密钥。
  • random_bytes:生成随机字节。
  • sodium_crypto_aeaad_xchacha20poly1305_ietf_encrypt:加密消息。
  • sodium_crypto_aeaad_xchacha20poly1305_iet_decrypt:解密消息。

创建密钥

对称加密的密钥是使用加密安全的伪随机数生成器生成的。

Sodium 提供了一个简单的函数来生成所需长度的密钥:

$key = sodium_crypto_aead_xchacha20poly1305_ietf_keygen();

该密钥必须安全地存储起来,它也用在解密消息。

生成 Nonce 值

为了防止重放攻击,即使源数据相同,每个加密消息也必须不同。这是通过生成一个称为 nonce 的随机值来实现的。此值使用一次,并每个加密消息都生成。

随机数不一定是随机的,但它必须是唯一的。使用具有足够长度的随机值使得不需要对照现有的 nonce 值进行检查以确保生成的 $nonce 是唯一的。

XChaCha20-Poly1305 密码需要 192 位的随机数长度,内置常数使其更容易

$nonce = \random_bytes(\SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);

加密消息

生成 $key$nonce 后,就可以加密消息了。

$message = 'Hello World';
$encrypted_text = sodium_crypto_aead_xchacha20poly1305_ietf_decrypt($message, '', $nonce, $key);

$encrypted_text 变量现在保存加密的消息。它包含身份验证标签(MAC),Sodium 会自动使用它来验证消息。

sodium_crypto_aeaad_xchacha20poly1305_iet_decrypt 的第二个参数接受包含额外数据的字符串。此值不加密或与$encrypted_text 一起存储,而是用作身份验证的附加值。在本例中,此值故意留空(“”)。如果必须使用其他身份验证数据(如用户 ID 或 IP 地址),请使用此参数。

保存/传送加密信息

一旦生成加密文本,它就包含身份验证标签,这应该可以防止对加密文本的意外和恶意篡改。

$nonce 值是解密消息所必需的,并且必须与加密消息一起存储。这方面的一个例子是存储在文件中的 $key(secret) 密钥,该文件只能由加密/解密消息的进程访问,以及存储在数据库中的 $nonce(nonce)$encrypted_text

根据加密时的 $additional_data 参数值,该值可能还需要存储。

此外,sodium_crypto_aeaad_xchacha20poly1305_ietf_encrypt 函数返回一个字节流。它不能直接打印(例如在网页或JSON 响应上),因此在打印或传输到无法处理原始字节流的介质之前,必须使用 base64_encodebin2hex 等函数将其转换为文本格式。

echo bin2hex($encrypted_text);

解密消息

解密消息需要原始密钥($key)和 Nonce($nonce) 值。

$original_message = sodium_crypto_aead_xchacha20poly1305_ietf_decrypt($encrypted_text, '', $nonce, $key);

如果提供的密钥、nonce 或附加数据无效,则此函数将返回 false

完整示例:身份认证对称加密/解密


// Generate a secret key. This value must be stored securely.
$key = sodium_crypto_aead_xchacha20poly1305_ietf_keygen();

// Generate a nonce for EACH MESSAGE. This can be public, and must be provided to decrypt the message.
$nonce = \random_bytes(\SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);

// Text to encrypt.
$message = 'Hello World';

// Encrypt
$encrypted_text = sodium_crypto_aead_xchacha20poly1305_ietf_encrypt($message, '', $nonce, $key);

// Decrypt
$original_message = sodium_crypto_aead_xchacha20poly1305_ietf_decrypt($encrypted_text, '', $nonce, $key);

从 Mcrypt 和 OpenSSL 中迁移

mcrypt 和 OpenSSL 扩展也提供对称加密。然而,它们对身份验证、密钥长度和随机数长度的支持程度各不相同。

一般来说,sodium_crypto_aeaad_xchacha20poly1305_ietf_* 函数是 mcrypt_encryptopenssl_encryption 函数的更好替代方案。

使用 Sodium 进行身份验证的不对称加密/解密

Sodium 还提供了一种不对称加密和解密消息的方法。

非对称加密涉及两方。双方创建一个密钥对,其中包含一个私钥和一个公钥。公钥被分发给需要与之通信的其他方。

例如,如果 Alice 和 Bob 想要安全地通信,并且必须确保:

  1. 消息确实来自 Alice/Bob,而不是来自其他人。
  2. 消息是私有的,没有人可以阅读或篡改它们。

Sodium 扩展提供了一个 Crypto Box API,可以实现此类消息的真实性和隐私性。在发送消息之前,Alice 和 Bob(或所有相关方)必须生成一个密钥对,并安全地交换公钥

Sodium 还提供了安全的密钥交换协议,但本文未对此进行介绍。

发送消息时,发送方使用接收方的公钥对其进行加密,并使用发送方的私钥对其进行签名。一旦消息被发送,接收方就用发送方的公钥对消息进行身份验证,并用接收方的私钥进行解密

 

asymmetric encryption with authentication

如果接收方不需要对收到的消息进行身份验证-以确保消息确实是由发送方发送的-Sodium 扩展也使用crypto_box_seal API 进行未经身份验证的不对称加密/解密。

生成密钥对

所有需要通信的各方都必须生成一个密钥对。该密钥必须保密,因为它用于对消息进行签名和解密。

在 Alice 和 Bob 的设备上分别生成密钥

$alice_keypair = sodium_crypto_box_keypair();
$alice_secret_key = sodium_crypto_box_secretkey($alice_keypair);
$alice_public_key = sodium_crypto_box_publickey($alice_keypair);
$bob_keypair = sodium_crypto_box_keypair();
$bob_secret_key = sodium_crypto_box_secretkey($bob_keypair);
$bob_public_key = sodium_crypto_box_publickey($bob_keypair);
  • sodium_crypto_box_keypair 生成一个随机密钥对,其中包含 X25519 密钥及其对应的 X25519 公钥
  • sodium_crypto_box_secretkey 提取密钥对的密钥部分。
  • sodium_crypto_box_publicey 提取密钥对的公钥。

交换密钥

对于经过身份验证的非对称加密和解密,相关双方必须安全地交换其公钥。这可以使用密钥交换协议来完成,也可以使用另一个安全通道(如 HTTPS 请求)来传输密钥。

Sodium 通过其 Sodium_crypto_kx_* 功能提供密钥交换功能。然而,本文在这一点上没有涵盖这些 API,以确保其简洁性。

成功交换密钥后:

  • Alice 必须拥有 Alice 的公钥、Alice 的私钥和 Bob 的公钥

Bob 必须拥有 Bob 的公钥、Bob 的私钥和 Alice 的公钥

创建 Nonce 值

与使用对称加密为每条消息创建 nonce 随机值类似,经过身份验证的非对称消息也必须使用 nonce 随机值。

nonce 值必须为 192 位(24 字节),可以使用 random_bytes 函数和内置的 SODIUM_CRYPTO_BOX_NONCEBYTES 常量轻松创建,其值为 24。

$nonce = \random_bytes(\SODIUM_CRYPTO_BOX_NONCEBYTES);

构建加密密钥对

在加密消息之前,发送方(本例中为 Alice)必须创建一个新的密钥对,其中包含接收方的公钥和发送方的私钥。这可以通过连接公钥和私钥来实现,也可以使用 sodium_crypto_box_keypair_from_secretkey_and_publickey 函数。

发送方:

$sender_keypair = sodium_crypto_box_keypair_from_secretkey_and_publickey($alice_secret_key, $bob_public_key);

消息加密和签名

交换了密钥,生成了随机数,建立了加密密钥对,现在是时候对消息进行加密了。

$message = "Hi Bob, I'm Alice";
$encrypted_signed_text = sodium_crypto_box($message, $nonce, $sender_keypair);

sodium_crypto_box 函数使用密钥对($sender_keypair)对消息进行加密和签名。发送方的私钥用于创建签名,接收方的公钥用于加密实际消息。身份验证标签与加密文本($encrypted_signed_text)一起存储。

存储/传输加密和签名的文本

一旦消息被加密和签名,只有拥有私钥的接收者才能解密消息。任何拥有发件人公钥的人都可以对邮件进行身份验证,但如果没有收件人的私钥,就无法读取邮件内容。

$nonce nonce 值必须与密文一起存储/传输,并且它可能是公共的。

请确保为下一条消息重新生成 $nonce值,以防止重放攻击。

解密和验证接收到的消息

如果接收方拥有发送方的公钥(本例中为 Alice 的公钥),则接收方可以对消息进行身份验证,并确保该消息由发送方签名。

$recipient_keypair = sodium_crypto_box_keypair_from_secretkey_and_publickey($bob_secret_key, $alice_public_key);
$orig_msg = sodium_crypto_box_open($encrypted_signed_text, $nonce, $sender_keypair);
var_dump($orig_msg); // "Hi Bob, I'm Alice"

sodium_crypto_box_keypair_from_secretkey_and_publickey 函数再次用于创建新的密钥对,但这一次,它是根据接收方的私钥和发送方的公钥创建的。

一旦生成了密钥对,就可以对消息进行身份验证和解密。

sodium_cryptobox_open 函数验证消息是否由发送者签名(使用发送者的私钥签名),并使用接收者的私钥解密消息。

如果密钥或 nonce 不匹配,那么 sodium_crypto_box_open 将返回 false

完整示例:经过身份验证的公钥加密/解密

// On Alice's device
$alice_keypair = sodium_crypto_box_keypair();
$alice_secret_key = sodium_crypto_box_secretkey($alice_keypair);
$alice_public_key = sodium_crypto_box_publickey($alice_keypair);

// On Bob's device
$bob_keypair = sodium_crypto_box_keypair();
$bob_secret_key = sodium_crypto_box_secretkey($bob_keypair);
$bob_public_key = sodium_crypto_box_publickey($bob_keypair);

// Exchange keys:
// - Send Alice's public key to Bob.
// - Send Bob's public key to Alice.

// On sender:

// Create nonce
$nonce = \random_bytes(\SODIUM_CRYPTO_BOX_NONCEBYTES);

// Create enc/sign key pair.
$sender_keypair = sodium_crypto_box_keypair_from_secretkey_and_publickey($alice_secret_key, $bob_public_key);

$message = "Hi Bob, I'm Alice";

// Encrypt and sign the message
$encrypted_signed_text = sodium_crypto_box($message, $nonce, $sender_keypair);

// On recipient:
$recipient_keypair = sodium_crypto_box_keypair_from_secretkey_and_publickey($bob_secret_key, $alice_public_key);

// Authenticate and decrypt message
$orig_msg = sodium_crypto_box_open($encrypted_signed_text, $nonce, $sender_keypair);

var_dump($orig_msg); // "Hi Bob, I'm Alice"

使用 Sodium 进行未经验证的非对称加密/解密

如果接收方不需要对传入消息进行身份验证,而只需要对其进行解密,那么这可能是一个使用 Sodium 的 crypto_box_seal API 的场景。

asymmetric encryption without authentication

Sodium中 的 crypto_box_seal

Sodium 的 crypto_box_seal 函数用于使用一对公钥和私钥对消息进行加密和解密。与经过身份验证的非对称加密/解密之间的主要区别在于 crypt_box_seal 不会对消息进行身份验证。

Sodium 的 crypt_box_seal 功能与 OpenSSL 的 openssl_public_encryptopenssl_private_decrypt 类似,因为 OpenSSL 功能对也不提供身份验证。

对于 crypto_box_seal,只有接收方需要生成一个密钥对。发送方可以获得接收方的公钥,并对消息进行加密。

接收者可以解密任何用其公钥加密的消息,但无法识别或验证发送者的身份。

为收件人创建密钥对

对于未经身份验证的非对称加密,只需要收件人生成密钥对:

$recipient_keypair = sodium_crypto_box_keypair();
$recipient_public_key = sodium_crypto_box_publickey($recipient_keypair);

分发公钥

接收方必须安全地存储密钥对的私钥部分。然后,可以通过安全通道分发公钥($recipient_public_key)。

分发公钥最简单、最常见的方法是通过 HTTPS 连接。例如,发送者可以通过 HTTPS 从接收者的网站下载发送者的公钥。

请注意,尽管匿名非对称加密不为消息提供身份验证,但公钥必须安全传输,发送方必须验证接收方的公钥是否与实际接收方一致,并且密钥在传输过程中不会被篡改。

加密消息

有了接收方的公钥,任何发送方都可以加密消息:

$message = "Hi Bob, you don't know who I am";
$encrypted_text = sodium_crypto_box_seal($message, $recipient_public_key);

存储/传输消息

加密消息只能由持有私钥的接收人打开。然而,由于不存在身份验证或 nonce 值,这很容易受到重放攻击。

解密消息

要解密使用公钥加密的消息,接收方必须拥有相应的私钥。

sodium_crypto_box_seal_open 使用密钥对加密的消息进行解密。

$original_message = sodium_crypto_box_seal_open($encrypted_text, $recipient_keypair);
var_dump($original_message); // "Hi Bob, you don't know who I am"

完整示例:未经身份验证的公钥加密

$recipient_keypair = sodium_crypto_box_keypair();
$recipient_public_key = sodium_crypto_box_publickey($recipient_keypair);

$message = "Hi Bob, you don't know who I am";
$encrypted_text = sodium_crypto_box_seal($message, $recipient_public_key);

$original_message = sodium_crypto_box_seal_open($encrypted_text, $recipient_keypair);
var_dump($original_message); // "Hi Bob, you don't know who I am"

 

PHP