{"record":{"id":"fc0273a61c715aaa","repo":"ruvnet/ruflo","slug":"encrypted-buffer-too-short-need-minlen-b-go","errorCode":null,"errorMessage":"Encrypted buffer too short: need >= ${minLen}B, got ${buf.length}B","messagePattern":"Encrypted buffer too short: need >= (.+?)B, got (.+?)B","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/appliance/rvfa-builder.ts","lineNumber":82,"sourceCode":"/** Encrypt API keys from a .env file. Output: salt(32)+iv(16)+tag(16)+ciphertext */\nexport function encryptApiKeys(envPath: string, passphrase: string): Buffer {\n  const keys = parseEnvFile(readFileSync(envPath, 'utf-8'));\n  const plaintext = Buffer.from(JSON.stringify(keys), 'utf-8');\n\n  const salt = randomBytes(SCRYPT_SALT_LEN);\n  const key = scryptSync(passphrase, salt, SCRYPT_KEY_LEN, SCRYPT_OPTS);\n  const iv = randomBytes(AES_IV_LEN);\n  const cipher = createCipheriv(AES_ALG, key, iv);\n  const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);\n\n  return Buffer.concat([salt, iv, cipher.getAuthTag(), encrypted]);\n}\n\n/** Decrypt API keys previously encrypted with encryptApiKeys. */\nexport function decryptApiKeys(buf: Buffer, passphrase: string): Record<string, string> {\n  const minLen = SCRYPT_SALT_LEN + AES_IV_LEN + AES_TAG_LEN + 1;\n  if (buf.length < minLen) {\n    throw new Error(`Encrypted buffer too short: need >= ${minLen}B, got ${buf.length}B`);\n  }\n\n  let off = 0;\n  const salt = buf.subarray(off, off += SCRYPT_SALT_LEN);\n  const iv = buf.subarray(off, off += AES_IV_LEN);\n  const tag = buf.subarray(off, off += AES_TAG_LEN);\n  const ciphertext = buf.subarray(off);\n\n  const key = scryptSync(passphrase, salt, SCRYPT_KEY_LEN, SCRYPT_OPTS);\n  const decipher = createDecipheriv(AES_ALG, key, iv);\n  decipher.setAuthTag(tag);\n\n  return JSON.parse(\n    Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf-8'),\n  );\n}\n\n// ── Builder ──────────────────────────────────────────────────","sourceCodeStart":64,"sourceCodeEnd":100,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/cli/src/appliance/rvfa-builder.ts#L64-L100","documentation":"decryptApiKeys expects the exact envelope encryptApiKeys produces: 32-byte scrypt salt + 16-byte AES-GCM IV + 16-byte auth tag + at least 1 byte of ciphertext — a hard minimum of 65 bytes. The guard fires before any crypto runs, so the input simply is not an encryptApiKeys artifact: empty, plaintext, truncated, or the wrong encoding.","triggerScenarios":"`decryptApiKeys(buf, passphrase)` with an empty Buffer (often the result of catching a missing-file error and defaulting to Buffer.alloc(0)); passing the raw plaintext .env instead of the encrypted output; passing a base64 string without decoding; a truncated copy of the key store.","commonSituations":"Reading the encrypted key file before the first encryptApiKeys(envPath, passphrase) ever ran; pointing decrypt at the original .env path; pipelines that persist the buffer as text and lose bytes; reading with readFile(path, 'utf8') instead of binary.","solutions":["Confirm the input came from encryptApiKeys: run the encrypt step first and persist its returned Buffer","Check size before decrypting: buf.length >= 65 (32 salt + 16 IV + 16 tag + ≥1 ciphertext)","If the store lives on disk, verify the file exists and stat > 0 instead of swallowing ENOENT into an empty buffer","If you stored base64, decode first: decryptApiKeys(Buffer.from(b64, 'base64'), passphrase)"],"exampleFix":"// before\nconst keys = decryptApiKeys(await readFile(keysPath, 'utf8'), pass); // plain .env text → too short\n\n// after\nconst buf = await readFile(keysPath); // binary buffer written by encryptApiKeys\nif (buf.length < 65) throw new Error(`${keysPath} is not an encrypted key store`);\nconst keys = decryptApiKeys(buf, pass);","handlingStrategy":"validation","validationCode":"const MIN_ENVELOPE = 32 + 16 + 16 + 1; // scrypt salt + AES-GCM IV + auth tag + >=1B ciphertext\nconst buf = await readFile(keysPath).catch(() => null);\nif (!buf || buf.length < MIN_ENVELOPE) {\n  throw new Error('No encrypted key store yet — run encryptApiKeys first');\n}","typeGuard":"function isEncryptedKeyStore(buf: unknown): buf is Buffer {\n  return Buffer.isBuffer(buf) && buf.length >= 65; // 32 salt + 16 IV + 16 tag + >=1 ciphertext\n}","tryCatchPattern":"try {\n  const keys = decryptApiKeys(buf, passphrase);\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('Encrypted buffer too short')) {\n    // input is not an encrypted store — re-run encryptApiKeys(envPath, passphrase) and persist its Buffer\n  } else {\n    throw e;\n  }\n}","preventionTips":["Write encryptApiKeys output to a dedicated binary path; never point decrypt at the raw .env","Treat missing or short key files as 'not provisioned' and re-run encryption rather than passing empties","Always read with binary readFile (no 'utf8' encoding) and decode base64 explicitly before decrypting"],"tags":["rvfa-builder","crypto","aes-gcm","buffer","input-validation"],"backgroundTag":"invalid-ciphertext-format","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}