Zie619/n8n-workflows · error · Error
AES Key is missing.
Error message
AES Key is missing.
What it means
Thrown by the 'Encrypt Return' Code node when `$('Decryption Code').first().json.aesKey` is empty/undefined. The 'Decryption Code' node (line ~206) RSA-decrypts the AES key and returns aesKey as base64; if that node did not run on this branch, its output is missing, or the `|| ""` default kicks in, encryption of the response cannot proceed.
Source
Thrown at workflows/Code/0924_Code_Respondtowebhook_Process_Webhook.json:220
-1320,
860
],
"parameters": {
"jsCode": "const crypto = require(\"crypto\");\n\nconst privateKey = `-----BEGIN PRIVATE KEY-----\n[INSERT YOUR KEY HERE]\n-----END PRIVATE KEY-----`;\n\n// Convert input buffers\nconst encryptedAesKeyBuffer = Buffer.from($json.encryptedAesKey.data);\nconst initialVector = Buffer.from($json.initialVector.data);\nconst encryptedFlowData = Buffer.from($json.encryptedFlowData.data);\n\n// Check if encrypted AES key, IV, and encrypted flow data exist\nif (!encryptedAesKeyBuffer || !initialVector || !encryptedFlowData) {\n throw new Error(\"Missing required data (encrypted AES key, IV, or flow data)\");\n}\n\n// Decrypt AES key using RSA\nconst decryptedKey = crypto.privateDecrypt(\n {\n key: privateKey,\n padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,\n oaepHash: \"sha256\",\n },\n encryptedAesKeyBuffer\n);\n\n// Ensure AES key is exactly 16 bytes (AES-128 requires it)\nconst aesKey = decryptedKey.slice(0, 16);\nif (aesKey.length !== 16) {\n throw new Error(\"Invalid AES Key length\");\n}\n\n// Handle initialization vector (IV): If needed, flip the IV bits (standardize behavior)\nconst standardizedIv = Buffer.from(initialVector);\nif (standardizedIv.length !== 16) {\n throw new Error(\"Invalid IV length, must be 16 bytes\");\n}\n\n// Extract the last 16 bytes as the authentication tag (GCM uses 16-byte tags)\nconst authTag = encryptedFlowData.slice(-16);\nconst encryptedDataWithoutTag = encryptedFlowData.slice(0, -16);\n\n// AES Decryption\nconst decipher = crypto.createDecipheriv(\"aes-128-gcm\", aesKey, standardizedIv);\ndecipher.setAuthTag(authTag);\n\nlet decrypted;\ntry {\n decrypted = Buffer.concat([\n decipher.update(encryptedDataWithoutTag),\n decipher.final(),\n ]);\n} catch (error) {\n throw new Error(\"Decryption failed: \" + error.message);\n}\n\nreturn [{ \n decryptedPayload: decrypted.toString(\"utf-8\"),\n aesKey: aesKey.toString(\"base64\")\n}];\n"
},
"typeVersion": 2,
"notes": "This code node performs automated tasks as part of the workflow."
},
{
"id": "17c055f3-c278-48c4-89d4-d305a35bc526",
"name": "Encrypt Return",
"type": "n8n-nodes-base.code",
"position": [
-200,
760
],
"parameters": {
"jsCode": "const crypto = require(\"crypto\");\n\n// Access initial_vector from the correct path\nconst initialVector = $('move to base64').first().json.initialVector;\n\nif (!initialVector) {\n throw new Error(\"Initial Vector is undefined or missing.\");\n}\n\n// Check if 'data' is a property of initialVector\nconst ivData = initialVector.data || initialVector; // Fallback to initialVector if no 'data' property\n\nif (!ivData) {\n throw new Error(\"Initial Vector 'data' is undefined or missing.\");\n}\n\n// Check for various formats of initialVector\nlet ivBuffer;\nif (typeof ivData === \"string\") {\n ivBuffer = Buffer.from(ivData, 'base64');\n} else if (Buffer.isBuffer(ivData)) {\n ivBuffer = ivData;\n} else if (Array.isArray(ivData)) {\n ivBuffer = Buffer.from(ivData);\n} else {\n throw new Error(\"Initial Vector 'data' is in an unsupported format.\");\n}\n\n// Invert Initialization Vector\nconst invertedIV = Buffer.from(ivBuffer.map((b) => ~b & 0xFF)); // Ensure the result stays a valid byte\n\n// Access AES Key from the correct path\nconst aesKeyBase64 = $('Decryption Code').first().json.aesKey || \"\";\nif (!aesKeyBase64) {\n throw new Error(\"AES Key is missing.\");\n}\n\nconst aesKey = Buffer.from(aesKeyBase64, \"base64\");\n\n// Extract data from the input with proper error handling\nlet date = \"2025-03-14\"; // Default fallback date\nlet startTimes = []; // Default empty array for start times\n\n// Check if $json exists and has the expected structure\nif ($json) {\n // Check if $json is an array\n if (Array.isArray($json) && $json.length > 0) {\n const appointmentData = $json[0];\n if (appointmentData && appointmentData.appointment_date) {\n date = appointmentData.appointment_date;\n }\n if (appointmentData && Array.isArray(appointmentData.start_times)) {\n startTimes = appointmentData.start_times;\n }\n } else if ($json.appointment_date) {\n // If $json is not an array but has appointment_date directly\n date = $json.appointment_date;\n if (Array.isArray($json.start_times)) {\n startTimes = $json.start_times;\n }\n }\n}\n\n// Log the structure of $json for debugging\nconsole.log(\"Input JSON structure:\", JSON.stringify($json, null, 2));\n\n// Ensure we have time slots (use defaults if none found)\nif (!startTimes.length) {\n console.log(\"No time slots found in input, using defaults\");\n startTimes = [\"12:00:00\", \"12:30:00\", \"13:30:00\", \"14:00:00\"];\n}\n\n// Map the time slots to the required format\nconst timeSlots = startTimes.map((timeString, index) => ({\n id: `time_${index + 1}`,\n title: timeString\n}));\n\n// Map the date slots for each time slot\nconst dateSlots = [{\n id: \"date_1\",\n title: date\n}];\n\n// Define the response data with the extracted time and date\nconst responseData = {\n status: \"active\",\n time: timeSlots,\n date: dateSlots\n};\n\n// Define the flow_token (accessed from the correct path)\nconst flowToken = $('Json Parser').first().json.flow_token || \"\"; // Fetch the flow_token dynamically from the path\n\nif (!flowToken) {\n throw new Error(\"Flow token is missing.\");\n}\n\n// Define the next screen (this should be based on your flow logic)\nconst nextScreen = \"APPOINTMENT\"; // You can set this dynamically depending on the flow\n\n// Define Response Message (updated to match the required response format)\nconst responseMessage = JSON.stringify({\n version: \"3.0\", // Fixed version as per your requirements\n action: \"data_exchange\", // Since we're responding to a data exchange request\n screen: nextScreen, // The next screen that the user will be redirected to\n data: responseData, // Data to send back (includes the time and date)\n flow_token: flowToken, // Flow token for session identification\n});\n\n// Encrypt Response using AES-GCM\nconst cipher = crypto.createCipheriv(\"aes-128-gcm\", aesKey, invertedIV);\nlet encryptedResponse = Buffer.concat([\n cipher.update(responseMessage, \"utf-8\"),\n cipher.final()\n]);\n\n// Get the authentication tag\nconst authTag = cipher.getAuthTag();\n\n// Append the authentication tag to the encrypted response\nconst result = Buffer.concat([encryptedResponse, authTag]);\n\n// Encode the entire response as Base64\nconst base64Response = result.toString(\"base64\");\n\n// Return the Base64-encoded response as the body\nreturn [{ body: base64Response }];\n"
},
"typeVersion": 2,
"notes": "This code node performs automated tasks as part of the workflow."
},
{
"id": "412f55e3-5867-4e65-a494-3e3bf991d59c",
"name": "Encrypt Return1",
"type": "n8n-nodes-base.code",
"position": [
-200,
1000
],
"parameters": {
"jsCode": "const crypto = require(\"crypto\");\n\nconst jsonData = items;\n\n// Parse the decryptedPayload string into a JSON object\nconst decryptedPayload = JSON.parse(jsonData[0].json.originalPayload.decryptedPayload);\n\n// Extract the seats array\nconst seats = decryptedPayload.data.seats;\n\nif (!seats || !Array.isArray(seats) || seats.length === 0) {\n throw new Error(\"Seats data is missing or invalid.\");\n}\n\n// Access initial_vector from the correct path\nconst initialVector = $('move to base64').first().json.initialVector;\nif (!initialVector) {\n throw new Error(\"Initial Vector is undefined or missing.\");\n}\n\nconst ivData = initialVector.data || initialVector;\nif (!ivData) {\n throw new Error(\"Initial Vector 'data' is undefined or missing.\");\n}\n\nlet ivBuffer;\nif (typeof ivData === \"string\") {\n ivBuffer = Buffer.from(ivData, 'base64');\n} else if (Buffer.isBuffer(ivData)) {\n ivBuffer = ivData;\n} else if (Array.isArray(ivData)) {\n ivBuffer = Buffer.from(ivData);\n} else {\n throw new Error(\"Initial Vector 'data' is in an unsupported format.\");\n}\n\nconst invertedIV = Buffer.from(ivBuffer.map((b) => ~b & 0xFF));\n\n// Access AES Key from the correct path\nconst aesKeyBase64 = $('Decryption Code').first().json.aesKey || \"\";\nif (!aesKeyBase64) {\n throw new Error(\"AES Key is missing.\");\n}\nconst aesKey = Buffer.from(aesKeyBase64, \"base64\");\n\n// Define the response data with the extracted seats\nconst responseData = {\n status: \"active\",\n seats: seats.map((seat, index) => ({\n id: `seat_${index + 1}`,\n title: seat\n }))\n};\n\n// Define the flow_token\nconst flowToken = $('Json Parser').first().json.flow_token || \"\";\nif (!flowToken) {\n throw new Error(\"Flow token is missing.\");\n}\n\nconst nextScreen = \"SUMMARY\";\n\nconst responseMessage = JSON.stringify({\n version: \"3.0\",\n action: \"data_exchange\",\n screen: nextScreen,\n data: responseData,\n flow_token: flowToken,\n});\n\n// Encrypt Response using AES-GCM\nconst cipher = crypto.createCipheriv(\"aes-128-gcm\", aesKey, invertedIV);\nlet encryptedResponse = Buffer.concat([\n cipher.update(responseMessage, \"utf-8\"),\n cipher.final()\n]);\n\nconst authTag = cipher.getAuthTag();\nconst result = Buffer.concat([encryptedResponse, authTag]);\nconst base64Response = result.toString(\"base64\");\n\n// Return the encrypted response\nreturn [{ body: base64Response }];\n"
},
"typeVersion": 2,
"notes": "This code node performs automated tasks as part of the workflow."
},View on GitHub (pinned to 94007c1445)
Solutions
- Verify the 'Decryption Code' node runs before 'Encrypt Return' on the same branch and check its execution output contains aesKey.
- Replace the placeholder private key in 'Decryption Code' with the real RSA private key for your WhatsApp Flows app and confirm decryption succeeds.
- Confirm the webhook body includes encrypted_aes_key so RSA privateDecrypt yields a 16-byte key.
- Distinguish 'node never ran' from 'empty value': if ($('Decryption Code').first().json.aesKey === undefined) throw new Error('Decryption Code did not run');
Example fix
// before
const aesKeyBase64 = $('Decryption Code').first().json.aesKey || "";
if (!aesKeyBase64) { throw new Error("AES Key is missing."); }
// after
const dec = $('Decryption Code').first().json;
if (dec.aesKey === undefined) throw new Error("Decryption Code node did not execute - check branch order");
if (typeof dec.aesKey !== 'string' || dec.aesKey.length === 0) throw new Error("AES Key is missing - RSA decryption produced no key");
const aesKey = Buffer.from(dec.aesKey, 'base64');
if (aesKey.length !== 16) throw new Error(`AES Key must be 16 bytes, got ${aesKey.length}`); Defensive patterns
Strategy: validation
Validate before calling
const dec = $('Decryption Code').first().json;
if (dec.aesKey === undefined) throw new Error('Decryption Code did not run - check branch order');
const aesKey = Buffer.from(dec.aesKey, 'base64');
if (!Buffer.isBuffer(aesKey) || aesKey.length !== 16) throw new Error('AES key must decode to 16 bytes'); Type guard
const isValidAesKey = (b64) => {
if (typeof b64 !== 'string' || b64.length === 0) return false;
try { return Buffer.from(b64, 'base64').length === 16; } catch { return false; }
}; Try / catch
try {
const aesKey = Buffer.from(aesKeyBase64, 'base64');
crypto.createCipheriv('aes-128-gcm', aesKey, invertedIV);
} catch (e) {
throw new Error(`AES setup failed (key b64 len ${aesKeyBase64?.length}): ${e.message}`);
} Prevention
- Replace template private-key placeholders before enabling the flow
- Store the RSA private key in n8n credentials or env vars, not inline code
- Distinguish 'node did not run' (undefined) from 'empty value' in guards
- Run the decrypt branch before any encrypt branch in every execution path
When it happens
Trigger: The 'Decryption Code' node has not executed before 'Encrypt Return' on the current path (no connection or different branch); the node name was renamed so `$('Decryption Code')` resolves to stale output; the RSA private key placeholder `[INSERT YOUR KEY HERE]` caused Decryption Code to fail or return without aesKey; the webhook payload had no encrypted_aes_key so decryption produced nothing.
Common situations: Reorganizing the workflow canvas and breaking the execution order dependency; keeping the template's placeholder private key instead of a real one; name typos after renaming nodes; testing the encrypt branch in isolation with 'Execute node' so upstream data is unavailable.
Related errors
- Initial Vector 'data' is undefined or missing.
- Initial Vector 'data' is in an unsupported format.
- Flow token is missing.
- Seats data is missing or invalid.
- Insufficient stock for ${$('Retrieve Issue Request Details')
AI-assisted analysis of Zie619/n8n-workflows@94007c1445 (2026-08-15).
Data as JSON: /api/errors/c370ee929484919f.
Report an issue: GitHub.