CognxSafeTrack
Claude Sonnet 4.6
refactor(worker): Sprint 2 β dead code removal and queue consolidation
37fb9ce Download scripts/db-ops/rotate_encryption_key.ts from safetrack/edtech: direct link, hf CLI and curl.
- Browser
- Download file 4.03 kB
-
https://huggingface.co/spaces/safetrack/edtech/resolve/main/scripts/db-ops/rotate_encryption_key.ts
- Command line
-
hf download hf://spaces/safetrack/edtech/scripts/db-ops/rotate_encryption_key.ts
-
curl -L -o rotate_encryption_key.ts https://huggingface.co/spaces/safetrack/edtech/resolve/main/scripts/db-ops/rotate_encryption_key.ts
4.03 kB
| /** | |
| * Key Rotation Script β decrypt with OLD key, re-encrypt with NEW key. | |
| * | |
| * Usage: | |
| * OLD_ENCRYPTION_SECRET=<old_key> NEW_ENCRYPTION_SECRET=<new_key> \ | |
| * DATABASE_URL=<prod_url> npx ts-node -e "require('./rotate_encryption_key.ts')" | |
| * | |
| * Or set vars in .env and run: | |
| * npx ts-node apps/api/scratch/rotate_encryption_key.ts | |
| * | |
| * The script is idempotent: if a field is already encrypted with NEW_KEY it is skipped. | |
| */ | |
| import 'dotenv/config'; | |
| import { PrismaClient } from '@prisma/client'; | |
| const prisma = new PrismaClient(); | |
| const OLD_SECRET = process.env.OLD_ENCRYPTION_SECRET; | |
| const NEW_SECRET = process.env.NEW_ENCRYPTION_SECRET || process.env.ENCRYPTION_SECRET; | |
| if (!OLD_SECRET || OLD_SECRET.length < 32) { | |
| console.error('β OLD_ENCRYPTION_SECRET must be set and at least 32 chars'); | |
| process.exit(1); | |
| } | |
| if (!NEW_SECRET || NEW_SECRET.length < 32) { | |
| console.error('β NEW_ENCRYPTION_SECRET (or ENCRYPTION_SECRET) must be set and at least 32 chars'); | |
| process.exit(1); | |
| } | |
| import crypto from 'crypto'; | |
| const ALGORITHM = 'aes-256-cbc'; | |
| const IV_LENGTH = 16; | |
| function encryptWith(text: string, secret: string): string { | |
| const iv = crypto.randomBytes(IV_LENGTH); | |
| const cipher = crypto.createCipheriv(ALGORITHM, Buffer.from(secret.slice(0, 32)), iv); | |
| let encrypted = cipher.update(text); | |
| encrypted = Buffer.concat([encrypted, cipher.final()]); | |
| return `enc:${iv.toString('hex')}:${encrypted.toString('hex')}`; | |
| } | |
| function decryptWith(text: string, secret: string): string | null { | |
| if (!text.startsWith('enc:')) return text; // plaintext β return as-is | |
| const [, ivHex, encryptedHex] = text.split(':'); | |
| if (!ivHex || !encryptedHex) return null; | |
| try { | |
| const iv = Buffer.from(ivHex, 'hex'); | |
| const encryptedText = Buffer.from(encryptedHex, 'hex'); | |
| const decipher = crypto.createDecipheriv(ALGORITHM, Buffer.from(secret.slice(0, 32)), iv); | |
| let decrypted = decipher.update(encryptedText); | |
| decrypted = Buffer.concat([decrypted, decipher.final()]); | |
| return decrypted.toString(); | |
| } catch { | |
| return null; // wrong key | |
| } | |
| } | |
| const FIELDS = ['systemUserToken', 'webhookSecret', 'openAiApiKey', 'googleAiApiKey'] as const; | |
| async function rotateKeys() { | |
| const orgs = await prisma.organization.findMany(); | |
| let rotated = 0; | |
| let skipped = 0; | |
| let failed = 0; | |
| for (const org of orgs) { | |
| const update: Record<string, string> = {}; | |
| for (const field of FIELDS) { | |
| const raw = (org as any)[field] as string | null; | |
| if (!raw) continue; | |
| if (!raw.startsWith('enc:')) { | |
| // Plaintext β just re-encrypt with new key | |
| update[field] = encryptWith(raw, NEW_SECRET!); | |
| continue; | |
| } | |
| // Try decrypting with new key first (idempotence check) | |
| const withNew = decryptWith(raw, NEW_SECRET!); | |
| if (withNew !== null && !withNew.startsWith('enc:')) { | |
| // Already decryptable with new key β nothing to do | |
| skipped++; | |
| continue; | |
| } | |
| // Try old key | |
| const withOld = decryptWith(raw, OLD_SECRET!); | |
| if (withOld === null) { | |
| console.warn(` β οΈ [${org.name}] ${field}: cannot decrypt with either key β skipping`); | |
| failed++; | |
| continue; | |
| } | |
| update[field] = encryptWith(withOld, NEW_SECRET!); | |
| } | |
| if (Object.keys(update).length > 0) { | |
| await prisma.organization.update({ where: { id: org.id }, data: update }); | |
| console.log(`β [${org.name}] rotated: ${Object.keys(update).join(', ')}`); | |
| rotated++; | |
| } | |
| } | |
| console.log(`\nDone β ${rotated} org(s) rotated, ${skipped} field(s) already on new key, ${failed} field(s) undecryptable.`); | |
| await prisma.$disconnect(); | |
| } | |
| rotateKeys().catch((err) => { console.error(err); process.exit(1); }); | |