{"record":{"id":"c370ee929484919f","repo":"Zie619/n8n-workflows","slug":"aes-key-is-missing","errorCode":null,"errorMessage":"AES Key is missing.","messagePattern":"AES Key is missing\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"workflows/Code/0924_Code_Respondtowebhook_Process_Webhook.json","lineNumber":220,"sourceCode":"        -1320,\n        860\n      ],\n      \"parameters\": {\n        \"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\"\n      },\n      \"typeVersion\": 2,\n      \"notes\": \"This code node performs automated tasks as part of the workflow.\"\n    },\n    {\n      \"id\": \"17c055f3-c278-48c4-89d4-d305a35bc526\",\n      \"name\": \"Encrypt Return\",\n      \"type\": \"n8n-nodes-base.code\",\n      \"position\": [\n        -200,\n        760\n      ],\n      \"parameters\": {\n        \"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\"\n      },\n      \"typeVersion\": 2,\n      \"notes\": \"This code node performs automated tasks as part of the workflow.\"\n    },\n    {\n      \"id\": \"412f55e3-5867-4e65-a494-3e3bf991d59c\",\n      \"name\": \"Encrypt Return1\",\n      \"type\": \"n8n-nodes-base.code\",\n      \"position\": [\n        -200,\n        1000\n      ],\n      \"parameters\": {\n        \"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\"\n      },\n      \"typeVersion\": 2,\n      \"notes\": \"This code node performs automated tasks as part of the workflow.\"\n    },","sourceCodeStart":202,"sourceCodeEnd":238,"githubUrl":"https://github.com/Zie619/n8n-workflows/blob/94007c1445d9258a7da116646b79473e7c7c3282/workflows/Code/0924_Code_Respondtowebhook_Process_Webhook.json#L202-L238","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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');"],"exampleFix":"// before\nconst aesKeyBase64 = $('Decryption Code').first().json.aesKey || \"\";\nif (!aesKeyBase64) { throw new Error(\"AES Key is missing.\"); }\n\n// after\nconst dec = $('Decryption Code').first().json;\nif (dec.aesKey === undefined) throw new Error(\"Decryption Code node did not execute - check branch order\");\nif (typeof dec.aesKey !== 'string' || dec.aesKey.length === 0) throw new Error(\"AES Key is missing - RSA decryption produced no key\");\nconst aesKey = Buffer.from(dec.aesKey, 'base64');\nif (aesKey.length !== 16) throw new Error(`AES Key must be 16 bytes, got ${aesKey.length}`);","handlingStrategy":"validation","validationCode":"const dec = $('Decryption Code').first().json;\nif (dec.aesKey === undefined) throw new Error('Decryption Code did not run - check branch order');\nconst aesKey = Buffer.from(dec.aesKey, 'base64');\nif (!Buffer.isBuffer(aesKey) || aesKey.length !== 16) throw new Error('AES key must decode to 16 bytes');","typeGuard":"const isValidAesKey = (b64) => {\n  if (typeof b64 !== 'string' || b64.length === 0) return false;\n  try { return Buffer.from(b64, 'base64').length === 16; } catch { return false; }\n};","tryCatchPattern":"try {\n  const aesKey = Buffer.from(aesKeyBase64, 'base64');\n  crypto.createCipheriv('aes-128-gcm', aesKey, invertedIV);\n} catch (e) {\n  throw new Error(`AES setup failed (key b64 len ${aesKeyBase64?.length}): ${e.message}`);\n}","preventionTips":["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"],"tags":["n8n","whatsapp-flows","rsa","aes-gcm","dependency-order"],"backgroundTag":null,"analyzedSha":"94007c1445d9258a7da116646b79473e7c7c3282","analyzedAt":"2026-08-15T04:10:37.591Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}