mastra-ai/mastra · error

[FactorySecretEncryption] Unable to decrypt encrypted value.

Error message

[FactorySecretEncryption] Unable to decrypt encrypted value.

What it means

This is the final decryption failure: the key was found and AES-256-GCM was initialized, but deciphering or the trailing JSON.parse of the plaintext threw. GCM authentication means wrong key material, tampered/corrupted ciphertext, mismatched iv/tag, or non-JSON plaintext all surface as this single error. It is deliberately generic so it does not leak cryptographic details.

Source

Thrown at mastracode/factory/src/secret-encryption.ts:112

      }

      const envelope = parseEnvelope(value);
      const key = keys.get(envelope.keyId);
      if (!key) throw new Error(`[FactorySecretEncryption] Unknown key id "${envelope.keyId}".`);

      try {
        const decipher = createDecipheriv(ALGORITHM, key, Buffer.from(envelope.iv, 'base64url'));
        decipher.setAuthTag(Buffer.from(envelope.tag, 'base64url'));
        const plaintext = Buffer.concat([
          decipher.update(Buffer.from(envelope.ciphertext, 'base64url')),
          decipher.final(),
        ]).toString('utf8');
        return {
          value: JSON.parse(plaintext) as T,
          needsReencryption: envelope.keyId !== config.primary.id,
        };
      } catch {
        throw new Error('[FactorySecretEncryption] Unable to decrypt encrypted value.');
      }
    },
  };
}

/** Explicit plaintext compatibility for local, no-auth Factory development. */
export function createPlaintextFactorySecretEncryption(): FactorySecretEncryption {
  return {
    async encrypt<T>(value: T): Promise<string> {
      return JSON.stringify(value);
    },
    async decrypt<T>(value: unknown): Promise<DecryptedFactorySecret<T>> {
      if (typeof value !== 'string') return { value: structuredClone(value) as T, needsReencryption: false };
      try {
        return { value: JSON.parse(value) as T, needsReencryption: false };
      } catch {
        // Pre-encryption rows stored raw secret strings (e.g. a bare
        // `custom_providers.api_key`), not JSON. Treat the raw string as the

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Re-encrypt the value from its original plaintext with the current encrypt(); the ciphertext cannot be repaired once GCM auth fails.
  2. Verify the key bytes for envelope.keyId are the exact ones used to encrypt (regenerating a key under the same id causes silent auth failure — rotate to a new id instead).
  3. Decode the envelope and check iv/ciphertext/tag round-trip through base64url intact (no padding/truncation by the storage layer).
  4. Check whether another system encrypted the value with the same keyId but a different format; migrate those values via re-encryption.

Example fix

// before (same id reused with regenerated key bytes)
const key = randomBytes(32); // new bytes, old id 'k1' -> old values fail auth
// after (new material gets a new id; old key retained for decryption)
createFactorySecretEncryption({
  primary: { id: 'k2', key: randomBytes(32) },
  previous: [{ id: 'k1', key: oldKeyBytes }],
});
Defensive patterns

Strategy: try-catch

Validate before calling

function envelopeFieldsIntact(value: string, prefix: string): boolean {
  try {
    const env = JSON.parse(Buffer.from(value.slice(prefix.length), 'base64url').toString('utf8'));
    const fields = [env.iv, env.ciphertext, env.tag];
    return fields.every(f => typeof f === 'string' && f.length > 0 && /^[A-Za-z0-9_-]*$/.test(f));
  } catch {
    return false;
  }
}

Try / catch

try {
  return await encryption.decrypt(stored);
} catch (err) {
  if (err instanceof Error && err.message.includes('Unable to decrypt')) {
    logger.error('Secret failed GCM authentication or plaintext parse; re-encrypt from source of truth', { keyIdHint: 'decode envelope to inspect' });
    return promptUserForSecret(); // fallback
  }
  throw err;
}

Prevention

When it happens

Trigger: decrypt() with an envelope whose ciphertext/tag/iv do not authenticate under the selected key (wrong key bytes, truncated base64 fields, bit-rot), or the decrypted plaintext is not valid JSON (e.g. encrypted as raw string by another tool), or the tag was reordered/replaced.

Common situations: Key material for a given id changed (id kept but key bytes regenerated), storage layer corrupted or truncated the envelope fields, value encrypted by a different implementation with the same id but different plaintext encoding, manual tampering or partial writes.

Related errors


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