Skip to content

fix(encryption): Harden encryption: AES-256-GCM for 2FA secrets, mandatory ENCRYPTION_KEY in production#212

Merged
therealbrad merged 1 commit intomainfrom
enhancement/encryption-hardening
Apr 15, 2026
Merged

fix(encryption): Harden encryption: AES-256-GCM for 2FA secrets, mandatory ENCRYPTION_KEY in production#212
therealbrad merged 1 commit intomainfrom
enhancement/encryption-hardening

Conversation

@therealbrad
Copy link
Copy Markdown
Contributor

Closes #204.

Summary

Two encryption-subsystem gaps from the security review, fixed together because they touch the same boot path:

  • 2FA TOTP secrets now use AES-256-GCM via the shared EncryptionService + ENCRYPTION_KEY. The previous XOR-with-repeating-key path (acknowledged as a placeholder in an in-file comment) was cryptographically broken against anyone with read access to the User table. The legacy v1: format is still decryptable for backward compatibility during migration; new writes are v2:.
  • Production refuses to start without ENCRYPTION_KEY. getMasterKey() throws in production when the key is unset, instead of silently falling back to the built-in default and effectively publishing every encrypted secret in the DB. Non-production keeps the convenient dev fallback with a warning.
  • Startup probe in instrumentation.ts calls assertEncryptionConfigured() on server boot so misconfiguration surfaces immediately, not on the first encrypt/decrypt mid-request.
  • Opportunistic write-back in the 2FA login path (server/auth.ts): when a user's stored secret is still v1:, re-encrypt as v2: and persist. Best-effort — a failure doesn't block login.

Pre-deploy migration (already completed in production)

Before this PR is deployed, every production tenant must have ENCRYPTION_KEY set — otherwise the startup probe crashes the container. This was done ahead of this PR:

  • All 26 active production tenants now have a unique 64-char hex ENCRYPTION_KEY in their .env.production + k8s Secret.
  • Encrypted rows that had been encrypted with the hardcoded default key (Integration.credentials, LlmIntegration.credentials, CodeRepository.credentials, UserIntegrationAuth.accessToken/refreshToken, User.twoFactorSecret) were cleared and affected integrations marked INACTIVE. Audit showed only 4 customer tenants (allego, ashwini, bhargavqa, rubenmariorossi) had any affected rows; zero 2FA enrollments and zero OAuth tokens across the fleet.
  • The orphaned bradfreetrial DB (and its role) was dropped — it had no corresponding deployment.

Test plan

  • pnpm type-check — clean
  • pnpm test — 5,593/5,593 passing (full suite)
  • pnpm vitest run lib/two-factor.test.ts utils/encryption.test.ts — covers: AES round-trip, legacy v1: decrypt backward-compat, unknown-version rejection, non-determinism of AES output, production-mode startup failure when ENCRYPTION_KEY is unset, dev-mode warn path, isLegacyEncryption helper
  • Post-deploy: look for [startup] ENCRYPTION_KEY configured ✓ in each tenant's first-boot logs
  • Post-deploy: confirm 2FA login flow works end-to-end (zero enrolled users today, but the path is still exercised on image restart)

Notes for reviewers

  • utils/encryption.ts gained assertEncryptionConfigured() as an explicit startup-probe export; getMasterKey() is still the primary entry point for encrypt/decrypt callers.
  • The legacy XOR path (decryptLegacyV1) stays in the codebase until v1: records are confirmed purged. Track removal as a follow-up.
  • Deterministic-output assertion in the old test suite was replaced with a non-determinism assertion — AES-GCM with per-call random salt/IV must never repeat ciphertext, and a determinism test would mask a regression to a weaker scheme.

🤖 Generated with Claude Code

…crets and add legacy support

- Introduced AES-256-GCM encryption for TOTP secrets, replacing the previous XOR method.
- Added versioning for encrypted secrets with prefixes `v1:` for legacy XOR and `v2:` for AES-256-GCM.
- Implemented backward compatibility to decrypt legacy `v1:` records.
- Enhanced tests to cover new encryption methods and legacy decryption paths.
- Updated the authorization flow to upgrade legacy secrets to the new format during user login.

This update improves security for two-factor authentication by using a stronger encryption method while maintaining compatibility with existing records.
@therealbrad therealbrad changed the title Harden encryption: AES-256-GCM for 2FA secrets, mandatory ENCRYPTION_KEY in production fix(encryption): Harden encryption: AES-256-GCM for 2FA secrets, mandatory ENCRYPTION_KEY in production Apr 15, 2026
@therealbrad therealbrad merged commit 8cdba1d into main Apr 15, 2026
5 checks passed
@therealbrad therealbrad deleted the enhancement/encryption-hardening branch April 15, 2026 22:54
@therealbrad
Copy link
Copy Markdown
Contributor Author

🎉 This PR is included in version 0.21.15 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Harden two encryption-subsystem gaps identified during security review

1 participant