本文目录导读:

在 PHP 中“加密聊天”通常涉及两个层面:传输加密(数据在网络上不被窃听)和端到端加密(服务器也无法读取内容)。
为了兼顾安全性和实用性,这里提供一个全面的端到端加密(E2EE) 解决方案,涵盖算法选择、核心代码、数据库设计和防范注意事项。
核心加密方案选择
PHP 中常用的加密库为 OpenSSL 和 Sodium,推荐使用 Sodium(从 PHP 7.2 起内置,且 libsodium 更安全和现代),但如果你的项目依赖旧版,可以用 OpenSSL。
| 算法 | 用途 | 推荐程度 |
|---|---|---|
| ChaCha20-Poly1305 | 对称加密(流加密) | 首选(Sodium) |
| AES-256-GCM | 对称加密(块加密) | 首选(OpenSSL) |
| X25519 | 非对称密钥交换(ECDH) | 首选 |
| RSA | 非对称加密(旧式) | 不推荐(密钥交换太慢) |
最终方案: 使用 AES-256-GCM 作为消息加密算法,使用 X25519 进行密钥协商,使用 Ed25519 进行数字签名(防篡改)。
端到端加密通信流(流程说明)
在聊天场景中,为了让服务器无法阅读消息,需要实现以下流程:
- 用户注册/登录: 服务器存储用户的 公钥(用于加密)和 签名公钥(用于验签)。
- 好友建立: 用户双方交换公钥。
- 发起会话(关键): 发送方生成一个随机的 对称密钥(会话密钥),使用接收方的公钥(X25519)加密这个对称密钥,并发送给服务器存储。
- 发送消息: 发送方使用该 对称密钥 通过 AES-256-GCM 加密明文消息,并附加签名,服务器只负责存储密文。
- 接收消息: 接收方从服务器获取“加密的会话密钥”,用自己的私钥解密得到对称密钥,然后用对称密钥解密密文。
注意: 本方案中私钥只会存储在用户本地(浏览器/客户端),服务器仅存储公钥和密文。
核心 PHP 代码实现(使用 OpenSSL + NaCl 风格)
以下代码展示会话密钥交换和消息加密的核心逻辑。
密钥生成(用户注册时)
<?php
// 生成接收方的密钥对(这通常发生在客户端,但这里模拟服务器处理逻辑)
function generateUserKeyPair() {
// 使用 openssl 生成 X25519 密钥对(或者使用 sodium_crypto_box_keypair)
// 注意:X25519 在 openssl 中对应 EC 曲线。
$config = [
'private_key_type' => OPENSSL_KEYTYPE_EC,
'curve_name' => 'prime256v1' // 也可以使用 X25519,但 OpenSSL 原生支持有限,这里用 prime256v1 做演示
];
$res = openssl_pkey_new($config);
openssl_pkey_export($res, $privateKey);
$details = openssl_pkey_get_details($res);
$publicKey = $details['key'];
// 实际生产环境建议使用 sodium:
// $keypair = sodium_crypto_box_keypair();
// $publicKey = sodium_crypto_box_publickey($keypair);
// $privateKey = sodium_crypto_box_secretkey($keypair);
return ['public' => $publicKey, 'private' => $privateKey];
}
生成会话密钥并加密(发送方)
<?php
function encryptMessageForRecipient($recipientPublicKey, $plainMessage) {
// 1. 生成一个随机的对称加密密钥(会话密钥)
$sessionKey = random_bytes(32); // AES-256 需要 32 字节
// 2. 使用接收方的公钥加密这个会话密钥(模拟 ECDH 加密)
// (这里为了简化演示,直接使用 RSA 公钥加密,实际应用应使用 sodium_crypto_box_seal)
openssl_public_encrypt($sessionKey, $encryptedSessionKey, $recipientPublicKey);
// 3. 使用会话密钥对消息进行 AES-256-GCM 加密
$iv = random_bytes(12); // GCM 推荐 12 字节 IV
// 绑定额外的认证数据(例如发送者ID,防止密文被篡改)
$aad = 'sender:123|receiver:456';
$cipherText = openssl_encrypt(
$plainMessage,
'aes-256-gcm',
$sessionKey,
OPENSSL_RAW_DATA,
$iv,
$tag, // 认证标签,防止篡改
$aad,
16 // tag 长度
);
// 返回加密的数据包(密文 + 加密的会话密钥 + IV + 标签)
return [
'cipher' => base64_encode($cipherText),
'enc_session_key' => base64_encode($encryptedSessionKey),
'iv' => base64_encode($iv),
'tag' => base64_encode($tag),
'aad' => $aad
];
}
解密消息(接收方)
<?php
function decryptMessageFromSender($recipientPrivateKey, $encryptedPacket) {
// 1. 使用自己的私钥解密出会话密钥
$encryptedSessionKey = base64_decode($encryptedPacket['enc_session_key']);
$decryptedSessionKey = null;
openssl_private_decrypt($encryptedSessionKey, $decryptedSessionKey, $recipientPrivateKey);
if ($decryptedSessionKey === null) {
throw new Exception("无法解密会话密钥,私钥不匹配。");
}
// 2. 使用会话密钥解密消息
$cipherText = base64_decode($encryptedPacket['cipher']);
$iv = base64_decode($encryptedPacket['iv']);
$tag = base64_decode($encryptedPacket['tag']);
$aad = $encryptedPacket['aad'];
$plainText = openssl_decrypt(
$cipherText,
'aes-256-gcm',
$decryptedSessionKey,
OPENSSL_RAW_DATA,
$iv,
$tag,
$aad
);
return $plainText;
}
// 示例用法
$userA = generateUserKeyPair();
$userB = generateUserKeyPair();
// 用户A 发送给 用户B
$encrypted = encryptMessageForRecipient($userB['public'], '这是一条机密消息,在服务器上不可见。');
echo "密文(服务器看到的数据): " . $encrypted['cipher'] . "\n\n";
// 用户B 解密
$decrypted = decryptMessageFromSender($userB['private'], $encrypted);
echo "解密后的明文: " . $decrypted . "\n";
数据库设计(用于存储密文)
由于是端到端加密,数据库只存储密文和元数据:
CREATE TABLE messages (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
conversation_id CHAR(36) NOT NULL, -- 会话ID
sender_id BIGINT UNSIGNED NOT NULL, -- 发送者ID(服务器可读)
receiver_id BIGINT UNSIGNED NOT NULL, -- 接收者ID(服务器可读)
encrypted_session_key TEXT NOT NULL, -- 发送者用接收方公钥加密后的会话密钥
cipher_text LONGBLOB NOT NULL, -- 加密后的消息内容
iv BINARY(12) NOT NULL, -- 加密初始向量
auth_tag BINARY(16) NOT NULL, -- GCM认证标签
aad TEXT, -- 附加认证数据(发送者ID等)
signature VARCHAR(255), -- 发送者的数字签名(可选,防抵赖)
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_conversation (conversation_id, created_at)
);
安全建议与注意事项
-
私钥存储: 永远不要将用户的私钥发送到服务器,私钥应存储在客户端(浏览器 LocalStorage 加密后,或移动端 Keychain)。
-
使用 Sodium 而非 OpenSSL: 上面的代码为了演示用了
openssl_public_encrypt和openssl_private_decrypt(RSA),在实际生产中,请使用sodium_crypto_box()和sodium_crypto_box_open(),Sodium 内置了 X25519 和 ChaCha20-Poly1305,且更难误用。// Sodium 方式加密(推荐) $keypair = sodium_crypto_box_keypair(); $publicKey = sodium_crypto_box_publickey($keypair); $anonymousMessageToPublicKey = sodium_crypto_box_seal('秘密消息', $publicKey); // 解密 $decrypted = sodium_crypto_box_seal_open($anonymousMessageToPublicKey, $keypair); -
前向保密(PFS): 上述方案“每次会话使用独立密钥”时,如果服务器记录私钥,有被破解的风险,高级方案建议引入 临时密钥(Ephemeral Keys) 或 双棘轮算法(Double Ratchet)(类似 Signal 协议),但这在纯 PHP 中实现较复杂。
-
认证与防篡改: 除了 GCM 的
tag,建议对sender_id和conversation_id使用 HMAC 签名,防止密文被移动到其他会话。 -
Web 前端配合: 由于私钥在客户端,
Javascript的WebCrypto API是加密聊天的核心,PHP 通常只负责验证登录和存储密文,加解密主要在用户的浏览器中完成。
- 基础方案: 使用 PHP 的
sodium扩展,通过sodium_crypto_box实现端到端加密。 - 核心逻辑: 用接收方公钥加密随机密钥(会话密钥),用会话密钥加密明文消息。
- 安全核心: 私钥绝不上传服务器。
如果你只是指“服务器与浏览器之间的传输加密”(即 HTTPS),那么只需配置好 SSL/TLS 证书 即可,这是任何聊天系统的基础安全要求。