1

I'm trying to encrypt/decrypt symmetrically from php/phpseclib to js/WebCrypto(SubtleCrypto). The algorithm is AES-GCM with PBKDF2 derivation and also with plain key. I had no success. The error received from the window.crypto.subtle.decrypt() function is:

OperationError: The operation failed for an operation-specific reason

RSA-OAEP works without any problems.

Did anybody do this before - is it possible at all? I didn't find anything that confirms or denies a compatibility between these modules.

Edit: adding code example

encryption:

<?php
require_once($_SERVER['DOCUMENT_ROOT'] . '/../vendor/autoload.php');
use phpseclib3\Crypt\AES;

$TEST_AES_KEY = "TWw4QCkeZEnXoCDkI1GEHQ==";
$TEST_AES_IV = "CRKTyQoWdWB2n56f";
$message = "123&abc";

$aes = new AES('gcm');
$aes->setKey($TEST_AES_KEY);
$aes->setNonce($TEST_AES_IV);

$ciphertext = $aes->encrypt($message);
$tag = $aes->getTag();
$ciphertextBase64 = base64_encode($ciphertext . $tag);
echo $ciphertextBase64;

decryption:

<!DOCTYPE html>
<html>
<script>
    function _base64ToArrayBuffer(base64) {
        var binary_string   = atob(base64);
        var len             = binary_string.length;
        var bytes           = new Uint8Array(len);
        for (var i = 0; i < len; i++) {
            bytes[i] = binary_string.charCodeAt(i);
        }
        return bytes.buffer;
    }

    async function _importKeyAes(key) {
        return await window.crypto.subtle.importKey(
            "raw",
            key,
            { name: "AES-GCM" },
            false,
            ["encrypt", "decrypt"]
        );
    }

    async function decryptMessageSymetric(key, data, iv) {
        keyArrayBuffer  = _base64ToArrayBuffer(key);
        key             = await _importKeyAes(keyArrayBuffer);
        iv              = _base64ToArrayBuffer(iv);
        data            = _base64ToArrayBuffer(data);
        result = await window.crypto.subtle.decrypt(
            { name: "AES-GCM", iv: iv, tagLength: 128 },
            key,
            data
        );
        return new TextDecoder().decode(result);
    }

    TEST_AES_KEY = "TWw4QCkeZEnXoCDkI1GEHQ==";
    TEST_AES_IV = "CRKTyQoWdWB2n56f";
    messageEncrypted = "LATYboD/FztIKGVkiJNWHOP72C77FiY="; // result from phpseclib encryption

    result = decryptMessageSymetric(TEST_AES_KEY, messageEncrypted, TEST_AES_IV);
    console.log(result);
</script>
</html>
Topaco
  • 40,594
  • 4
  • 35
  • 62
stk
  • 55
  • 6
  • 1
    I see no reason why it should not work. AES, GCM, PBKDF2 are supported by both libraries. Post executable codes with test data. – Topaco Aug 23 '22 at 14:34
  • While writing the example code, I might have come across the mistake I made: from the docs of both libraries, I assumed that GCM can be used without an authentication tag. That might not be the case and the error might be thrown because a tag is missing. Am I right? I'm doing a test with tag included... – stk Aug 24 '22 at 09:34
  • 1
    No luck, I'm adding test code. – stk Aug 24 '22 at 13:05
  • I'm not seeing any PBKDF2 stuff in your code... – neubert Aug 25 '22 at 01:57
  • @neubert I chose to post the example without key derivation for brevity. – stk Aug 25 '22 at 08:05

1 Answers1

2

There are only two minor encoding bugs:

  • In the phpseclib code the key is not Base64 encoded, in the WebCrypto code it is Base64 encoded. This needs to be changed so that both sides use the same key.
    For the test below I arbitrarily decide to use the WebCrypto solution, i.e. in the phpseclib code a Base64 encoding is added:

    $TEST_AES_KEY = base64_decode("TWw4QCkeZEnXoCDkI1GEHQ==");
    

    This produces a 16 bytes key so that AES-128 is applied (note that the phpseclib solution would also be possible, since the Base64 encoded key is 24 bytes in size and corresponds to AES-192; no matter which key is applied in the end, the important thing is that on both sides the same key must be used).
    Running the phpseclib code again gives the following ciphertext:

    7K+HAB7Ch9V4jJ1XJPM0sANXA2ocJok= 
    

    In the WebCrypto code, this new ciphertext is now used.

  • In the WebCrypto code the 16 bytes IV is Base64 decoded. This creates an IV that is too short for AES. Therefore the Base64 decoding is removed and a UTF-8 encoding (analogous to the phpseclib code) is performed:

    iv = new TextEncoder().encode(iv);
    

With these changes decryption is successful:

(async () => {

    function _base64ToArrayBuffer(base64) {
        var binary_string   = atob(base64);
        var len             = binary_string.length;
        var bytes           = new Uint8Array(len);
        for (var i = 0; i < len; i++) {
            bytes[i] = binary_string.charCodeAt(i);
        }
        return bytes.buffer;
    }

    async function _importKeyAes(key) {
        return await window.crypto.subtle.importKey(
            "raw",
            key,
            { name: "AES-GCM" },
            false,
            ["encrypt", "decrypt"]
        );
    }

    async function decryptMessageSymetric(key, data, iv) {
        keyArrayBuffer  = _base64ToArrayBuffer(key);
        key             = await _importKeyAes(keyArrayBuffer);
        iv              = new TextEncoder().encode(iv); // Remove Base64 decoding
        data            = _base64ToArrayBuffer(data);
        result = await window.crypto.subtle.decrypt(
            { name: "AES-GCM", iv: iv, tagLength: 128 },
            key,
            data
        );
        return new TextDecoder().decode(result);
    }

    TEST_AES_KEY = "TWw4QCkeZEnXoCDkI1GEHQ==";
    TEST_AES_IV = "CRKTyQoWdWB2n56f";
    messageEncrypted = "7K+HAB7Ch9V4jJ1XJPM0sANXA2ocJok="; // Apply modified ciphertext

    result = await decryptMessageSymetric(TEST_AES_KEY, messageEncrypted, TEST_AES_IV); 
    console.log(result);
})();

Note that a static IV is a serious security risk for GCM, s. here.

Topaco
  • 40,594
  • 4
  • 35
  • 62
  • 1
    Thanks a lot, that solved the problem! I will review the the key/iv encodings and lengths thoroughly. The IV is static only in testing of course, it will be dynamically generated with every message later in prod. – stk Aug 25 '22 at 09:05