AES-GCM Secret/Symmetric Key Validation with Encrypted Buffer Comparison of SubtleCrypto and SJCL

Our application has a group chat feature which involves end-to-end encryption of group chat messages.

The group is hosted at server.

All the encryption and decryption is handled on the clientside.

It also utilizes the same mechanism of encryption for the real-time messages.

The group chat works by foremost someone creating this group at serverside, when a client is instating its creation they generate a new key [clientside], and validate it on their end [successful encryption and decryption of a same static text from SubtleCrypto], and then successfully hosts it by sending the encrypted buffer to server where groups are managed, storing it, and then others can join if they know the secret key. We use the same AES-GCM Symmetric Key / Secret Key that is generated for this purpose. The server-side doesn’t have the key stored anywhere.

Now, to validate whether the key a client trying to join this group is valid or not before joining is, with the key that was shared to them [by other means, such as email etc], at client-side, encrypt the SAME static text, and send its buffer. Then the buffer value stored at the server-side on creation time is compared to this new client joining with the buffer of the newly encrypted static text they performed on client-side, and if the buffer is equal on server-side, they are authorized into this group.

Now with reference to my previous question(s), I’m attempting to replace Web API SubtleCrypto to SJCL, and the newly generated SJCL encrypted buffer is always smaller than the SubtleCrypto. While encrypting and decrypting between each other is already established, the problem at hand is that their buffers don’t match, even though they’re both using the same key, IV, and AES-GCM mode. And they both have to simultaneously work for backwards compatibility of different client versions.

Here is an example:

const buf2hex = (buffer) =>
{
    return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join('');
}

const string2hex = (input) =>
{
    let hex;
    let result = "";
    let i = 0;
    for (i = 0; i < input.length; i++)
    {
        hex = input.charCodeAt(i).toString(16);
        result += ("000" + hex).slice(-4);
    }
    return result
}

const hex2bytes = (string) =>
{
    const normal = string.length % 2 ? "0" + string : string; // Make even length
    const bytes = new Uint8Array(normal.length / 2);
    for (let index = 0; index < bytes.length; ++index)
    {
        const c1 = normal.charCodeAt(index * 2);
        const c2 = normal.charCodeAt(index * 2 + 1);
        const n1 = c1 - (c1 < 58 ? 48 : 87);
        const n2 = c2 - (c2 < 58 ? 48 : 87);
        bytes[index] = n1 * 16 + n2;
    }
    return bytes;
}

//JWK K Value
const generateKey = async () =>
{
    const key = await window.crypto.subtle.generateKey(
    {
        name: "AES-GCM",
        length: 128
    }, true, ["encrypt", "decrypt"]);
    const key_exported = await window.crypto.subtle.exportKey("jwk", key);
    return key_exported.k;
}

//CryptoKey generated from SubtleCrypto:
const generateSubtleCryptoKey = async (kvalue) =>
{
    return window.crypto.subtle.importKey(
        "jwk",
        {
            k: kvalue,
            alg: "A128GCM",
            ext: true,
            key_ops: ["encrypt", "decrypt"],
            kty: "oct",
        },
        {
            name: "AES-GCM",
            length: 128
        },
        false,
        ["encrypt", "decrypt"]
    );
}

//Cipher generated from SJCL:
const generateCipherSJCL = (kkey) =>
{
    const ekkeyB64 = kkey.replace(/-/g, '+').replace(/_/g, '/');    // Base64url -> Base64 (ignore optional padding)
    const ebkey = sjcl.codec.base64.toBits(ekkeyB64);               // conert to bitArray
    const ecipher = new sjcl.cipher.aes(ebkey);
    return ecipher;
}

const encryptText = "STATIC TEXT";

const compareBuffers = async () =>
{
    const kkey = await generateKey();
    const cryptokey = await generateSubtleCryptoKey(kkey)
    const ecipher = generateCipherSJCL(kkey);

    const subtleCrypto = await window.crypto.subtle.encrypt(
    {
        name: "AES-GCM",
        iv: new Uint8Array(12)
    }, cryptokey, new TextEncoder().encode(JSON.stringify(encryptText)));

    const encryptionIv = sjcl.codec.hex.toBits(buf2hex(new Uint8Array(12).buffer));
    const encryptedMessageFormat = sjcl.codec.hex.toBits(string2hex(JSON.stringify(encryptText)));
    const sjclEncrypted = sjcl.mode.gcm.encrypt(ecipher, encryptedMessageFormat, encryptionIv);

    const originalEncryptedSJCL = Buffer.from(sjclEncrypted);
    console.log({subtleCrypto});
    console.log({originalEncryptedSJCL});
    const e1 = Buffer.from(new Uint8Array(sjclEncrypted));
    const e2 = Buffer.from(new Uint8Array(subtleCrypto));
    console.log({e1, e2});                                  //{e1: Uint8Array(11), e2: Uint8Array(29)}
    console.log(Buffer.compare(e1, e2));                    //should be 0, equal buffer.
}

compareBuffers();

I suppose I should preface this by stating that I have very limited cryptography knowledge, but why would the buffers differ when they’re both encrypted and decrypted across both libraries when the mechanism is same?