PHP中带认证的对称加密解密偶尔失效问题求助
Hey DevelJoe,我仔细扒了你的代码,发现几个很可能导致解密偶尔出问题的点,咱们一个个捋清楚:
1. 加密函数的致命语法错误
你看这段加密代码:
$cipher = sodium_crypto_secretbox( random_bytes(8) . $secret_code $nonce, $key );
在random_bytes(8) . $secret_code和$nonce之间少了一个逗号!这会让PHP把这两个表达式直接拼接成一个字符串,当成sodium_crypto_secretbox的第一个参数(明文),原本该当第二个参数的nonce直接错位成了第三个,key变成了多余的参数被忽略。
这种错误不会每次都直接报错(取决于PHP错误级别),但会彻底打乱加密逻辑:你实际加密的内容是随机8字节 + 整数字符串 + nonce,而非预期的随机8字节 + 整数字符串。解密时用正确的nonce去解混乱的密文,偶尔就会碰出错误的明文内容,而不是直接解密失败。
2. 整数转字符串拼接的不确定性
你把整数$secret_code直接和二进制字符串拼接,PHP会自动把整数转成十进制文本字符串(比如152变成"152",占3个ASCII字节)。这种方式有两个隐患:
- 不同长度的整数对应的字符串字节数不一样,比如99是2字节,100是3字节
- 一旦加密逻辑出问题(比如上面的语法错误),解密后取出的文本字符串很容易混入多余字符,转成错误的整数
正确的做法是把整数打包成固定长度的二进制数据,比如用pack函数:
// 加密时:把整数转成4字节无符号大端二进制 $plaintext = random_bytes(8) . pack('N', $secret_code);
解密时再用unpack还原:
// 解密后取第8到第11字节(共4字节)还原整数 $secret_code_packed = mb_substr($secret_code_string, 8, 4, '8bit'); $secret_code = unpack('N', $secret_code_packed)[1];
这样不管整数是多少,都是固定4字节,彻底避免文本字符串的不确定性。
3. 解密时的key变量问题
你的加密代码最后调用了sodium_memzero($key)把key清零,但解密代码里直接用了$key变量——如果没有重新从安全存储加载key的话,这里的$key已经是空的了。虽然你说key是用更安全的方式生成的,但一定要确认解密时的key是重新正确获取的,否则解密要么失败,要么出错误内容。
修复后的完整示例代码
我把你的代码修正后整理出来了,你可以参考:
// 加密部分 $secret_code = 152; $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); // 从安全存储加载key(示例用你提供的base64) $key = sodium_base642bin( 'Jdjkfhuehrfkjshdfkjhskjfoshdfishf', SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING ); // 把整数转成固定4字节二进制,再和随机8字节拼接 $plaintext = random_bytes(8) . pack('N', $secret_code); // 修正参数逗号,按正确顺序传参 $cipher = sodium_crypto_secretbox($plaintext, $nonce, $key); sodium_memzero($key); // 拼接nonce和密文后转base64 $ciphertext_base64 = sodium_bin2base64( $nonce . $cipher, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING ); sodium_memzero($nonce); sodium_memzero($cipher); // 解密部分 // 重新加载key(必须和加密时的key完全一致) $key = sodium_base642bin( 'Jdjkfhuehrfkjshdfkjhskjfoshdfishf', SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING ); $ciphertext = sodium_base642bin( $ciphertext_base64, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING ); $nonce = mb_substr($ciphertext, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit'); $cipher = mb_substr($ciphertext, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit'); $secret_code_string = sodium_crypto_secretbox_open($cipher, $nonce, $key); // 必须判断解密是否成功 if ($secret_code_string === false) { die('解密失败:密文被篡改或key/nonce错误'); } sodium_memzero($key); sodium_memzero($nonce); sodium_memzero($cipher); // 从第8字节开始取4字节,还原整数 $secret_code_packed = mb_substr($secret_code_string, 8, 4, '8bit'); $secret_code = unpack('N', $secret_code_packed)[1]; echo $secret_code; // 应该稳定输出152
额外提醒
- 一定要检查
sodium_crypto_secretbox_open的返回值,如果是false,说明密文被篡改、key错误或者nonce不匹配,绝对不能继续处理 - 用
pack/unpack处理整数能保证二进制数据的固定长度,避免文本字符串带来的各种意外 - 加密和解密必须使用完全相同的key,key的存储和加载一定要保证安全
备注:内容来源于stack exchange,提问作者DevelJoe

