现代 PHP:使用 Sodium 扩展对数据进行加密/解密
多年来,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。
对称及非对称加密
使用对称加密和解密,加密和加密使用同一个密钥。就像生活中我们用同一把钥匙去锁门和开门一样。
如果消息在同一个设备上加密和解密,对称加密更加合适。以下是一些使用对称加密的场景:
- 在发送给用户前加密浏览器 cookie,并解密传入的 cookie。
- 加密存储驱动器并使用同一个密钥对其进行解密。
- Zip/Rar 文件加密。
非对称加密涉及一对密钥:公钥和私钥。
这里的非对称是指有公钥加密的信息,只能由私钥解密。诚如其名,公钥可以自由公开分发。
当生成密钥对时,它的生成方式保证从数学上讲,私钥(并且只有私钥)可以解密用公钥加密的消息。
非对称使得我们可以将公钥和任何感兴趣的
这种不对称性使得我们可以与任何感兴趣的人共享公钥,并让他们发送加密消息,如果没有私钥,其他人都无法读取。
非对称加密的一些用例包括:
- 加密服务器日志,并发送到远程服务器,使得只有远程服务器可以能够读取。
- SSL/TLS 握手
- 通过接收方的公钥发送加密信息。
使用 Sodium 进行对称加密/解密
PHP Sodium 扩展提供了一些具有最佳默认值和特定密钥大小的算法,以使用密钥加密/解密数据。
Sodium扩展提供的所有算法都提供经过身份验证的加密,这意味着加密文本将经过身份验证,不会被篡改。这可以防止选择密文攻击(Chosen-ciphertext attacks)。对于 mcrypt 或 OpenSSL 中的大多数密码等方法,它们是通过调用者生成HMAC/签名并防止此类攻击。
需要存储身份验证标签(MAC)和加密文本的应用程序可以通过使用 Sodium 提供的 “detached” API 变体来实现。
目前,Sodium 提供了四种密码可供选择:
Cipher | Key size | Nonce size | MAC size | Notes |
---|---|---|---|---|
AES256-GCM | 256 bits | 96 bits | 128 bits | 在多个库中广泛支持,需要硬件支持 |
ChaCha20-Poly1305 | 256 bits | 64 bits | 128 bits | 在多个库中广泛支持,且 Libsodium >= 0.6.0 |
ChaCha20-Poly1305 - IETF | 256 bits | 96 bits | 128 bits | 在多个库中广泛支持,且 Libsodium >= 1.0.4 |
XChaCha20-Poly1305 - IETF | 256 bits | 192 bits | 128 bits | 在多个库中广泛支持,且 Libsodium >= 1.0.12 |
XSalsa20-Poly1305 | 256 bits | 192 bits | 128 bits | secretbox 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_encode
或 bin2hex
等函数将其转换为文本格式。
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_encrypt
和 openssl_encryption
函数的更好替代方案。
使用 Sodium 进行身份验证的不对称加密/解密
Sodium 还提供了一种不对称加密和解密消息的方法。
非对称加密涉及两方。双方创建一个密钥对,其中包含一个私钥和一个公钥。公钥被分发给需要与之通信的其他方。
例如,如果 Alice 和 Bob 想要安全地通信,并且必须确保:
- 消息确实来自 Alice/Bob,而不是来自其他人。
- 消息是私有的,没有人可以阅读或篡改它们。
Sodium 扩展提供了一个 Crypto Box API,可以实现此类消息的真实性和隐私性。在发送消息之前,Alice 和 Bob(或所有相关方)必须生成一个密钥对,并安全地交换公钥。
Sodium 还提供了安全的密钥交换协议,但本文未对此进行介绍。
发送消息时,发送方使用接收方的公钥对其进行加密,并使用发送方的私钥对其进行签名。一旦消息被发送,接收方就用发送方的公钥对消息进行身份验证,并用接收方的私钥进行解密。
如果接收方不需要对收到的消息进行身份验证-以确保消息确实是由发送方发送的-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 的场景。
Sodium中 的 crypto_box_seal
Sodium 的 crypto_box_seal
函数用于使用一对公钥和私钥对消息进行加密和解密。与经过身份验证的非对称加密/解密之间的主要区别在于 crypt_box_seal
不会对消息进行身份验证。
Sodium 的 crypt_box_seal
功能与 OpenSSL 的 openssl_public_encrypt
和 openssl_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"