Search code examples
google-apps-scriptwhatsapp-cloud-apiwhatsapp-flows

WhatsApp Flows - Endpoint Encryption Issues


I am building WhatsApp Flow using the MetaUI, but I am having issues testing the Flow data-exchange action using my endpoint on the Meta Ui Flow Builder. Screenshot of Flow Builder

I'm using a GoogleScripts endpoint that receives the Encrypted payload Meta send you about the flow "data-exchange" actions encrypted using the public key I´ve uploaded to the Meta servers.

My Google Scripts Function handles this request and then calls a Lambda function that decrypts the data, and based on certain logic it decides which screen should be next in the WhatsApp Flow:

var mainss = SpreadsheetApp.openById('11wVWWD17CvhHChcTnJ4r6MbI6F1GUHCpDXhQZWxMwDA');
var flowsLog = mainss.getSheetByName('Flow Log')


function doPost(e) {

  try {
    // Extract the payload
    var requestBody = e.postData.contents;
    flowsLog.appendRow([new Date(), "Received data", JSON.stringify(requestBody)]);

    //Preserve encryption values
    var encryptionVariables = JSON.parse(requestBody);

    // Check for error notification
    if (encryptionVariables.action === 'data_exchange' && encryptionVariables.data.hasOwnProperty('error_key')) {
      flowsLog.appendRow([new Date(), "Error Notification", JSON.stringify(encryptionVariables)]);
      
      // Respond to acknowledge the error notification
      var errorAckResponse = {
        "data": {
          "acknowledged": true
        }
      };
      
      return ContentService.createTextOutput(JSON.stringify(errorAckResponse))
                           .setMimeType(ContentService.MimeType.JSON);
    }

  //Decrypt payload call Lambda through Gateway API
  var decryptedPayload = decryptData(encryptionVariables);
  var decryptedData = JSON.parse(decryptedPayload.decrypted_data);
  var action = decryptedData.action;

  
  flowsLog.appendRow([new Date(), "Descrypted Data", JSON.stringify(decryptedData)]);
  

  // Check for a health check request
  if (action === 'ping') {
    return ContentService.createTextOutput(JSON.stringify({
      'version': '3.0',
      'data': {
        'status': 'active'
      }
    })).setMimeType(ContentService.MimeType.JSON);
  } else if (action === 'data_exchange') {

    // Extract the current screen ID
    var screen = decryptedData.screen
    var data = decryptedData.data

    
    // Initialize variables for the next screen and response data
    var nextScreen = '';
    var responseData = {};

    // Determine the next screen and response data based on the current screen
    //switch (screen) {}
      
    //Forcing some data to test the flow
    var response = {
      "rawData": {
        "version": "3.0",
        "screen": "SESSION_ONE",
        "data": responseData
      },
      "encrypted_aes_key": encryptionVariables.encrypted_aes_key,
      "initial_vector": encryptionVariables.initial_vector
    };
    

    //Encrypt data calling Lambda Function
    var encryptedData = encryptData(response);


    flowsLog.appendRow([new Date(), "Raw Data", JSON.stringify(response.rawData)]);
    flowsLog.appendRow([new Date(), "Encrypted Data", encryptedData]);

    // Devolver la respuesta
    return ContentService.createTextOutput(encryptedData).setMimeType(ContentService.MimeType.TEXT);


  } else {
    return ContentService.createTextOutput("ERROR in Request")

  }
  }catch(e){
        flowsLog.appendRow([new Date(), "Error in endpoint", e])
        Logger.log(e);

  }
}

