ruvnet/ruflo · error
Encrypted buffer too short: need >=
Error message
Encrypted buffer too short: need >= ${minLen}B, got ${buf.length}B What it means
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.
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)
Example fix
// before
const keys = decryptApiKeys(await readFile(keysPath, 'utf8'), pass); // plain .env text → too short
// after
const buf = await readFile(keysPath); // binary buffer written by encryptApiKeys
if (buf.length < 65) throw new Error(`${keysPath} is not an encrypted key store`);
const keys = decryptApiKeys(buf, pass); Defensive patterns
Strategy: validation
Validate before calling
const MIN_ENVELOPE = 32 + 16 + 16 + 1; // scrypt salt + AES-GCM IV + auth tag + >=1B ciphertext
const buf = await readFile(keysPath).catch(() => null);
if (!buf || buf.length < MIN_ENVELOPE) {
throw new Error('No encrypted key store yet — run encryptApiKeys first');
} Type guard
function isEncryptedKeyStore(buf: unknown): buf is Buffer {
return Buffer.isBuffer(buf) && buf.length >= 65; // 32 salt + 16 IV + 16 tag + >=1 ciphertext
} Try / catch
try {
const keys = decryptApiKeys(buf, passphrase);
} catch (e) {
if (e instanceof Error && e.message.startsWith('Encrypted buffer too short')) {
// input is not an encrypted store — re-run encryptApiKeys(envPath, passphrase) and persist its Buffer
} else {
throw e;
}
} Prevention
- 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
When it happens
Trigger: `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.
Common situations: 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.
Related errors
- Cannot select from empty array
- browser/eval: script must not be empty
- Buffer too small for RVFP preamble
- Buffer too small to contain RVFA preamble
- channel key must be 32 bytes (64 hex)
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/fc0273a61c715aaa.
Report an issue: GitHub.
Appendix: source
Thrown at v3/@claude-flow/cli/src/appliance/rvfa-builder.ts:82
/** Encrypt API keys from a .env file. Output: salt(32)+iv(16)+tag(16)+ciphertext */
export function encryptApiKeys(envPath: string, passphrase: string): Buffer {
const keys = parseEnvFile(readFileSync(envPath, 'utf-8'));
const plaintext = Buffer.from(JSON.stringify(keys), 'utf-8');
const salt = randomBytes(SCRYPT_SALT_LEN);
const key = scryptSync(passphrase, salt, SCRYPT_KEY_LEN, SCRYPT_OPTS);
const iv = randomBytes(AES_IV_LEN);
const cipher = createCipheriv(AES_ALG, key, iv);
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
return Buffer.concat([salt, iv, cipher.getAuthTag(), encrypted]);
}
/** Decrypt API keys previously encrypted with encryptApiKeys. */
export function decryptApiKeys(buf: Buffer, passphrase: string): Record<string, string> {
const minLen = SCRYPT_SALT_LEN + AES_IV_LEN + AES_TAG_LEN + 1;
if (buf.length < minLen) {
throw new Error(`Encrypted buffer too short: need >= ${minLen}B, got ${buf.length}B`);
}
let off = 0;
const salt = buf.subarray(off, off += SCRYPT_SALT_LEN);
const iv = buf.subarray(off, off += AES_IV_LEN);
const tag = buf.subarray(off, off += AES_TAG_LEN);
const ciphertext = buf.subarray(off);
const key = scryptSync(passphrase, salt, SCRYPT_KEY_LEN, SCRYPT_OPTS);
const decipher = createDecipheriv(AES_ALG, key, iv);
decipher.setAuthTag(tag);
return JSON.parse(
Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf-8'),
);
}
// ── Builder ──────────────────────────────────────────────────View on GitHub (pinned to fa13ee4ad6)