reactjsnode.jscryptojsaes-gcmwebcrypto-api

how can I decrypt the content in frontend which was encrypted in backend


It might be a little problem but I'm neither getting any specific error nor getting any solution. I am building a full stack project. In here I need a condition where I have to encrypt a content in backend part of the project. I'm using symmetric key cryptography and using aes_256_gcm in crypto-js. this is the code of backend part.

import { randomBytes, createCipheriv } from "crypto";

const encryptMessage = (message, credential) => {
  const key = Buffer.from(credential, "utf-8");
  const iv = randomBytes(16);
  const cipher = createCipheriv("aes-256-gcm", key, iv);
  const encryptedMessage = Buffer.concat([
    cipher.update(message, "utf-8"),
    cipher.final(),
  ]);
  const tag = cipher.getAuthTag();

  return {
    iv: iv.toString("hex"),
    encryptedMessage: encryptedMessage.toString("hex"),
    tag: tag.toString("hex"),
  };
};


export default encryptMessage;

now I am storing this content in a variable in backend and then passing it to frontend.

now I am trying to decrypt this message in frontend using same iv, tag, and credential in the code. this is the code of decryption in frontend.

const decryptMessage = async (encryptedData, credential) => {
  
  const decoder = new TextDecoder();
  
  const iv = new Uint8Array(encryptedData.iv.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));
  const encryptedMessage = new Uint8Array(encryptedData.encryptedMessage.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));
  const tag = new Uint8Array(encryptedData.tag.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));

  const encodedCredential =new TextEncoder().encode(credential)

  try{
  const key = await crypto.subtle.importKey(
    "raw",
    encodedCredential,
    { name: "AES-GCM" },
    false,
    ["decrypt"]
  );

  const decrypted = await crypto.subtle.decrypt(
    { name: "AES-GCM", 
    iv: iv,
    tag: tag },
    key,
    encryptedMessage
  );

    const decryptedMessage = decoder.decode(decrypted);

    console.log("Decrypted Message:", decryptedMessage);
    return decryptedMessage;

  }catch(error){
    console.error("Decryption Error:", error);
    throw new Error ("Decryption Failed" + error.message);
  }
}

export default decryptMessage

In here I am not getting any specific error. just getting Decryption Error: Error. When I applied console.log() I'm getting the output till before decrypted function. And this is how I'm calling this function in another file. const privateKey = await decryptMessage(keys, credential);

Anyone can please explain why is the error happening. And if it will never work, then how can do this. means to encrypt the content in backend and decrypt it in frontend.


Solution

  • The value passed to crypto.subtle.decrypt() in the first parameter for algorithm is incorrect: { name: "AES-GCM", iv: iv, tag: tag }. A property tag is not defined for AesGcmParams at all.
    Instead, crypto.subtle.decrypt() expects the concatenation of ciphertext and tag to be passed for data in the third parameter in the case of GCM.

    Fixed code (the input data was generated with your NodeJS code):

    (async () => {
    
    const decryptMessage = async (encryptedData, credential) => {      
      const decoder = new TextDecoder();      
      const iv = new Uint8Array(encryptedData.iv.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));
      const encryptedMessage = new Uint8Array(encryptedData.encryptedMessage.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));
      const tag = new Uint8Array(encryptedData.tag.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));
      const encodedCredential =new TextEncoder().encode(credential)
      try{
        const key = await crypto.subtle.importKey(
          "raw",
          encodedCredential,
          { name: "AES-GCM" },
          false,
          ["decrypt"]
        );
        /*
        const decrypted = await crypto.subtle.decrypt(
          { name: "AES-GCM", 
          iv: iv,
          tag: tag },
          key,
          encryptedMessage
        );
        */
        const decrypted = await crypto.subtle.decrypt( 
          { name: "AES-GCM", 
          iv: iv }, // Fix: remove tag
          key,
          new Uint8Array([...encryptedMessage, ...tag]) // Fix: concatenate ciphertext and tag
        );
        const decryptedMessage = decoder.decode(decrypted);
        console.log("Decrypted Message:", decryptedMessage);
        return decryptedMessage;
      } catch(error){
        console.error("Decryption Error:", error);
        throw new Error ("Decryption Failed" + error.message);
      }
    }
    var encryptedData = {
      iv: 'd38d7f07a4511b9f3cc79acb66b42528',
      encryptedMessage: 'd3da8f3da785b05ad4c2d462672379c287ba7640b971a3360d783e35acd6c9d1580555a1cb2927f1678147',
      tag: '7146b3fd125b59e718c4cd8cb39f8142'
    };
    var credential = '01234567890123456789012345678901';
    var ct = '123ce86e38eed47a1658d8df832ed59eac3ac5465f0865583c2e48dba32b170307fa12d18e9f46691acd2c';
    var dt = await decryptMessage(encryptedData, credential);
    
    })();


    Security: Keep in mind that in case of a passphrase as key material it is more secure to use a key derivation function like Argon2 or at least PBKDF2 (in conjunction with a random salt).