{"record":{"id":"e84b01147aaab8ce","repo":"santifer/career-ops","slug":"mokahr-necromancer-key-is-key-length-bytes-ex","errorCode":null,"errorMessage":"mokahr: necromancer key is ${key.length} bytes, expected 16 (aes-128-cbc)","messagePattern":"mokahr: necromancer key is (.+?) bytes, expected 16 \\(aes-128-cbc\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/mokahr.mjs","lineNumber":133,"sourceCode":"  const pathname = u.pathname.replace(/\\/$/, '');\n  if (ROBOTS_EXCLUDED_PATHS.has(pathname)) return null;\n  return { orgId: m[1], siteId, baseUrl: `${u.origin}${pathname}` };\n}\n\n/**\n * Decrypt one `{data, necromancer}` envelope into the plaintext response.\n * Exported for tests — deliberately separate from the HTTP call so tests\n * never need a real network round-trip to exercise the crypto.\n * @param {{ data?: string, necromancer?: string }} envelope\n * @returns {any}\n */\nexport function decryptMokaHrEnvelope(envelope) {\n  if (!envelope?.data || !envelope?.necromancer) {\n    throw new Error('mokahr: response missing data/necromancer — not the expected envelope shape');\n  }\n  const key = Buffer.from(envelope.necromancer, 'utf8');\n  if (key.length !== 16) {\n    throw new Error(`mokahr: necromancer key is ${key.length} bytes, expected 16 (aes-128-cbc)`);\n  }\n  const ciphertext = Buffer.from(envelope.data, 'base64');\n  const decipher = createDecipheriv('aes-128-cbc', key, AES_IV);\n  const plain = Buffer.concat([decipher.update(ciphertext), decipher.final()]);\n  return JSON.parse(plain.toString('utf8'));\n}\n\n/**\n * @param {any} decrypted - Already-decrypted response body.\n * @param {string} companyName\n * @param {string} tenantBaseUrl - Validated tenant careers URL without a trailing slash.\n * @returns {import('./_types.js').Job[]}\n */\nexport function parseMokaHrJobs(decrypted, companyName, tenantBaseUrl) {\n  const list = decrypted?.data?.jobs;\n  if (!Array.isArray(list)) return [];\n\n  const jobs = [];","sourceCodeStart":115,"sourceCodeEnd":151,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/mokahr.mjs#L115-L151","documentation":"The necromancer field is the raw AES-128 key and must be exactly 16 bytes of UTF-8 for aes-128-cbc. decryptMokaHrEnvelope() measures the Buffer length and throws this error when it differs. A wrong-length key means createDecipheriv would otherwise fail or the API changed its key format.","triggerScenarios":"The decrypted envelope's necromancer string is empty, truncated, base64/hex-encoded when the code expects raw UTF-8, or a longer/shorter key introduced by an API change — any UTF-8 encoding that is not exactly 16 bytes.","commonSituations":"MokaHR rotates to a 32-byte (AES-256) key without the provider being updated; the envelope was partially parsed so a nested/wrong field was passed as the key; double-decoding the field (e.g. Buffer.from(x,'base64') first) changes its byte length.","solutions":["Log Buffer.byteLength(envelope.necromancer) and the first bytes of the key to see what arrived.","Confirm the envelope you passed in is the raw API response, not a re-parsed/wrapped object where necromancer picked up the wrong field.","If MokaHR moved to 32-byte keys, switch the cipher to aes-256-cbc and the IV length accordingly in decryptMokaHrEnvelope.","If the key is base64/hex encoded, decode it to raw bytes before measuring/using it."],"exampleFix":"// before\nconst key = Buffer.from(envelope.necromancer, 'utf8');\nconst decipher = createDecipheriv('aes-128-cbc', key, AES_IV);\n// after (if the API now sends a 32-byte key)\nconst key = Buffer.from(envelope.necromancer, 'utf8');\nconst algo = key.length === 16 ? 'aes-128-cbc' : 'aes-256-cbc';\nconst decipher = createDecipheriv(algo, key, AES_IV.slice(0, algo === 'aes-256-cbc' ? 16 : AES_IV.length));","handlingStrategy":"validation","validationCode":null,"typeGuard":"const isAes128Key = (s) => typeof s === 'string' && Buffer.byteLength(s, 'utf8') === 16;","tryCatchPattern":"try {\n  decrypted = decryptMokaHrEnvelope(envelope);\n} catch (err) {\n  if (String(err.message).includes('expected 16')) {\n    console.error(`Key length was wrong; necromancer bytes=${Buffer.byteLength(envelope?.necromancer ?? '', 'utf8')}`);\n    return partialResults;\n  }\n  throw err;\n}","preventionTips":["Pass the raw API envelope straight into decryptMokaHrEnvelope — never re-encode the necromancer field first.","Log key byte length on failure to spot AES-256 migrations quickly.","Record a known-good envelope in tests so key-format changes fail CI, not production scans.","Do not base64/hex-decode the key unless the provider code says to."],"tags":["crypto","aes","key-length","mokahr"],"backgroundTag":"invalid-argument-value","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}