data_api_version in the Flow JSON and configure endpoint url, see reference for more details.public-key-missing or public-key-signiture-verification.https://business.com/scheduleappointmentPOST requests, use HTTPS and have a valid TLS/SSL certificate installed. This certificate does not have to be used in payload encryption/decryption.{ encrypted_flow_data: "<ENCRYPTED FLOW DATA>", encrypted_aes_key: "<ENCRYPTED_AES_KEY>", initial_vector: "<INITIAL VECTOR>" }
| Parameter | Description |
|---|---|
encrypted_flow_datastring | Required. The encrypted request payload. |
encrypted_aes_keystring | Required. The encrypted 128-bit AES key. |
initial_vectorstring | Required. The 128-bit initialization vector. |
curl -i -H "Content-Type: application/json" -X POST -d '{ "encrypted_flow_data":"4Wor0bpfvrNqnkH+XQZLn3HnU2Zi7hG\\/UHjISS93Fzn9J7youssaLeXlNUH", "encrypted_aes_key":"<ufA0fXD1WzMS4f2aCyr2JI4KtV2X+puen78fLjjt7mI+NqITDCypLOlc2MLc0899ApX5FZI78Yp5ZObEvR\\/3SiOo04aOLAcZ5SGlqcQLL1npaHoTZCBkExjDr0+5F7w+a18hLCByc00nuDoVZvX7qKAYTwDJw==.>", "initial_vector":"<G\\/1rq1naEOMR4TJHFvIs\\/Q==.>" }' 'https://business.com/testing_flow' HTTP/2 200 content-type: text/plain content-length: 232 date: Wed, 06 Jul 2022 14:03:03 GMT yZcJQaH3AqfzKgjn64vAcASaJrOMN27S6CESyU68WN/cDCP6abskoMa/pPjszXGKyyh/23lw84HW6ZilMfU6KL3j5AWwOx6GWNwtq8Aj7gz/Y7R+LccmJWxKo2UccMu5xJlduIFlFlOS1gAnOwKrk8wpuprsi4jAOspw3xO2uh3J883aC/csu/MhRPiYCaGGy/tTNvVDmb2Gw1WXFmpvLsZ/SBrgG0cDQJjQzpTO
flow_action field in the parameters that you pass to the Cloud API when sending a flow message is data_exchange;flow_action to avoid calling your endpoint. You can supply parameters by using the flow_action_payload.data message field instead.name attribute specified inside on-click-action field in Flow JSON is data_exchange;on-click-action name to navigate to avoid calling your endpoint.refresh_on_back attribute specified in Flow JSON for the screen is true;refresh_on_back to avoid calling your endpoint.on-select-action for the component is defined in Flow JSON.{ "version": "<VERSION>", "action": "<ACTION_NAME>", "screen": "<SCREEN_NAME>", "data": { "prop_1": "value_1", … "prop_n": "value_n" }, "flow_token": "<FLOW-TOKEN>" }
| Parameter | Description |
|---|---|
versionstring | Required. Value must be set to 3.0. |
screenstring | Required. If action is set to INIT or BACK, this field may not be populated.
(Note: “SUCCESS” is a reserved name and cannot be used by any screens.) |
actionstring | Required. Defines the type of the request.
For data exchange request there are multiple choices depending on when the trigger:
|
dataobject | Required. An object passing arbitrary key-value data as a JSON object. If action is set to INIT or BACK, this field may not be populated.<key> string, boolean, number, object, array - <value> |
flow_tokenstring | Required. A Flow token generated and sent by you as part of the Flow message. The flow token is similar to a session identifier commonly used in web applications. It should be generated using established best practices (e.g. it should not be predictable) to ensure the security of data exchanges with an endpoint. |
flow_token_signaturestring | Please note that flow_token_signature will only be sent with flows version >= 7.3 and data_api_version >=4.0. A Flow token signature is generated and sent by flows as part of the data exchange request payload. The flow_token_signature is a JSON Web Token (JWT) created by flows to securely sign the flow token using the Meta app secret as the secret key. You can choose to use this signature to verify the authenticity of the flow token. (see Flow token Signature for more details) |
{ "screen": "<SCREEN_NAME>", "data": { "property_1": "value_1", ... "property_n": "value_n", "error_message": "<ERROR-MESSAGE>" } }
error_message in the data object as part of the response.<SCREEN_NAME> and will trigger a snackbar error with the error_message present.| Parameter | Description |
|---|---|
screenstring | Required. The screen to be rendered once the data exchange is complete. |
dataobject | Required. A JSON of properties and its values to render the screen after data exchange is complete.
|
{ "screen": "SUCCESS", "data": { "extension_message_response": { "params": { "flow_token": "<FLOW_TOKEN>", "optional_param1": "<value1>", "optional_param2": "<value2>" } } } }
| Parameter | Description |
|---|---|
screenstring | Value must be SUCCESS |
data.extension_message_response.paramsobject | A JSON with data which will be included to the flow completion message (see Response Message Webhook for more details) |
data.extension_message_response.params.flow_tokenstring | Required. Flow token generated by a business signifying a session or a user flow |

