WhatsApp Flows Decryption Error - error:02000079:rsa routines::oaep decoding error

529 views Asked by At

I have utilized the WhatsApp Flows API to integrate flows into our application. When publishing the flow, I need to decrypt it using the following structure:

{
    encrypted_flow_data: "<ENCRYPTED FLOW DATA>",
    encrypted_aes_key: "<ENCRYPTED_AES_KEY>",
    initial_vector: "<INITIAL VECTOR>"
 }

referred to this documentation (https://developers.facebook.com/docs/whatsapp/flows/guides/implementingyourflowendpoint#nodejs-express-example) for decrypting the key.

However, I am encountering the following error:

{
"code": "ERR_OSSL_RSA_OAEP_DECODING_ERROR",
"library": "rsa routines",
"reason": "oaep decoding error",
"message": "error:02000079:rsa routines::oaep decoding error",
"stack": "Error: error:02000079:rsa routines::oaep decoding error\n    at Object.privateDecrypt (node:internal/crypto/cipher:79:12)\n    at decryptRequest"

}

For reference, here is the relevant code snippet:

const express = require("express");
const crypto = require("crypto");
const fs = require('fs');
const util = require('util');
const { promisify } = require('util');
const readFile = promisify(fs.readFile);

require("dotenv").config();

const PORT = 3000;
const app = express();
app.use(express.json());

app.post("/data", async ({ body }, res) => {
    const PRIVATE_KEY_PATH = 'path/to/your/private_key.pem'; 
    const PRIVATE_KEY_PASSPHRASE = 'your_private_key_passphrase';

    const PRIVATE_KEY = await readFile(PRIVATE_KEY_PATH, 'utf-8');

    const { decryptedBody, aesKeyBuffer, initialVectorBuffer } = decryptRequest(
        body,
        PRIVATE_KEY,
        PRIVATE_KEY_PASSPHRASE,
    );

    const { screen, data, version, action } = decryptedBody;
    // Return the next screen & data to the client
    const screenData = {
        version,
        screen: "SCREEN_NAME",
        data: {
            some_key: "some_value",
        },
    };

    // Return the response as plaintext
    res.send(encryptResponse(screenData, aesKeyBuffer, initialVectorBuffer));
});

const decryptRequest = async (body, privatePem, passphrase) => {
    try {
        const { encrypted_aes_key, encrypted_flow_data, initial_vector } = body;

        // Decrypt the AES key created by the client
        const decryptedAesKey = crypto.privateDecrypt(
            {
                key: crypto.createPrivateKey(
                    {
                        key: privatePem,
                        passphrase: passphrase,
                    }
                ),
                padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
                oaepHash: "sha256",
            },
            Buffer.from(encrypted_aes_key, "base64"),
        );

        // Decrypt the Flow data
        const flowDataBuffer = Buffer.from(encrypted_flow_data, "base64");
        const initialVectorBuffer = Buffer.from(initial_vector, "base64");

        const TAG_LENGTH = 16;
        const encrypted_flow_data_body = flowDataBuffer.subarray(0, -TAG_LENGTH);
        const encrypted_flow_data_tag = flowDataBuffer.subarray(-TAG_LENGTH);

        const decipher = crypto.createDecipheriv(
            "aes-128-gcm",
            decryptedAesKey,
            initialVectorBuffer,
        );
        decipher.setAuthTag(encrypted_flow_data_tag);

        const decryptedJSONString = Buffer.concat([
            decipher.update(encrypted_flow_data_body),
            decipher.final(),
        ]).toString("utf-8");

        return {
            decryptedBody: JSON.parse(decryptedJSONString),
            aesKeyBuffer: decryptedAesKey,
            initialVectorBuffer,
        };
    } catch (e) {
        console.log('Error decoding:', e)
    }
};

const encryptResponse = (
    response,
    aesKeyBuffer,
    initialVectorBuffer,
) => {
    // Flip the initialization vector
    const flipped_iv = [];
    for (const pair of initialVectorBuffer.entries()) {
        flipped_iv.push(~pair[1]);
    }
    // Encrypt the response data
    const cipher = crypto.createCipheriv(
        "aes-128-gcm",
        aesKeyBuffer,
        Buffer.from(flipped_iv),
    );
    return Buffer.concat([
        cipher.update(JSON.stringify(response), "utf-8"),
        cipher.final(),
        cipher.getAuthTag(),
    ]).toString("base64");
};

app.listen(PORT, () => {
    console.log(`App is listening on port ${PORT}!`);
});

Could you assist me in resolving this issue? I appreciate your help in advance. Thank you.

3

There are 3 answers

0
gafi On

The decryption code seems fine to me. The issue might be with the private key.

  1. Can you verify if the private & public keys are matching? You can log the private key to confirm it's correct.
  2. Can you encrypt any arbitrary payload with the public key and verify you can decrypt it with the current code?
0
theGateway On

If you are absolutely sure that the private key you are using is corresponding to the public key that Meta is using, then proceed further with method 1, otherwise perform method 2 first, and if the error still persists, come back to method 1.

Method 1:

I was facing the same issue, now while I was not able to resolve this error specifically, but I found a Glitch example from Meta, and that seems to work for me (it is not throwing this error).

Visit Glitch project (Meta Version)

Only change I had to make was that, when I put private key in environment variable, it was throwing error, so I put it in assets folder, and read it using fs. For reference, you can visit my project on Glitch.

Visit Glitch Project (My Version)

Method 2:

  1. Regenerate the key pair, then upload the public key to Meta server, as mentioned here.
  2. Update the private key in your server.
  3. The critical step to make it work: Make a data-exchange request from WhatsApp client, and from your webhook, send an error with status code 421, this forces the WhatsApp client to re-fetch the public key.
res.sendStatus(421) // Express.js
  1. Be patient: Generally, it takes around 30 minutes for the newly generated key pair to work (from the time your webhook replies with status code 421).
0
xOCh On

I found that depends on the WELJ's version. If you're using version 100 won't work. But instead use version 200 or viceversa. But right now, seems that version 100 is already deprecated. So in your WELJ file, put:

{
    version: 200,
    data_api_version: 200
}

And should work.

Also found that updating a publicKey to Meta, we needed to wait till their cache takes effect. Otherwise, all encryptedRequest will be encrypted with old publicKey.