С подсказкой от
ab1 стало понятно что в PHP ключ (key) не декодируется из base64, а используется как есть. Т.е. из этой строки используются только 32 байта, а остальное просто мусор.
И тогда всё работает.
# key
$ echo qEExPE+jkJxQUt8fSO+XwzXgRGh6kLHy+lWEe6Z8T6s= | xxd -l 32 -ps -c 256
7145457850452b6a6b4a785155743866534f2b58777a5867524768366b4c4879
# iv
$ echo jPNGTNLtGIBc7Jv2UXj7a3FNQk13eUJ5T3VIUXlOS0ZVOEpnMUpPMnhvQXg5bE5kMGFHejVxaTFnYTA9 | base64 -d | xxd -l 16 -ps
8cf3464cd2ed18805cec9bf65178fb6b
# encrypted text
$ echo jPNGTNLtGIBc7Jv2UXj7a3FNQk13eUJ5T3VIUXlOS0ZVOEpnMUpPMnhvQXg5bE5kMGFHejVxaTFnYTA9 | base64 -d | cut -b 17-
qMBMwyByOuHQyNKFU8Jg1JO2xoAx9lNd0aGz5qi1ga0=
# результат
$ echo qMBMwyByOuHQyNKFU8Jg1JO2xoAx9lNd0aGz5qi1ga0= | openssl AES-256-CBC -d -a -K 7145457850452b6a6b4a785155743866534f2b58777a5867524768366b4c4879 -iv 8CF3464CD2ED18805CEC9BF65178FB6B
/playlist.m3u8+1729705980
Ну и код на JS используя встроенный
crypto.subtle:
// Вспомогательные функции
const B = (str) => Uint8Array.from(str, c => c.charCodeAt(0));
const S = (buf) => String.fromCharCode(...new Uint8Array(buf));
// дано
const encryptedData = "jPNGTNLtGIBc7Jv2UXj7a3FNQk13eUJ5T3VIUXlOS0ZVOEpnMUpPMnhvQXg5bE5kMGFHejVxaTFnYTA9";
const key = "qEExPE+jkJxQUt8fSO+XwzXgRGh6kLHy+lWEe6Z8T6s=";
const iv = B(atob(encryptedData).slice(0, 16));
const cryptoKey = await crypto.subtle.importKey(
'raw',
// На самом деле используется только 32 байта из key
B(key.slice(0, 32)),
'AES-CBC',
true,
['encrypt', 'decrypt']
);
const encryptedText = B(atob(atob(encryptedData).slice(16)));
let decrypted = S(await crypto.subtle.decrypt({ name: 'AES-CBC', iv }, cryptoKey, encryptedText));
console.log(decrypted); // /playlist.m3u8+1729705980