You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 17:44:51