| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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; |
| 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; |
| } |
| } |
|
|
| 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:')) { |
| |
| update[field] = encryptWith(raw, NEW_SECRET!); |
| continue; |
| } |
|
|
| |
| const withNew = decryptWith(raw, NEW_SECRET!); |
| if (withNew !== null && !withNew.startsWith('enc:')) { |
| |
| skipped++; |
| continue; |
| } |
|
|
| |
| 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); }); |
|
|