Search code examples
encryptionphpseclibaes-gcmsubtlecryptowebcrypto

Is phpseclib AES-GCM encryption compatible with javascript WebCrypto?


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>

Solution

  • 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.