mastra-ai/mastra · error

Invalid ciphertext format

Error message

Invalid ciphertext format

What it means

decrypt() expects ciphertext in the format '<ALGO_PREFIX>:<...>' produced by encrypt(). The algorithm prefix is read before the first colon; if the string contains no colon at all it cannot be a valid encrypted payload, so the function throws immediately.

Source

Thrown at channels/slack/src/crypto.ts:81

export function encrypt(plaintext: string, key: string): string {
  const salt = randomBytes(16);
  const derived = Buffer.from(hkdfSync('sha256', key, salt, 'mastra-slack-encryption', 32));
  const iv = randomBytes(12);
  const cipher = createCipheriv('aes-256-gcm', derived, iv);

  const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
  const authTag = cipher.getAuthTag();

  return `${ALGO_PREFIX}:${salt.toString('base64')}:${iv.toString('base64')}:${authTag.toString('base64')}:${encrypted.toString('base64')}`;
}

/**
 * Decrypt data produced by encrypt().
 */
export function decrypt(ciphertext: string, key: string): string {
  const colonIdx = ciphertext.indexOf(':');
  if (colonIdx === -1) {
    throw new Error('Invalid ciphertext format');
  }

  const prefix = ciphertext.slice(0, colonIdx);
  if (prefix !== ALGO_PREFIX) {
    throw new Error(`Unsupported encryption algorithm: ${prefix}`);
  }

  const payload = ciphertext.slice(colonIdx + 1);
  const [saltB64, ivB64, authTagB64, encryptedB64] = payload.split(':');
  if (!saltB64 || !ivB64 || !authTagB64 || encryptedB64 === undefined) {
    throw new Error('Invalid ciphertext payload');
  }

  const salt = Buffer.from(saltB64, 'base64');
  const derived = Buffer.from(hkdfSync('sha256', key, salt, 'mastra-slack-encryption', 32));
  const iv = Buffer.from(ivB64, 'base64');
  const authTag = Buffer.from(authTagB64, 'base64');
  const encrypted = Buffer.from(encryptedB64, 'base64');

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Re-save the affected values through the library's encrypt path (or reinstall/reconnect the channel) so plaintext/stale values are replaced with properly encrypted ciphertext
  2. Verify you are passing the ciphertext value, not the key or another field, to decrypt
  3. Check for data written before encryption was enabled or by an older version, and migrate it (encrypt on read-and-replace)

Example fix

// before
await db.set('slack_tokens', JSON.stringify(tokens)); // plaintext, later decrypt fails
// after
await db.set('slack_tokens', encrypt(JSON.stringify(tokens), key)); // '<ALGO_PREFIX>:...'
Defensive patterns

Strategy: validation

Validate before calling

function looksEncrypted(v: string): boolean {
  return typeof v === 'string' && v.includes(':') && /^[a-z0-9_-]+:/i.test(v);
}
const stored = await storage.get('slack_config');
if (stored && !looksEncrypted(stored)) await reEncrypt(stored); // migrate plaintext before decrypt is attempted

Type guard

function isCiphertext(v: unknown): v is string {
  return typeof v === 'string' && v.indexOf(':') !== -1;
}

Try / catch

try {
  const plain = decrypt(cipher, key);
} catch (e) {
  if ((e as Error).message === 'Invalid ciphertext format') {
    // value is plaintext or legacy — re-encrypt via encrypt() and persist, or reinstall channel
  } else throw e;
}

Prevention

When it happens

Trigger: Calling decrypt (directly or via #decryptPendingInstallation, #decryptInstallation, #decryptConfigTokens) with a value that has no ':' separator — e.g. a plaintext token stored unencrypted, an empty string, or a value written by a different/older storage format.

Common situations: Switching encryption on after plaintext values were already persisted; manually inserting tokens into the DB; restoring data encrypted with a different scheme; passing the raw key or wrong field as the ciphertext argument.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/de98bb5e6c356c96. Report an issue: GitHub.