params field in addition to flow_token. All these parameters are forwarded to the messages webhook.{ "version": "<VERSION>", "flow_token": "<FLOW-TOKEN>", "action": "data_exchange | INIT", "data": { "error": "<ERROR-KEY>", "error_message": "<ERROR-MESSAGE>" } }
| Parameter | Description |
|---|---|
versionstring | Required. 3.0 |
screenstring | Required. Screen name where bad intermediate response payload was sent |
flow_tokenstring | Required. A flow token generated by your business |
actionstring | Required. Will be "data_exchange" or "INIT" |
dataobject | Required. data object representing the error.
|
{ "data": { "acknowledged": true } }
{ "version": "3.0", "action": "ping" }
{ "data": { "status": "active" } }
X-Hub-Signature-256 header, preceded with sha256=.X-Hub-Signature-256 header (everything after sha256=). If the signatures match, the payload is genuine.X-Hub-Signature-256 header to be correct if it can be validated using either old or new app secret.X-Hub-Signature-256 header correct only if it can be validated using new app secret.encrypted_aes_key field:
encrypted_flow_data field:
initial_vector field (which is base64-encoded as well and should be decoded first). Note that the 128-bit authentication tag for the AES-GCM algorithm is appended to the end of the encrypted array.import json
import os
from base64 import b64decode, b64encode
from cryptography.hazmat.primitives.asymmetric.padding import OAEP, MGF1, hashes
from cryptography.hazmat.primitives.ciphers import algorithms, Cipher, modes
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from django.http import HttpResponse
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
# Load the private key string
PRIVATE_KEY = os.environ.get('PRIVATE_KEY')
# Example:
# '''-----BEGIN RSA PRIVATE KEY-----
# MIIE...
# ...
# ...AQAB
# -----END RSA PRIVATE KEY-----'''
@csrf_exempt
def data(request):
try:
# Parse the request body
body = json.loads(request.body)
# Read the request fields
encrypted_flow_data_b64 = body['encrypted_flow_data']
encrypted_aes_key_b64 = body['encrypted_aes_key']
initial_vector_b64 = body['initial_vector']
decrypted_data, aes_key, iv = decrypt_request(
encrypted_flow_data_b64, encrypted_aes_key_b64, initial_vector_b64)
print(decrypted_data)
# Return the next screen & data to the client
response = {
"screen": "SCREEN_NAME",
"data": {
"some_key": "some_value"
}
}
# Return the response as plaintext
return HttpResponse(encrypt_response(response, aes_key, iv), content_type='text/plain')
except Exception as e:
print(e)
return JsonResponse({}, status=500)
def decrypt_request(encrypted_flow_data_b64, encrypted_aes_key_b64, initial_vector_b64):
flow_data = b64decode(encrypted_flow_data_b64)
iv = b64decode(initial_vector_b64)
# Decrypt the AES encryption key
encrypted_aes_key = b64decode(encrypted_aes_key_b64)
private_key = load_pem_private_key(
PRIVATE_KEY.encode('utf-8'), password=None)
aes_key = private_key.decrypt(encrypted_aes_key, OAEP(
mgf=MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None))
# Decrypt the Flow data
encrypted_flow_data_body = flow_data[:-16]
encrypted_flow_data_tag = flow_data[-16:]
decryptor = Cipher(algorithms.AES(aes_key),
modes.GCM(iv, encrypted_flow_data_tag)).decryptor()
decrypted_data_bytes = decryptor.update(
encrypted_flow_data_body) + decryptor.finalize()
decrypted_data = json.loads(decrypted_data_bytes.decode("utf-8"))
return decrypted_data, aes_key, iv
def encrypt_response(response, aes_key, iv):
# Flip the initialization vector
flipped_iv = bytearray()
for byte in iv:
flipped_iv.append(byte ^ 0xFF)
# Encrypt the response data
encryptor = Cipher(algorithms.AES(aes_key),
modes.GCM(flipped_iv)).encryptor()
return b64encode(
encryptor.update(json.dumps(response).encode("utf-8")) +
encryptor.finalize() +
encryptor.tag
).decode("utf-8")
import express from "express";
import crypto from "crypto";
const PORT = 3000;
const app = express();
app.use(express.json());
const PRIVATE_KEY = process.env.PRIVATE_KEY as string;
/*
Example:
-----BEGIN RSA PRIVATE KEY-----
MIIE...
...
...AQAB
-----END RSA PRIVATE KEY-----
*/
app.post("/data", async ({ body }, res) => {
const { decryptedBody, aesKeyBuffer, initialVectorBuffer } = decryptRequest(
body,
PRIVATE_KEY,
);
const { screen, data, version, action } = decryptedBody;
// Return the next screen & data to the client
const screenData = {
screen: "SCREEN_NAME",
data: {
some_key: "some_value",
},
};
// Return the response as plaintext
res.send(encryptResponse(screenData, aesKeyBuffer, initialVectorBuffer));
});
const decryptRequest = (body: any, privatePem: string) => {
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(privatePem),
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,
};
};
const encryptResponse = (
response: any,
aesKeyBuffer: Buffer,
initialVectorBuffer: Buffer,
) => {
// 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}!`);
});
<?php
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use phpseclib3\Crypt\RSA;
use phpseclib3\Crypt\AES;
require __DIR__ . '/vendor/autoload.php';
$app = Slim\Factory\AppFactory::create();
$app->post('/data', function (Request $request, Response $response) {
$body = json_decode($request->getBody()->getContents(), true);
$privatePem = getenv('PRIVATE_KEY');
/*
Example:
-----BEGIN RSA PRIVATE KEY-----
MIIE...
...
...AQAB
-----END RSA PRIVATE KEY-----
*/
$decryptedData = decryptRequest($body, $privatePem);
// Return the next screen & data to client
$screen = [
"screen" => "SCREEN_NAME",
"data" => [
"some_key" => "some_value"
]
];
$resBody = encryptResponse($screen, $decryptedData['aesKeyBuffer'], $decryptedData['initialVectorBuffer']);
// Return the response as plaintext
$response->getBody()->write($resBody);
return $response;
});
function decryptRequest($body, $privatePem)
{
$encryptedAesKey = base64_decode($body['encrypted_aes_key']);
$encryptedFlowData = base64_decode($body['encrypted_flow_data']);
$initialVector = base64_decode($body['initial_vector']);
// Decrypt the AES key created by the client
$rsa = RSA::load($privatePem)
->withPadding(RSA::ENCRYPTION_OAEP)
->withHash('sha256')
->withMGFHash('sha256');
$decryptedAesKey = $rsa->decrypt($encryptedAesKey);
if (!$decryptedAesKey) {
throw new Exception('Decryption of AES key failed.');
}
// Decrypt the Flow data
$aes = new AES('gcm');
$aes->setKey($decryptedAesKey);
$aes->setNonce($initialVector);
$tagLength = 16;
$encryptedFlowDataBody = substr($encryptedFlowData, 0, -$tagLength);
$encryptedFlowDataTag = substr($encryptedFlowData, -$tagLength);
$aes->setTag($encryptedFlowDataTag);
$decrypted = $aes->decrypt($encryptedFlowDataBody);
if (!$decrypted) {
throw new Exception('Decryption of flow data failed.');
}
return [
'decryptedBody' => json_decode($decrypted, true),
'aesKeyBuffer' => $decryptedAesKey,
'initialVectorBuffer' => $initialVector,
];
}
function encryptResponse($response, $aesKeyBuffer, $initialVectorBuffer)
{
// Flip the initialization vector
$flipped_iv = ~$initialVectorBuffer;
// Encrypt the response data
$cipher = openssl_encrypt(json_encode($response), 'aes-128-gcm', $aesKeyBuffer, OPENSSL_RAW_DATA, $flipped_iv, $tag);
return base64_encode($cipher . $tag);
}
$app->run();
-----BEGIN PRIVATE KEY----- at the beginning of the file.-----BEGIN RSA PRIVATE KEY-----) or PKCS#8 encrypted format (starts with -----BEGIN ENCRYPTED PRIVATE KEY-----), you can use below command to convert it to the unencrypted PKCS#8:package org.example;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.OAEPParameterSpec;
import javax.crypto.spec.PSource;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
public class App {
private static class DecryptionInfo {
public final String clearPayload;
public final byte[] clearAesKey;
public DecryptionInfo(String clearPayload, byte[] clearAesKey) {
this.clearPayload = clearPayload;
this.clearAesKey = clearAesKey;
}
}
private static final int AES_KEY_SIZE = 128;
private static final String KEY_GENERATOR_ALGORITHM = "AES";
private static final String AES_CIPHER_ALGORITHM = "AES/GCM/NoPadding";
private static final String RSA_ENCRYPT_ALGORITHM = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
private static final String RSA_MD_NAME = "SHA-256";
private static final String RSA_MGF = "MGF1";
public static void main(String[] args) throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress(3000), 0);
server.createContext("/data", new EndpointHandler());
server.setExecutor(null);
server.start();
System.out.print("Server started on " + server.getAddress());
}
static class EndpointHandler implements HttpHandler {
@Override
public void handle(HttpExchange t) throws IOException {
String response;
int responseCode;
try {
final JSONParser parser = new JSONParser();
final JSONObject requestJson = (JSONObject) parser.parse(new InputStreamReader(t.getRequestBody(), StandardCharsets.UTF_8));
final byte[] encrypted_flow_data = Base64.getDecoder().decode((String) requestJson.get("encrypted_flow_data"));
final byte[] encrypted_aes_key = Base64.getDecoder().decode((String) requestJson.get("encrypted_aes_key"));
final byte[] initial_vector = Base64.getDecoder().decode((String) requestJson.get("initial_vector"));
final DecryptionInfo decryptionInfo = decryptRequestPayload(encrypted_flow_data, encrypted_aes_key, initial_vector);
final JSONObject clearRequestData = (JSONObject) parser.parse(decryptionInfo.clearPayload);
final String clearResponse = String.format("{\"screen\":\"SCREEN_NAME\",\"data\":{\"some_key\":\"some_value\"}}");
response = encryptAndEncodeResponse(clearResponse, decryptionInfo.clearAesKey, flipIv(initial_vector));
responseCode = 200;
} catch (Exception ex) {
response = "Processing error: " + ex.getMessage();
responseCode = 500;
}
t.getResponseHeaders().add("Content-Type", "text/plain; charset=UTF-8");
final byte[] responseBytes = response.getBytes();
t.sendResponseHeaders(responseCode, responseBytes.length);
OutputStream os = t.getResponseBody();
os.write(response.getBytes());
os.close();
}
}
private static DecryptionInfo decryptRequestPayload(byte[] encrypted_flow_data, byte[] encrypted_aes_key, byte[] initial_vector) throws Exception {
final RSAPrivateKey privateKey = readPrivateKeyFromPkcs8UnencryptedPem(System.getenv("ENDPOINT_PRIVATE_KEY_FILE_PATH"));
final byte[] aes_key = decryptUsingRSA(privateKey, encrypted_aes_key);
return new DecryptionInfo(decryptUsingAES(encrypted_flow_data, aes_key, initial_vector), aes_key);
}
private static String decryptUsingAES(final byte[] encrypted_payload, final byte[] aes_key, final byte[] iv) throws GeneralSecurityException {
final GCMParameterSpec paramSpec = new GCMParameterSpec(AES_KEY_SIZE, iv);
final Cipher cipher = Cipher.getInstance(AES_CIPHER_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(aes_key, KEY_GENERATOR_ALGORITHM), paramSpec);
final byte[] data = cipher.doFinal(encrypted_payload);
return new String(data, StandardCharsets.UTF_8);
}
private static byte[] decryptUsingRSA(final RSAPrivateKey privateKey, final byte[] payload) throws GeneralSecurityException {
final Cipher cipher = Cipher.getInstance(RSA_ENCRYPT_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, privateKey, new OAEPParameterSpec(RSA_MD_NAME, RSA_MGF, MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT));
return cipher.doFinal(payload);
}
private static RSAPrivateKey readPrivateKeyFromPkcs8UnencryptedPem(String filePath) throws Exception {
final String prefix = "-----BEGIN PRIVATE KEY-----";
final String suffix = "-----END PRIVATE KEY-----";
String key = new String(Files.readAllBytes(new File(filePath).toPath()), StandardCharsets.UTF_8);
if (!key.contains(prefix)) {
throw new IllegalStateException("Expecting unencrypted private key in PKCS8 format starting with " + prefix);
}
String privateKeyPEM = key.replace(prefix, "").replaceAll("[\\r\\n]", "").replace(suffix, "");
byte[] encoded = Base64.getDecoder().decode(privateKeyPEM);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);
return (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
}
private static String encryptAndEncodeResponse(final String clearResponse, final byte[] aes_key, final byte[] iv) throws GeneralSecurityException {
final GCMParameterSpec paramSpec = new GCMParameterSpec(AES_KEY_SIZE, iv);
final Cipher cipher = Cipher.getInstance(AES_CIPHER_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(aes_key, KEY_GENERATOR_ALGORITHM), paramSpec);
final byte[] encryptedData = cipher.doFinal(clearResponse.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encryptedData);
}
private static byte[] flipIv(final byte[] iv) {
final byte[] result = new byte[iv.length];
for (int i = 0; i < iv.length; i++) {
result[i] = (byte) (iv[i] ^ 0xFF);
}
return result;
}
}
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Modes;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;
var app = WebApplication.CreateBuilder(args).Build();
var PRIVATE_KEY = Environment.GetEnvironmentVariable("PRIVATE_KEY") ?? throw new InvalidOperationException("The environment variable 'PRIVATE_KEY' is not set.");
var PASSPHRASE = Environment.GetEnvironmentVariable("PASSPHRASE") ?? throw new InvalidOperationException("The environment variable 'PASSPHRASE' is not set.");
app.MapPost("/", (EndpointPayload body) =>
{
var decrypted = EncryptionUtils.DecryptRequest(body.encrypted_aes_key, body.encrypted_flow_data, body.initial_vector, PRIVATE_KEY, PASSPHRASE);
// Example to read decrypted fields
var action = decrypted.decryptedBody.GetProperty("action").GetString();
// Return the next screen & data to client
var response = new { screen = "SCREEN_NAME", data = new { some_key = "some_value" } };
var encryptedResponse = EncryptionUtils.EncryptResponse(response, decrypted.aesKeyBytes, decrypted.initialVectorBytes);
// Return the response as plaintext
return Results.Content(encryptedResponse, "text/plain");
})
.WithName("PostEndpointData");
app.Run();
record EndpointPayload(string encrypted_aes_key, string encrypted_flow_data, string initial_vector);
public class EncryptionUtils
{
const int TAG_LENGTH = 16;
public static (dynamic decryptedBody, byte[] aesKeyBytes, byte[] initialVectorBytes)
DecryptRequest(string encryptedAesKey, string encryptedFlowData, string initialVector, string privatePem, string passphrase)
{
using (var rsa = RSA.Create())
{
// Load the private key from PEM
var pemReader = new PemReader(new StringReader(privatePem), new PasswordFinder(passphrase));
if (pemReader.ReadObject() is AsymmetricCipherKeyPair keyPair)
{
// Extract the private key parameters
var privateKey = keyPair.Private as RsaPrivateCrtKeyParameters;
if (privateKey == null)
{
throw new CryptographicException("The provided PEM does not contain a valid RSA private key.");
}
// Convert Bouncy Castle RSA key parameters to .NET-compatible RSA parameters
var rsaParams = DotNetUtilities.ToRSAParameters(privateKey);
// Import into .NET RSA
rsa.ImportParameters(rsaParams);
}
else
{
throw new CryptographicException("The provided PEM is not a valid encrypted PKCS#1 RSA private key.");
}
// Decrypt the AES key created by the client
byte[] encryptedAesKeyBytes = Convert.FromBase64String(encryptedAesKey);
byte[] aesKeyBytes = rsa.Decrypt(encryptedAesKeyBytes, RSAEncryptionPadding.OaepSHA256);
// Decrypt the Flow data
byte[] initialVectorBytes = Convert.FromBase64String(initialVector);
byte[] flowDataBytes = Convert.FromBase64String(encryptedFlowData);
byte[] plainTextBytes = new byte[flowDataBytes.Length - TAG_LENGTH];
var cipher = new GcmBlockCipher(new AesEngine());
var parameters = new AeadParameters(new KeyParameter(aesKeyBytes), TAG_LENGTH * 8, initialVectorBytes);
cipher.Init(false, parameters);
var offset = cipher.ProcessBytes(flowDataBytes, 0, flowDataBytes.Length, plainTextBytes, 0);
cipher.DoFinal(plainTextBytes, offset);
string decryptedJsonString = Encoding.UTF8.GetString(plainTextBytes);
dynamic decryptedBody = JsonSerializer.Deserialize<dynamic>(decryptedJsonString);
return (decryptedBody: decryptedBody, aesKeyBytes: aesKeyBytes, initialVectorBytes: initialVectorBytes);
}
}
public static string EncryptResponse(dynamic response, byte[] aesKeyBytes, byte[] initialVectorBytes)
{
// Flip the initialization vector
byte[] flippedIV = initialVectorBytes.Select(b => (byte)~b).ToArray();
// Encrypt the response data
string jsonResponse = JsonSerializer.Serialize(response);
byte[] dataToEncrypt = Encoding.UTF8.GetBytes(jsonResponse);
var cipher = new GcmBlockCipher(new AesEngine());
var cipherParameters = new AeadParameters(new KeyParameter(aesKeyBytes), TAG_LENGTH * 8, flippedIV);
// Encrypt the data
cipher.Init(true, cipherParameters);
byte[] encryptedDataBytes = new byte[cipher.GetOutputSize(dataToEncrypt.Length)];
var offset = cipher.ProcessBytes(dataToEncrypt, 0, dataToEncrypt.Length, encryptedDataBytes, 0);
cipher.DoFinal(encryptedDataBytes, offset);
// Get the authentication tag
byte[] authTag = new byte[TAG_LENGTH];
Array.Copy(encryptedDataBytes, encryptedDataBytes.Length - TAG_LENGTH, authTag, 0, TAG_LENGTH);
// Concatenate encrypted data and auth tag, then return as base64
byte[] encryptedResponse = new byte[encryptedDataBytes.Length - TAG_LENGTH + TAG_LENGTH];
Array.Copy(encryptedDataBytes, 0, encryptedResponse, 0, encryptedDataBytes.Length - TAG_LENGTH);
Array.Copy(authTag, 0, encryptedResponse, encryptedDataBytes.Length - TAG_LENGTH, TAG_LENGTH);
return Convert.ToBase64String(encryptedResponse);
}
}
// Helper class for providing a password to the PemReader
public class PasswordFinder : IPasswordFinder
{
private readonly char[] _password;
public PasswordFinder(string password)
{
_password = password.ToCharArray();
}
public char[] GetPassword()
{
return _password;
}
}
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"log"
"os"
"github.com/gin-gonic/gin"
)
const nonceSize = 16
type endpointPayload struct {
EncryptedAESKey string `json:"encrypted_aes_key"`
EncryptedFlowData string `json:"encrypted_flow_data"`
InitialVector string `json:"initial_vector"`
}
type decryptionResult struct {
DecryptedBody map[string]interface{}
AESKeyBytes []byte
InitialVectorBytes []byte
}
func main() {
privateKey := os.Getenv("PRIVATE_KEY")
passphrase := os.Getenv("PASSPHRASE")
if privateKey == "" || passphrase == "" {
log.Fatal("Environment variables 'PRIVATE_KEY' and 'PASSPHRASE' are required.")
}
r := gin.Default()
r.POST("/", func(c *gin.Context) {
encryptedResponse, err := processRequest(c, privateKey, passphrase)
if err != nil {
log.Print(err)
c.String(500, "Internal Server Error")
return
}
// Return encrypted response as plain text
c.String(200, encryptedResponse)
})
r.Run(":3000")
}
func processRequest(c *gin.Context, privateKey string, passphrase string) (string, error) {
var payload endpointPayload
if err := c.ShouldBindJSON(&payload); err != nil {
return "", err
}
// Decrypt the request data
decrypted, err := decryptRequest(payload.EncryptedAESKey, payload.EncryptedFlowData, payload.InitialVector, privateKey, passphrase)
if err != nil {
return "", err
}
// Access decrypted fields
action, ok := decrypted.DecryptedBody["action"].(string)
if ok {
fmt.Printf("Action: %s\n", action)
}
// Create a response object
response := map[string]interface{}{
"screen": "SCREEN_NAME",
"data": map[string]string{"some_key": "some_value"},
}
// Encrypt the response
encryptedResponse, err := encryptResponse(response, decrypted.AESKeyBytes, decrypted.InitialVectorBytes)
if err != nil {
return "", err
}
return encryptedResponse, nil
}
func decryptRequest(encryptedAESKey string, encryptedFlowData string, initialVector string, privatePem string, passphrase string) (decryptionResult, error) {
// Parse the private key
block, _ := pem.Decode([]byte(privatePem))
if block == nil || !x509.IsEncryptedPEMBlock(block) {
return decryptionResult{}, errors.New("invalid PEM format or not encrypted")
}
decryptedKey, err := x509.DecryptPEMBlock(block, []byte(passphrase))
if err != nil {
return decryptionResult{}, err
}
privateKey, err := x509.ParsePKCS1PrivateKey(decryptedKey)
if err != nil {
return decryptionResult{}, err
}
// Decrypt the AES key
encryptedAESKeyBytes, _ := base64.StdEncoding.DecodeString(encryptedAESKey)
aesKeyBytes, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, privateKey, encryptedAESKeyBytes, nil)
if err != nil {
return decryptionResult{}, err
}
// Decrypt the Flow data
initialVectorBytes, _ := base64.StdEncoding.DecodeString(initialVector)
flowDataBytes, _ := base64.StdEncoding.DecodeString(encryptedFlowData)
blockCipher, err := aes.NewCipher(aesKeyBytes)
if err != nil {
return decryptionResult{}, err
}
gcm, err := cipher.NewGCMWithNonceSize(blockCipher, nonceSize)
if err != nil {
return decryptionResult{}, err
}
decryptedPlaintext, err := gcm.Open(nil, initialVectorBytes, flowDataBytes, nil)
if err != nil {
return decryptionResult{}, err
}
var decryptedBody map[string]interface{}
if err := json.Unmarshal(decryptedPlaintext, &decryptedBody); err != nil {
return decryptionResult{}, err
}
return decryptionResult{
DecryptedBody: decryptedBody,
AESKeyBytes: aesKeyBytes,
InitialVectorBytes: initialVectorBytes,
}, nil
}
func encryptResponse(response map[string]interface{}, aesKeyBytes, initialVectorBytes []byte) (string, error) {
// Flip the initialization vector
flippedIV := make([]byte, len(initialVectorBytes))
for i, b := range initialVectorBytes {
flippedIV[i] = ^b
}
// Encrypt the response
jsonResponse, err := json.Marshal(response)
if err != nil {
return "", err
}
blockCipher, err := aes.NewCipher(aesKeyBytes)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCMWithNonceSize(blockCipher, nonceSize)
if err != nil {
return "", err
}
encryptedData := gcm.Seal(nil, flippedIV, jsonResponse, nil)
return base64.StdEncoding.EncodeToString(encryptedData), nil
}
encrypted_aes_key field is calculated and how it can be decrypted.// demo encryption/decryption script
// put public key in public_key.pem file in the same folder as this script
// put private key in private_key.pem file in the same folder as this script
// run with: node <script-file-name>
import crypto from "crypto";
import fs from "fs";
const CLEAR_AES_KEY_STR = "<some-key-data>"
const PRIVATE_KEY_DATA = fs.readFileSync('private_key.pem', 'utf8');
const PUBLIC_KEY_DATA = fs.readFileSync('public_key.pem', 'utf8');
console.log("Clear key: " + CLEAR_AES_KEY_STR)
const encryptedAesKey = crypto.publicEncrypt(
{
key: PUBLIC_KEY_DATA,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: "sha256"
}
,
Buffer.from(CLEAR_AES_KEY_STR)
);
const encryptedAesKeyBase64 = Buffer.from(encryptedAesKey).toString('base64');
console.log("Encrypted base64 key: " + encryptedAesKeyBase64)
const decryptedAesKey = crypto.privateDecrypt(
{
key: crypto.createPrivateKey({
key: PRIVATE_KEY_DATA,
format: 'pem',
type: 'pkcs1',//ignored if format is pem
passphrase: '<passphrase>'
}),
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: "sha256",
},
Buffer.from(encryptedAesKeyBase64, "base64"),
);
console.log("Decrypted key: " + decryptedAesKey)
if (decryptedAesKey.toString() === CLEAR_AES_KEY_STR) {
console.log("Success, keys match!")
} else {
console.log("Failed, keys do not match!")
}