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 theView on GitHub (pinned to 75dd419e61)
Solutions
- Re-encrypt the value from its original plaintext with the current encrypt(); the ciphertext cannot be repaired once GCM auth fails.
- 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).
- Decode the envelope and check iv/ciphertext/tag round-trip through base64url intact (no padding/truncation by the storage layer).
- 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
- Never reuse a key id with different key bytes — new material always gets a new id.
- Decrypt-and-verify a canary value at service startup to catch key mismatch before serving traffic.
- Store envelopes in a content-addressed or integrity-checked storage so corruption is detected before decryption.
- When integrating with other tools, ensure they either use this library's encrypt() or that migrated values are re-encrypted on ingest.
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
- Invalid encrypted session data
- [FactorySecretEncryption] Unknown key id "${envelope.keyId}"
- Invalid encrypted session data
- Cookie password must be at least 32 characters for SSO. Set
- Cookie password must be at least 32 characters for SSO. Set
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/82f7958e73eaef89.
Report an issue: GitHub.