使用 PHP 中的 openssl_decrypt 从 Subtle Crypto Javascript Payload 解密 AES-256-CBC
Decrypt AES-256-CBC using openssl_decrypt in PHP from Subtle Crypto Javascript Payload
我正在尝试使用 webcrypto/window.crypto 到 AES-256-CBC encrypt JS 中的某些内容并尝试 decrypt 它使用 PHP 的 openssl_decrypt
函数。
我的问题是解密功能只是 returns 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
有什么我遗漏的吗?
JavaScript这边,Base64编码失败,从结果的长度可以看出。在以下 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))
//
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 方面使用了错误的模式,即 GCM 必须替换为 CBC 以与 JavaScript 代码兼容(尽管 GCM 实际上是更安全的选择)。
此外,密钥必须是 Base64url(不是 Base64)解码,而 IV 和密文必须是 Base64 解码。对于密文,可以通过将 openssl_decrypt()
的第 4 个参数设置为 0
:
来完成 implicit Base64 解码
<?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."
?>
编辑:
虽然 CBC 仅提供机密性,但 GCM 提供机密性和 authenticity/integrity,使 GCM 更加安全。请注意,对于 CBC,可以使用消息验证码 (MAC),以便(除了机密性之外)还提供真实性;然而,GCM 的优势在于,这是 隐式 .
完成的
对于 JavaScript 端的 GCM,generateKey()
和 encrypt()
中的算法必须从 AES-CBC
更改为 AES-GCM
。 GCM 推荐的 nonce 长度为 12 字节(虽然也支持包括 16 字节在内的其他 nonce 长度),这需要在 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))
//
function ab2b64(arrayBuffer) {
return window.btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));
}
})();
可能的输出是:
key (Base64url): jno7Ydkris18yDtJ2nvPeWBrdiPqmqoZheYcc0qpjO8
iv (Base64): zETSdleg3nYdTbDv
ciphertext (Base64): HiPRcKl3MhzG+U4gKnpnK44hl9jqIzunMd15WnM9l4XkCjylXg==
如前所述,GCM是一种鉴权加密方式,使用tag进行鉴权。 WebCrypto 按此顺序隐式连接密文和标签(默认 16 字节长),而 PHP 分别处理两者。所以密文和tag必须在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."
?>
我正在尝试使用 webcrypto/window.crypto 到 AES-256-CBC encrypt JS 中的某些内容并尝试 decrypt 它使用 PHP 的 openssl_decrypt
函数。
我的问题是解密功能只是 returns 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
有什么我遗漏的吗?
JavaScript这边,Base64编码失败,从结果的长度可以看出。在以下 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))
//
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 方面使用了错误的模式,即 GCM 必须替换为 CBC 以与 JavaScript 代码兼容(尽管 GCM 实际上是更安全的选择)。
此外,密钥必须是 Base64url(不是 Base64)解码,而 IV 和密文必须是 Base64 解码。对于密文,可以通过将 openssl_decrypt()
的第 4 个参数设置为 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."
?>
编辑:
虽然 CBC 仅提供机密性,但 GCM 提供机密性和 authenticity/integrity,使 GCM 更加安全。请注意,对于 CBC,可以使用消息验证码 (MAC),以便(除了机密性之外)还提供真实性;然而,GCM 的优势在于,这是 隐式 .
完成的对于 JavaScript 端的 GCM,generateKey()
和 encrypt()
中的算法必须从 AES-CBC
更改为 AES-GCM
。 GCM 推荐的 nonce 长度为 12 字节(虽然也支持包括 16 字节在内的其他 nonce 长度),这需要在 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))
//
function ab2b64(arrayBuffer) {
return window.btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));
}
})();
可能的输出是:
key (Base64url): jno7Ydkris18yDtJ2nvPeWBrdiPqmqoZheYcc0qpjO8
iv (Base64): zETSdleg3nYdTbDv
ciphertext (Base64): HiPRcKl3MhzG+U4gKnpnK44hl9jqIzunMd15WnM9l4XkCjylXg==
如前所述,GCM是一种鉴权加密方式,使用tag进行鉴权。 WebCrypto 按此顺序隐式连接密文和标签(默认 16 字节长),而 PHP 分别处理两者。所以密文和tag必须在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."
?>