{"record":{"id":"82f7958e73eaef89","repo":"mastra-ai/mastra","slug":"factorysecretencryption-unable-to-decrypt-encryp","errorCode":null,"errorMessage":"[FactorySecretEncryption] Unable to decrypt encrypted value.","messagePattern":"\\[FactorySecretEncryption\\] Unable to decrypt encrypted value\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"mastracode/factory/src/secret-encryption.ts","lineNumber":112,"sourceCode":"      }\n\n      const envelope = parseEnvelope(value);\n      const key = keys.get(envelope.keyId);\n      if (!key) throw new Error(`[FactorySecretEncryption] Unknown key id \"${envelope.keyId}\".`);\n\n      try {\n        const decipher = createDecipheriv(ALGORITHM, key, Buffer.from(envelope.iv, 'base64url'));\n        decipher.setAuthTag(Buffer.from(envelope.tag, 'base64url'));\n        const plaintext = Buffer.concat([\n          decipher.update(Buffer.from(envelope.ciphertext, 'base64url')),\n          decipher.final(),\n        ]).toString('utf8');\n        return {\n          value: JSON.parse(plaintext) as T,\n          needsReencryption: envelope.keyId !== config.primary.id,\n        };\n      } catch {\n        throw new Error('[FactorySecretEncryption] Unable to decrypt encrypted value.');\n      }\n    },\n  };\n}\n\n/** Explicit plaintext compatibility for local, no-auth Factory development. */\nexport function createPlaintextFactorySecretEncryption(): FactorySecretEncryption {\n  return {\n    async encrypt<T>(value: T): Promise<string> {\n      return JSON.stringify(value);\n    },\n    async decrypt<T>(value: unknown): Promise<DecryptedFactorySecret<T>> {\n      if (typeof value !== 'string') return { value: structuredClone(value) as T, needsReencryption: false };\n      try {\n        return { value: JSON.parse(value) as T, needsReencryption: false };\n      } catch {\n        // Pre-encryption rows stored raw secret strings (e.g. a bare\n        // `custom_providers.api_key`), not JSON. Treat the raw string as the","sourceCodeStart":94,"sourceCodeEnd":130,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/factory/src/secret-encryption.ts#L94-L130","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before (same id reused with regenerated key bytes)\nconst key = randomBytes(32); // new bytes, old id 'k1' -> old values fail auth\n// after (new material gets a new id; old key retained for decryption)\ncreateFactorySecretEncryption({\n  primary: { id: 'k2', key: randomBytes(32) },\n  previous: [{ id: 'k1', key: oldKeyBytes }],\n});","handlingStrategy":"try-catch","validationCode":"function envelopeFieldsIntact(value: string, prefix: string): boolean {\n  try {\n    const env = JSON.parse(Buffer.from(value.slice(prefix.length), 'base64url').toString('utf8'));\n    const fields = [env.iv, env.ciphertext, env.tag];\n    return fields.every(f => typeof f === 'string' && f.length > 0 && /^[A-Za-z0-9_-]*$/.test(f));\n  } catch {\n    return false;\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  return await encryption.decrypt(stored);\n} catch (err) {\n  if (err instanceof Error && err.message.includes('Unable to decrypt')) {\n    logger.error('Secret failed GCM authentication or plaintext parse; re-encrypt from source of truth', { keyIdHint: 'decode envelope to inspect' });\n    return promptUserForSecret(); // fallback\n  }\n  throw err;\n}","preventionTips":["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."],"tags":["encryption","aes-gcm","integrity","key-management"],"backgroundTag":"decryption-failed","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}