ruvnet/ruflo · error · Error

decryptBuffer: bad magic — blob is not Ruflo-encrypted (RFE1

Error message

decryptBuffer: bad magic — blob is not Ruflo-encrypted (RFE1)

What it means

Thrown by decryptBuffer() when the first 4 bytes of the blob do not equal the MAGIC bytes 'RFE1' (compared with timingSafeEqual). The blob is long enough to have a header but is not a Ruflo-encrypted blob — typically a plaintext value or ciphertext from a different/older encryption scheme. The guard exists so a mistaken plaintext input fails loudly rather than yielding garbage plaintext.

Source

Thrown at v3/@claude-flow/cli/src/encryption/vault.ts:163

 * garbage.
 */
export function decryptBuffer(blob: Buffer, key: Buffer): Buffer {
  if (!Buffer.isBuffer(blob)) {
    throw new TypeError('decryptBuffer: blob must be a Buffer');
  }
  if (!Buffer.isBuffer(key) || key.length !== KEY_LEN) {
    throw new TypeError(`decryptBuffer: key must be a ${KEY_LEN}-byte Buffer`);
  }
  if (blob.length < MIN_BLOB_LEN) {
    throw new Error(
      `decryptBuffer: blob too short (${blob.length}B; need >= ${MIN_BLOB_LEN}B)`,
    );
  }
  const magic = blob.subarray(0, MAGIC_LEN);
  // timingSafeEqual to avoid an oracle on the magic bytes specifically;
  // not strictly required (the magic isn't secret) but cheap and correct.
  if (!timingSafeEqual(magic, MAGIC)) {
    throw new Error(
      'decryptBuffer: bad magic — blob is not Ruflo-encrypted (RFE1)',
    );
  }
  const iv = blob.subarray(MAGIC_LEN, MAGIC_LEN + IV_LEN);
  const tag = blob.subarray(blob.length - TAG_LEN);
  const ciphertext = blob.subarray(MAGIC_LEN + IV_LEN, blob.length - TAG_LEN);

  const decipher = createDecipheriv(ALG, key, iv);
  decipher.setAuthTag(tag);
  return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
}

/**
 * Magic-byte sniff. True iff the blob starts with the RFE1 magic AND is
 * long enough to be a valid encrypted blob. Used by readers during the
 * incremental migration: legacy plaintext files return false and flow
 * through the existing read path unchanged.
 *

View on GitHub (pinned to 6b01dc5a68)

Solutions

  1. Gate decryption with isEncryptedBlob() and skip plaintext values (or migrate them) rather than unconditionally decrypting.
  2. If migrating an old format, write a one-time converter that re-encrypts under the current scheme.
  3. Confirm the file actually came from encryptBuffer() (the only writer of the RFE1 magic).

Example fix

// before
decryptBuffer(maybeCiphertext, key)   // bad magic
// after
import { isEncryptedBlob } from '../encryption/vault.js';
const out = isEncryptedBlob(maybeCiphertext)
  ? decryptBuffer(maybeCiphertext, key)
  : maybeCiphertext; // already plaintext
Defensive patterns

Strategy: type-guard

Validate before calling

import { isEncryptedBlob } from '../encryption/vault.js';
function readSecret(buf: Buffer, key: Buffer): Buffer {
  return isEncryptedBlob(buf) ? decryptBuffer(buf, key) : buf; // plaintext passthrough
}

Type guard

import { isEncryptedBlob } from '../encryption/vault.js';
const isRufloCiphertext = (b: Buffer): boolean => isEncryptedBlob(b);

Try / catch

try {
  decryptBuffer(blob, key);
} catch (e) {
  const msg = e instanceof Error ? e.message : String(e);
  if (msg.includes('bad magic')) {
    // probably plaintext from a legacy store; handle migration
    return blob;
  }
  throw e;
}

Prevention

When it happens

Trigger: Passing a plaintext secret (never encrypted) to decryptBuffer, passing ciphertext produced by a different tool or an older format without the RFE1 magic, or a buffer that begins mid-stream.

Common situations: Migrating from a previous non-vault secret store and forgetting to encrypt first, a field that is sometimes plaintext and sometimes encrypted, or a down-level vault format predating the magic header.

Related errors


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/56ed9a7dfbaa21ff. Report an issue: GitHub.