使用openssl_decrypt在php中解密AES-256-CBC,来自Sirtle Crypto JavaScript有效载荷

发布于 2025-01-28 02:56:35 字数 1813 浏览 3 评论 0 原文

我正在尝试使用webcrypto/window.crypto到aes-256-cbc中加密在js中进行 demrypt 它使用php的 openssl_decrypt 功能。

我的问题是,瘫痪功能只是返回 false ,因此似乎不起作用。

const encoder = new TextEncoder();
const encoded = encoder.encode('Hello this is a test.');

const encryptionKey = await window.crypto.subtle.generateKey(
    {
        name: 'AES-CBC',
        length: 256,
    },
    true,
    ['encrypt', 'decrypt'],
);

const iv = window.crypto.getRandomValues(new Uint8Array(16));
const cipher = await window.crypto.subtle.encrypt(
    {
        name: 'AES-CBC',
        iv,
    },
    encryptionKey,
    encoded,
);


const exportedKey = await window.crypto.subtle.exportKey(
    'jwk',
    encryptionKey,
);

console.log(exportedKey.k); 

sendToBackend({
    cipher: btoa(new Uint8Array(cipher)), // "MTMsMjIzLDE5NSwxNzYsMjA0LDE5MSwxOTYsMjEyLDIwNCwyMzAsMjcsMSwxMjAsMTQzLDE2MSwxMTgsMTYwLDIzOSw4NywyMDksMjQ0LDIwNCwyMzgsODYsMTgzLDIyOCwxMzksMjIwLDcwLDY5LDI0OSwxODQ="
    iv: btoa(new Uint8Array(iv)), // "MTQ5LDE2Nyw4LDE2NywyMjAsMTA4LDEwMSw1Niw4Miw3MiwxMjAsMjM5LDE4NCw0OCwyNTIsMTE=",
    password: exportedKey.k, // "szq1aOg-F_72vWrdJatWyQp3iOXIus-cE19sO4bSOLs"
});

现在,当我尝试使用PHP在后端解密它时,我会得到 false

$key = "szq1aOg-F_72vWrdJatWyQp3iOXIus-cE19sO4bSOLs";
$payload = "MTMsMjIzLDE5NSwxNzYsMjA0LDE5MSwxOTYsMjEyLDIwNCwyMzAsMjcsMSwxMjAsMTQzLDE2MSwxMTgsMTYwLDIzOSw4NywyMDksMjQ0LDIwNCwyMzgsODYsMTgzLDIyOCwxMzksMjIwLDcwLDY5LDI0OSwxODQ=";
$iv = "MTQ5LDE2Nyw4LDE2NywyMjAsMTA4LDEwMSw1Niw4Miw3MiwxMjAsMjM5LDE4NCw0OCwyNTIsMTE=";
$dec = openssl_decrypt($payload, 'AES-256-GCM', $key, false, $iv);
var_dump($dec); // false

我缺少一些东西吗?

I'm trying to encrypt something in JS using webcrypto/window.crypto to AES-256-CBC and trying to decrypt it using PHP's openssl_decrypt function.

My problem is that the decyrption function simply returns false and thus does not seem to work.

const encoder = new TextEncoder();
const encoded = encoder.encode('Hello this is a test.');

const encryptionKey = await window.crypto.subtle.generateKey(
    {
        name: 'AES-CBC',
        length: 256,
    },
    true,
    ['encrypt', 'decrypt'],
);

const iv = window.crypto.getRandomValues(new Uint8Array(16));
const cipher = await window.crypto.subtle.encrypt(
    {
        name: 'AES-CBC',
        iv,
    },
    encryptionKey,
    encoded,
);


const exportedKey = await window.crypto.subtle.exportKey(
    'jwk',
    encryptionKey,
);

console.log(exportedKey.k); 

