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

  1. Confirm the input came from encryptApiKeys: run the encrypt step first and persist its returned Buffer
  2. Check size before decrypting: buf.length >= 65 (32 salt + 16 IV + 16 tag + ≥1 ciphertext)
  3. If the store lives on disk, verify the file exists and stat > 0 instead of swallowing ENOENT into an empty buffer
  4. 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

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


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)