如何用k6 WebCrypto实现与PHP openssl_encrypt一致的加密
问题描述
我有一段PHP加密文本的代码如下:
$key = '1234aaaff80b56233525ac2355ac3456'; // 类似这样的密钥 $utf16Content = \mb_convert_encoding('Some text', 'UTF-16LE'); $cipher = 'aes-256-gcm'; $nonceLength = \openssl_cipher_iv_length($cipher); $nonce = \openssl_random_pseudo_bytes($nonceLength); $encryptedContent = \openssl_encrypt($utf16Content, $cipher, $key, $options=0, $nonce, $tag); echo 'Nonce: '.\base64_encode($nonce); echo 'Auth Tag: '.\base64_encode($tag); echo 'Content: '.$encryptedContent;
我需要在JS WebCrypto中实现相同的行为(用于k6测试),但目前写的代码无法成功解密,推测问题出在生成正确的authTag以及字符串密钥转ArrayBuffer的步骤上。我的JS代码如下:
const rawKey = Uint8Array.from(new String('1234aaaff80b56233525ac2355ac3456'), (x) => x.charCodeAt(0)); const key = await crypto.subtle.importKey( 'raw', rawKey, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt'] ); const nonce = crypto.getRandomValues(new Uint8Array(12)); const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce, tagLength: 128, }, key, stringToArrayBuffer('Some text') ); const sentData = {}; sentData.content = b64encode(encrypted); sentData.nonce = b64encode(nonce); sentData.authTag = b64encode(GetTag(encrypted, 128)); function stringToArrayBuffer(str) { const buf = new ArrayBuffer(str.length * 2); const bufView = new Uint16Array(buf); for (let i = 0, strLen = str.length; i < strLen; i++) { bufView[i] = str.charCodeAt(i); } return buf; } function GetTag(encrypted, tagLength) { if (tagLength === void 0) tagLength = 128; return encrypted.slice(encrypted.byteLength - ((tagLength + 7) >> 3)) }
对应的PHP解密代码如下:
// $content, $nonce, $authenticationTag 来自PHP加密脚本的输出 $content = \base64_decode($content, true); $nonce = \base64_decode($nonce, true); $authenticationTag = \base64_decode($authenticationTag, true); $result = \openssl_decrypt( $content, 'aes-256-gcm', '1234aaaff80b56233525ac2355ac3456', \OPENSSL_RAW_DATA, $nonce, $authenticationTag );
解决方法
你的代码存在三个核心问题,修正后即可和PHP逻辑完全对齐:
1. 明文编码:强制转为UTF-16LE(小端序)
PHP中明确使用mb_convert_encoding将明文转为UTF-16LE,但原JS的stringToArrayBuffer依赖主机字节序,在大端机器上会生成UTF-16BE编码,导致明文不一致。需要手动处理小端字节:
function stringToUtf16LEBuffer(str) { const buf = new ArrayBuffer(str.length * 2); const view = new DataView(buf); for (let i = 0; i < str.length; i++) { // 小端序写入:低字节在前,高字节在后 view.setUint16(i * 2, str.charCodeAt(i), true); } return buf; }
2. 密文与标签的拆分逻辑
WebCrypto的encrypt返回的是密文+认证标签拼接的ArrayBuffer(密文在前,16字节标签在后),而PHP的openssl_encrypt是单独返回密文(base64编码)和标签。因此需要拆分加密结果:
const tagLength = 16; // 128位标签对应16字节 const ciphertext = encrypted.slice(0, encrypted.byteLength - tagLength); const tag = encrypted.slice(encrypted.byteLength - tagLength);
将拆分后的ciphertext做base64编码作为sentData.content,而非整个encrypted。
3. 密钥处理优化(可选但更简洁)
原JS代码中new String()是多余的,直接用字符串生成Uint8Array即可:
const rawKey = Uint8Array.from('1234aaaff80b56233525ac2355ac3456', c => c.charCodeAt(0));
完整修正后的JS代码
async function encryptData() { // 处理密钥 const rawKey = Uint8Array.from('1234aaaff80b56233525ac2355ac3456', c => c.charCodeAt(0)); const key = await crypto.subtle.importKey( 'raw', rawKey, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt'] ); // 生成12字节nonce(AES-GCM推荐长度) const nonce = crypto.getRandomValues(new Uint8Array(12)); // 明文转UTF-16LE const plaintextBuffer = stringToUtf16LEBuffer('Some text'); // 加密 const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce, tagLength: 128 }, key, plaintextBuffer); // 拆分密文和标签 const tagLength = 16; const ciphertext = encrypted.slice(0, encrypted.byteLength - tagLength); const tag = encrypted.slice(encrypted.byteLength - tagLength); // 转为base64 const sentData = { content: b64encode(ciphertext), nonce: b64encode(nonce), authTag: b64encode(tag) }; return sentData; } // 工具函数:字符串转UTF-16LE ArrayBuffer function stringToUtf16LEBuffer(str) { const buf = new ArrayBuffer(str.length * 2); const view = new DataView(buf); for (let i = 0; i < str.length; i++) { view.setUint16(i * 2, str.charCodeAt(i), true); } return buf; } // 工具函数:ArrayBuffer转base64(k6环境可用) function b64encode(buffer) { return btoa(String.fromCharCode(...new Uint8Array(buffer))); }
验证说明
修正后的代码和PHP逻辑完全对齐:
- 密钥均以ASCII字节序列作为AES-256密钥
- 明文均转为UTF-16LE字节序列
- 加密后密文和标签分离,分别base64编码后传输
- PHP解密代码无需修改,可直接解密JS生成的内容
内容的提问来源于stack exchange,提问作者RobertC
相关产品推荐
相关产品推荐