sendToBackend({
    cipher: btoa(new Uint8Array(cipher)), // "MTMsMjIzLDE5NSwxNzYsMjA0LDE5MSwxOTYsMjEyLDIwNCwyMzAsMjcsMSwxMjAsMTQzLDE2MSwxMTgsMTYwLDIzOSw4NywyMDksMjQ0LDIwNCwyMzgsODYsMTgzLDIyOCwxMzksMjIwLDcwLDY5LDI0OSwxODQ="
    iv: btoa(new Uint8Array(iv)), // "MTQ5LDE2Nyw4LDE2NywyMjAsMTA4LDEwMSw1Niw4Miw3MiwxMjAsMjM5LDE4NCw0OCwyNTIsMTE=",
    password: exportedKey.k, // "szq1aOg-F_72vWrdJatWyQp3iOXIus-cE19sO4bSOLs"
});

Now when I try to decrypt this in the backend using PHP, I get false:

$key = "szq1aOg-F_72vWrdJatWyQp3iOXIus-cE19sO4bSOLs";
$payload = "MTMsMjIzLDE5NSwxNzYsMjA0LDE5MSwxOTYsMjEyLDIwNCwyMzAsMjcsMSwxMjAsMTQzLDE2MSwxMTgsMTYwLDIzOSw4NywyMDksMjQ0LDIwNCwyMzgsODYsMTgzLDIyOCwxMzksMjIwLDcwLDY5LDI0OSwxODQ=";
$iv = "MTQ5LDE2Nyw4LDE2NywyMjAsMTA4LDEwMSw1Niw4Miw3MiwxMjAsMjM5LDE4NCw0OCwyNTIsMTE=";
$dec = openssl_decrypt($payload, 'AES-256-GCM', $key, false, $iv);
var_dump($dec); // false

Is there something I am missing?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

花开雨落又逢春i 2025-02-04 02:56:35

在JavaScript侧,基本64编码失败,从结果的长度可以看出。在以下JavaScript代码中,函数 ab2b64()用于此转换:

(async () => {

    const encoder = new TextEncoder();
    const encoded = encoder.encode('Hello this is a test.');

    const encryptionKey = await window.crypto.subtle.generateKey(
        {
            name: 'AES-CBC',
            length: 256,
        },
        true,
        ['encrypt', 'decrypt'],
    );

    const iv = window.crypto.getRandomValues(new Uint8Array(16));
    const cipher = await window.crypto.subtle.encrypt(
        {
            name: 'AES-CBC',
            iv,
        },
        encryptionKey,
        encoded,
    );

    const exportedKey = await window.crypto.subtle.exportKey(
        'jwk',
        encryptionKey,
    );

    console.log("key (Base64url): " + exportedKey.k)
    console.log("iv (Base64): " + ab2b64(iv))
    console.log("ciphertext (Base64): " + ab2b64(cipher))

    // https://stackoverflow.com/a/11562550/9014097
    function ab2b64(arrayBuffer) {
        return window.btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));
    }

})();

可能的输出:

key (Base64url): 2TCb_J7EnsXgpYRhrJUG4ChgDNcnpcZ4sSCOK739U8A
iv (Base64): cWXBcXDyEcKTSRi2zPsqrg==
ciphertext (Base64): DRFokcfbdsfhNz/IeFUdmUQzxEAg09Y+gTE1DTfmzoA=

在PHP侧使用了错误的模式,IE GCM必须由CBC替换为与JavaScript代码的兼容性(尽管GCM实际上是更安全的选择)。

此外,钥匙必须是base64url(不是base64),而iv和ciphertext必须是基础64。对于ciphertext and intagit base64解码可以通过设置 openssl_decrypt() to 0 0 :

<?php
$keyB64url = "2TCb_J7EnsXgpYRhrJUG4ChgDNcnpcZ4sSCOK739U8A";
$keyB64 = str_replace(['-','_'], ['+','/'], $keyB64url );
$key = base64_decode($keyB64);
$iv = base64_decode("cWXBcXDyEcKTSRi2zPsqrg==");
$payload = "DRFokcfbdsfhNz/IeFUdmUQzxEAg09Y+gTE1DTfmzoA=";
$dec = openssl_decrypt($payload, 'AES-256-CBC', $key, 0, $iv);
var_dump($dec); // string(21) "Hello this is a test."
?>

edit: edit:

虽然CBC仅提供机密性,但