Here´s an example of the logs I´m adding to my "flowLog" sheet: Received from Meta: "{\"encrypted_flow_data\":\"ikGRIjrW1sBzHECFHvVFkTXcsiVR24eW2Jvd5gmPvrkTp38NA4kG4mLCBB3qWyi36r5IX6BVBZ415Tar0fBHmWiyEEirVjmyPF1N\\/WEcVEHcgCuHAOC0Vj2z1tRSBQOSMfQdUbsGqWncMpWEEztD\\/a8CfrD5WTRWFG52p0twN6XnbnuX20DOjHoOMMGgAuPr8rB4rE88UkXS4XOBuSbxomS89UBPZsVfn4ZSe8xj70dVuAmNqmBOvZQVlLr2edOENssLTb6dj0cTvdGOEAia6HbyxZoOXNzMp5q38guT7Jw=\",\"encrypted_aes_key\":\"q4Ww1XFn0TadlE5H0pnJ6PzX3lXfeZwqYSZPKop\\/jWCweaY9d+Z1pMjfM1UX3te\\/GdHIpa8sa1chPSLOgxiYDbAcQ3uiJqlfOKLbdCuz\\/HTvVoTvrn3e\\/BJl9ALHeqOCqqIwGuHz2YcXvqc7zD4asU3mczrRa8Xvo8+YY89qebfF62F4f77ApXjT3LT\\/ynn7hYkxbMKo02bdIbteZAwSh+OMFVoMJo6vwOmH3fvbLSzcA80C8QIf7WVKQ7imik5+u2wETQOUDZ2Drmeyyh+xc\\/bTDi+GrSEpfXrI4jtnbd4YxPZLSlcaXAooK6ot9DP4ioOGSTxCCsAmz8ht87mHGQ==\",\"initial_vector\":\"SFWNZlzEXH6Cs0JeWMcJJQ==\"}"

Decrypted data: {"version":"3.0","action":"data_exchange","screen":"WELCOME_SCREEN_EVENTS","data":{"event_type":"${form.event_type}","house":"${form.house}","event_date":"${form.date}"},"flow_token":"flows-builder-adacf0e2"}

Raw response: {"version":"3.0","screen":"SESSION_ONE","data":{"event_type":"2","house":"1","event_date":"30/11/2023"}}

Encrypted response: 4/qcTvBZpQmZtKVPjow0jpISGTG3SicgOmCoxJKYUnhJqWylgZY2sNzp0ICFb1J3STDZugVWuolrmfAOSeTS6mCLUGj4sBStAZrTBz1p0NNlXW/XXVBe/a5c3d3iSiAO0YZdaQRvKuKW47YZHbFcUxuTX6AWiBZ3j6dADvLElwpPBg==

Additionally, here´s what the Flow Builder is instructing me to send as encrypted payload in my response in order to navigate to my screen "SESSION_ONE":

// Navigate to screen: SESSION_ONE
{
    "version": "3.0",
    "screen": "SESSION_ONE",
    "data": {
        "event_type": "Sesi\u00f3n fotogr\u00e1fica",
        "house": "Casa Kali",
        "event_date": "22\/12\/2023"
    }
}

Here's the final part of my lambda encrypting function:

# Cifrar rawData
cipher = Cipher(algorithms.AES(key), modes.GCM(flipped_iv))
encryptor = cipher.encryptor()
encrypted = encryptor.update(json.dumps(raw_data).encode("utf-8")) + encryptor.finalize() + encryptor.tag
encrypted_response_b64 = b64encode(encrypted).decode("utf-8")

# Crear la respuesta
response = {
    'encrypted_response': encrypted_response_b64
}

# Modificación: Solo devolver encrypted_response
return {
    'statusCode': 200,
    'body': json.dumps(encrypted_response_b64)
}

So far I know my decrypting/encrypting lambdas are working fine, I've decrypted my encrypted response and it gives my raw response back perfectly.


Solution

  • It seems that your response object is not the right format, it should be just the raw response like

    response = {
      "version": "3.0",
      "screen": "SESSION_ONE",
      "data": responseData 
    }
    

    And then the response you return from the lambda should be plain text and not json encoded, like this

    return {
        'statusCode': 200,
        'body': encrypted_response_b64
    }
    

    You can also checkout the full encryption code samples in python & nodejs in the docs here https://developers.facebook.com/docs/whatsapp/flows/guides/implementingyourflowendpoint#request-decryption-and-encryption