{"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":"Error","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/6b01dc5a687b26b3e218f796de45ec51f8fa9e8c/v3/@claude-flow/cli/src/appliance/rvfa-builder.ts#L64-L100","documentation":"Thrown by decryptApiKeys() when the supplied buffer is shorter than the minimum AES-256-GCM envelope (32B salt + 16B IV + 16B auth tag + at least 1B ciphertext = 65 bytes). The encryption format is produced only by the paired encryptApiKeys() in rvfa-builder.ts, so any buffer that does not originate from that function is invalid. This guard prevents feeding truncated or foreign data into scrypt/AES which would otherwise read garbage offsets.","triggerScenarios":"Calling decryptApiKeys(buf, passphrase) with a buffer under 65 bytes. Most commonly: passing the raw base64 string instead of Buffer.from(b64, 'base64'); passing an empty buffer; passing a JSON object that was not the encrypted output; truncating the vault field read from an RVFA section.","commonSituations":"The vault is stored in the appliance as enc.toString('base64') — forgetting to base64-decode it back to a Buffer before decrypting is the most frequent mistake. Other cases: reading the wrong .env file, a corrupted appliance image, or passing the plaintext keys buffer by accident.","solutions":["Ensure the buffer came from encryptApiKeys() and round-trips as Buffer (not a string).","If reading from the RVFA vault field, base64-decode first: Buffer.from(vault.encrypted, 'base64').","Confirm the buffer length is at least 65 before calling decryptApiKeys.","Re-derive the buffer from a known-good encrypted source if it was corrupted in transit."],"exampleFix":"// before\nconst keys = decryptApiKeys(vault.encrypted, passphrase); // encrypted is a base64 string\n\n// after\nconst keys = decryptApiKeys(Buffer.from(vault.encrypted, 'base64'), passphrase);","handlingStrategy":"validation","validationCode":"const MIN_ENC_LEN = 32 + 16 + 16 + 1; // salt + iv + tag + >=1 byte ciphertext\nfunction isValidEncryptedVault(buf: Buffer): boolean {\n  return Buffer.isBuffer(buf) && buf.length >= MIN_ENC_LEN;\n}","typeGuard":"function isEncryptedApiKeysBuffer(buf: unknown): buf is Buffer {\n  return Buffer.isBuffer(buf) && buf.length >= 65;\n}","tryCatchPattern":"try {\n  const keys = decryptApiKeys(buf, passphrase);\n} catch (e) {\n  if (/Encrypted buffer too short/.test((e as Error).message)) {\n    throw new Error('Vault buffer is not a valid encryptApiKeys() output; expected base64-decoded Buffer >= 65 bytes');\n  }\n  throw e;\n}","preventionTips":["Always round-trip the vault as a Buffer: store with enc.toString('base64'), restore with Buffer.from(b64, 'base64').","Never pass a raw string, JSON object, or the plaintext .env to decryptApiKeys.","Add a unit test that encrypts then decrypts to catch format regressions.","Log buf.length before decrypting in debug builds to catch truncation early."],"tags":["crypto","buffer","aes-gcm","validation"],"backgroundTag":null,"analyzedSha":"6b01dc5a687b26b3e218f796de45ec51f8fa9e8c","analyzedAt":"2026-08-12T13:20:50.148Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}