对于JavaScript侧的GCM,必须从 AES> aes-cbc gentatekey() and codekekey()中的算法() GCM 。 GCM的NONCE的建议长度为12个字节(尽管支持16个字节在内的其他非CEN长度),这需要在> getrandomvalues()中进行相应的更改:

(async () => {

    const encoder = new TextEncoder();
    const encoded = encoder.encode('Hello this is a test.');

    const encryptionKey = await window.crypto.subtle.generateKey(
        {
            name: 'AES-GCM',
            length: 256,
        },
        true,
        ['encrypt', 'decrypt'],
    );

    const iv = window.crypto.getRandomValues(new Uint8Array(12));
    const cipher = await window.crypto.subtle.encrypt(
        {
            name: 'AES-GCM',
            iv,
        },
        encryptionKey,
        encoded,
    );

    const exportedKey = await window.crypto.subtle.exportKey(
        'jwk',
        encryptionKey,
    );

    console.log("key (Base64url): " + exportedKey.k)
    console.log("iv (Base64): " + ab2b64(iv))
    console.log("ciphertext (Base64): " + ab2b64(cipher))

    // https://stackoverflow.com/a/11562550/9014097
    function ab2b64(arrayBuffer) {
        return window.btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));
    }

})();

可能的输出是:

key (Base64url): jno7Ydkris18yDtJ2nvPeWBrdiPqmqoZheYcc0qpjO8
iv (Base64): zETSdleg3nYdTbDv
ciphertext (Base64): HiPRcKl3MhzG+U4gKnpnK44hl9jqIzunMd15WnM9l4XkCjylXg==

如上所述,GCM是一种身份验证的加密模式,并使用标签进行身份验证。 WebCrypto隐式串联ciphertext和TAG(默认情况下为16个字节),而PHP则分别处理。因此,必须将密文和标签在PHP侧分开:

<?php
$keyB64url = "jno7Ydkris18yDtJ2nvPeWBrdiPqmqoZheYcc0qpjO8";
$keyB64 = str_replace(['-','_'], ['+','/'], $keyB64url );
$key = base64_decode($keyB64);
$iv = base64_decode("zETSdleg3nYdTbDv");
$payload = base64_decode("HiPRcKl3MhzG+U4gKnpnK44hl9jqIzunMd15WnM9l4XkCjylXg==");
$payloadLen = strlen($payload);
$ciphertext = substr($payload, 0, $payloadLen - 16);
$tag = substr($payload, $payloadLen - 16, 16);
$dec = openssl_decrypt($ciphertext, 'AES-256-GCM', $key, OPENSSL_RAW_DATA, $iv, $tag);
var_dump($dec); // string(21) "Hello this is a test."
?>

On the JavaScript side, the Base64 encoding fails, as can be seen from the length of the result. In the following JavaScript code the function ab2b64() is used for this conversion:

(async () => {

    const encoder = new TextEncoder();
    const encoded = encoder.encode('Hello this is a test.');

    const encryptionKey = await window.crypto.subtle.generateKey(
        {
            name: 'AES-CBC',
            length: 256,
        },
        true,
        ['encrypt', 'decrypt'],
    );

    const iv = window.crypto.getRandomValues(new Uint8Array(16));
    const cipher = await window.crypto.subtle.encrypt(
        {
            name: 'AES-CBC',
            iv,
        },
        encryptionKey,
        encoded,
    );

    const exportedKey = await window.crypto.subtle.exportKey(
        'jwk',
        encryptionKey,
    );

    console.log("key (Base64url): " + exportedKey.k)
    console.log("iv (Base64): " + ab2b64(iv))
    console.log("ciphertext (Base64): " + ab2b64(cipher))

    // https://stackoverflow.com/a/11562550/9014097
    function ab2b64(arrayBuffer) {
        return window.btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));
    }

})();

Possible output:

key (Base64url): 2TCb_J7EnsXgpYRhrJUG4ChgDNcnpcZ4sSCOK739U8A
iv (Base64): cWXBcXDyEcKTSRi2zPsqrg==
ciphertext (Base64): DRFokcfbdsfhNz/IeFUdmUQzxEAg09Y+gTE1DTfmzoA=

