feat(#352): Encrypt existing plaintext Account tokens
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

Implements transparent encryption/decryption of OAuth tokens via Prisma middleware with progressive migration strategy.

Core Implementation:
- Prisma middleware transparently encrypts tokens on write, decrypts on read
- Auto-detects ciphertext format: aes:iv:authTag:encrypted, vault:v1:..., or plaintext
- Uses existing CryptoService (AES-256-GCM) for encryption
- Progressive encryption: tokens encrypted as they're accessed/refreshed
- Zero-downtime migration (schema change only, no bulk data migration)

Security Features:
- Startup key validation prevents silent data loss if ENCRYPTION_KEY changes
- Secure error logging (no stack traces that could leak sensitive data)
- Graceful handling of corrupted encrypted data
- Idempotent encryption prevents double-encryption
- Future-proofed for OpenBao Transit encryption (Phase 2)

Token Fields Encrypted:
- accessToken (OAuth access tokens)
- refreshToken (OAuth refresh tokens)
- idToken (OpenID Connect ID tokens)

Backward Compatibility:
- Existing plaintext tokens readable (encryptionVersion = NULL)
- Progressive encryption on next write
- BetterAuth integration transparent (middleware layer)

Test Coverage:
- 20 comprehensive unit tests (89.06% coverage)
- Encryption/decryption scenarios
- Null/undefined handling
- Corrupted data handling
- Legacy plaintext compatibility
- Future vault format support
- All CRUD operations (create, update, updateMany, upsert)

Files Created:
- apps/api/src/prisma/account-encryption.middleware.ts
- apps/api/src/prisma/account-encryption.middleware.spec.ts
- apps/api/prisma/migrations/20260207_encrypt_account_tokens/migration.sql

Files Modified:
- apps/api/src/prisma/prisma.service.ts (register middleware)
- apps/api/src/prisma/prisma.module.ts (add CryptoService)
- apps/api/src/federation/crypto.service.ts (add key validation)
- apps/api/prisma/schema.prisma (add encryptionVersion)
- .env.example (document ENCRYPTION_KEY)

Fixes #352

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-07 13:16:43 -06:00
parent 89464583a4
commit 737eb40d18
9 changed files with 951 additions and 3 deletions

View File

@@ -1,15 +1,20 @@
import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from "@nestjs/common";
import { PrismaClient } from "@prisma/client";
import { CryptoService } from "../federation/crypto.service";
import { registerAccountEncryptionMiddleware } from "./account-encryption.middleware";
/**
* Prisma service that manages database connection lifecycle
* Extends PrismaClient to provide connection management and health checks
*
* IMPORTANT: CryptoService is required (not optional) because it will throw
* if ENCRYPTION_KEY is not configured, providing fail-fast behavior.
*/
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(PrismaService.name);
constructor() {
constructor(private readonly cryptoService: CryptoService) {
super({
log: process.env.NODE_ENV === "development" ? ["query", "info", "warn", "error"] : ["error"],
});
@@ -22,6 +27,11 @@ export class PrismaService extends PrismaClient implements OnModuleInit, OnModul
try {
await this.$connect();
this.logger.log("Database connection established");
// Register Account token encryption middleware
// CryptoService constructor will have already validated ENCRYPTION_KEY exists
registerAccountEncryptionMiddleware(this, this.cryptoService);
this.logger.log("Account encryption middleware registered");
} catch (error) {
this.logger.error("Failed to connect to database", error);
throw error;