Setting Up Encrypted Webhooks

This guide outlines the process for setting up and using encrypted webhooks between the TAPI Portal and an Express.js server.

1. Generate a Secret Key

📘

Key generation

This guide outlines a common method for generating a cryptographically secure secret key. Technically the secret key can be any 32-character string. Using a cryptographically secure string helps ensure that the encryption cannot be cracked.

Generate a secure 16-byte (32-character) secret key:

openssl rand -hex 16

This command generates a 32-character hexadecimal string representing 16 bytes of random data.

Example output:

3a7bd3e2360a3d29eea436e7242deb2e

❗️

Secret key must be 32 characters

The secret key must be exactly 32 characters long for the encryption algorithm to work properly. The Transact backend will pad it out to 32 characters if necessary.

2. Set Up Secret Key in TAPI Portal

  1. Log in to the TAPI Portal.

  2. Navigate to /admin_v3/index.php/client/administrative/updatewebhook.

  3. Locate the field for entering the webhook secret key.

  4. Enter the generated secret key (all 32 characters from the previous step).

  5. Save the changes.

This step ensures that TAPI will use this key to encrypt webhook payloads.

3. Node.js Server (Receiver) Setup

This example uses the Node.js crypto library. You can find more information on how to use this library at:
https://nodejs.org/api/crypto.html

📘

Plus (+) character custom encoding

At the time of this writing +characters are encoded as plusencr and must be decoded on the receiving server prior to decryption. In the future custom encoding will be removed in favor of URL encoding at which time + will be encoded as %2B and will be decoded with the rest of the payload.

 str_replace('plusencr', '+', $encryptedData);

3.1 Store the Secret Key

Store the secret key securely on your Node.js server. For example, you might save it in an environment variable or a secure configuration file.

3.2 Decryption Function

Use the following Node.js function to decrypt incoming webhook payloads:

const crypto = require('crypto');

// Retrieve the secret key from your secure storage
const secretKey = process.env.WEBHOOK_SECRET_KEY; // or read from a secure config file

function decryptWebhookData(encryptedData) {
  // Decode custom '+' encoding, then base64 decode
  // In the future the custom encoding will be removed
  const decodedData = Buffer.from(encryptedData.replace(/plusencr/g, '+'), 'base64');
  // Base64 decode (with no custom encoding)
  //const decodedData = Buffer.from(encryptedData, 'base64');

  const iv = decodedData.slice(0, 16);
  const encrypted = decodedData.slice(16);

  // Create decipher
  const decipher = crypto.createDecipheriv('aes-256-cbc', secretKey, iv);

  // Decrypt
  let decrypted = decipher.update(encrypted);
  decrypted = Buffer.concat([decrypted, decipher.final()]);

  // Parse the decrypted data
  const decodedString = decrypted.toString('utf8');
  const parsedData = {};
  decodedString.split(', ').forEach((pair) => {
    const [key, value] = pair.split('=');
    parsedData[key] = decodeURIComponent(value);
  });

  return parsedData;
}

3.3 Usage in Express.js

Set up a route to handle incoming webhooks:

const crypto = require('crypto');
const express = require('express');
const morgan = require('morgan');

const app = express();
const port = process.env.PORT || 3000;

app.use(morgan('combined'));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

const secretKey = process.env.WEBHOOK_SECRET_KEY; 

function decryptWebhookData(encryptedData) {
  const decodedData = Buffer.from(encryptedData, 'base64');
  const iv = decodedData.slice(0, 16);
  const encrypted = decodedData.slice(16);

  // Create decipher
  const decipher = crypto.createDecipheriv('aes-256-cbc', secretKey, iv);

  // Decrypt
  let decrypted = decipher.update(encrypted);
  decrypted = Buffer.concat([decrypted, decipher.final()]);

  // Parse the decrypted data
  const decodedString = decrypted.toString('utf8');
  const parsedData = {};
  decodedString.split(', ').forEach((pair) => {
    const [key, value] = pair.split('=');
    parsedData[key] = decodeURIComponent(value);
  });

  return parsedData;
}

app.get('/', (req, res) => {
  console.log(req.params);
  res.send('Hello World!');
});

app.post('/', (req, res) => {
  console.log(req.body);
  
  const decryptedData = decryptWebhookData(req.body.params);
  console.log('Decrypted webhook data:', decryptedData);
  res.send('Received webhook!');
});

app.listen(port, () => {
  console.log(`Listening for webhooks on port ${port}`);
});

4. Security Considerations

  • Keep the secret key confidential and secure.
  • Use HTTPS for all webhook communications.
  • Regularly rotate the secret key by generating a new one and updating it in both the TAPI Portal and your Node.js server.

5. Troubleshooting

If you encounter decryption errors:

  • Ensure the secret key in your Node.js server exactly matches the one entered in the TAPI Portal.
  • Verify that the encrypted data is being properly received and passed to the decryption function.
  • Confirm that the key length used for decryption is exactly 16 bytes.

6. Notes

  • The TAPI Portal uses AES-256-CBC encryption for the webhooks.
  • The first 16 bytes of the decoded data represent the Initialization Vector (IV).
  • The encrypted payload is URL-safe Base64 encoded.
  • The decrypted data is in the format of a URL-encoded query string, separated by ', ' (comma and space).