On the PHP side the wrong mode is used, i.e. GCM must be replaced by CBC for compatibility with the JavaScript code (although GCM would actually be the more secure choice).
Furthermore, the key must be Base64url (not Base64) decoded, while IV and ciphertext must be Base64 decoded. For the ciphertext an implicit Base64 decoding can be done by setting the 4th parameter of openssl_decrypt() to 0:

<?php
$keyB64url = "2TCb_J7EnsXgpYRhrJUG4ChgDNcnpcZ4sSCOK739U8A";
$keyB64 = str_replace(['-','_'], ['+','/'], $keyB64url );
$key = base64_decode($keyB64);
$iv = base64_decode("cWXBcXDyEcKTSRi2zPsqrg==");
$payload = "DRFokcfbdsfhNz/IeFUdmUQzxEAg09Y+gTE1DTfmzoA=";
$dec = openssl_decrypt($payload, 'AES-256-CBC', $key, 0, $iv);
var_dump($dec); // string(21) "Hello this is a test."
?>

Edit:

While CBC only provides confidentiality, GCM provides confidentiality and authenticity/integrity, making GCM more secure. Note that with CBC, a message authentication code (MAC) can be used so that (in addition to confidentiality) also authenticity is provided; however, the advantage of GCM is that this is done implicitly.

For GCM on the JavaScript side the algorithm in generateKey() and encrypt() must be changed from AES-CBC to AES-GCM. The recommended length of the nonce for GCM is 12 bytes (although other nonce lengths including 16 bytes are supported), which requires a corresponding change in getRandomValues():

(async () => {

    const encoder = new TextEncoder();
    const encoded = encoder.encode('Hello this is a test.');

    const encryptionKey = await window.crypto.subtle.generateKey(
        {
            name: 'AES-GCM',
            length: 256,
        },
        true,
        ['encrypt', 'decrypt'],
    );

    const iv = window.crypto.getRandomValues(new Uint8Array(12));
    const cipher = await window.crypto.subtle.encrypt(
        {
            name: 'AES-GCM',
            iv,
        },
        encryptionKey,
        encoded,
    );

    const exportedKey = await window.crypto.subtle.exportKey(
        'jwk',
        encryptionKey,
    );

    console.log("key (Base64url): " + exportedKey.k)
    console.log("iv (Base64): " + ab2b64(iv))
    console.log("ciphertext (Base64): " + ab2b64(cipher))

    // https://stackoverflow.com/a/11562550/9014097
    function ab2b64(arrayBuffer) {
        return window.btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));
    }

})();

A possible output is:

key (Base64url): jno7Ydkris18yDtJ2nvPeWBrdiPqmqoZheYcc0qpjO8
iv (Base64): zETSdleg3nYdTbDv
ciphertext (Base64): HiPRcKl3MhzG+U4gKnpnK44hl9jqIzunMd15WnM9l4XkCjylXg==

As mentioned above, GCM is an authenticated encryption mode and uses a tag for authentication. WebCrypto implicitly concatenates ciphertext and tag (16 bytes long by default) in this order, while PHP processes both separately. Therefore, ciphertext and tag must be separated on the PHP side:

<?php
$keyB64url = "jno7Ydkris18yDtJ2nvPeWBrdiPqmqoZheYcc0qpjO8";
$keyB64 = str_replace(['-','_'], ['+','/'], $keyB64url );
$key = base64_decode($keyB64);
$iv = base64_decode("zETSdleg3nYdTbDv");
$payload = base64_decode("HiPRcKl3MhzG+U4gKnpnK44hl9jqIzunMd15WnM9l4XkCjylXg==");
$payloadLen = strlen($payload);
$ciphertext = substr($payload, 0, $payloadLen - 16);
$tag = substr($payload, $payloadLen - 16, 16);
$dec = openssl_decrypt($ciphertext, 'AES-256-GCM', $key, OPENSSL_RAW_DATA, $iv, $tag);
var_dump($dec); // string(21) "Hello this is a test."
?>
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文