import { PrismaClient } from "@prisma/client"; import { decryptWithKey, encryptWithKey } from "../src/utils/encryption"; const prisma = new PrismaClient(); // Reuse the dual-format decrypt/encrypt from src/utils/encryption.ts so this // script handles BOTH wire formats: the TS `hex:hex:hex` form and the // PHP-legacy single-base64 blob. The previous hand-rolled `decrypt` only parsed // the TS form and threw `Buffer.from(undefined, 'hex')` on any legacy secret — // inside the $transaction that aborted/rolled back the ENTIRE batch on the first // legacy user. These key-parameterized helpers accept the old/new keys passed on // the command line (the config-bound `decrypt`/`encrypt` use the env key only). const decrypt = (ciphertext: string, keyHex: string): string => decryptWithKey(ciphertext, keyHex); const encrypt = (plaintext: string, keyHex: string): string => encryptWithKey(plaintext, keyHex); async function main() { const oldKey = process.argv[2]; const newKey = process.argv[3]; const dryRun = process.argv.includes("--dry-run"); if (!oldKey || !newKey) { console.error( "Usage: tsx scripts/rotate-totp-key.ts [--dry-run]", ); process.exit(1); } const users = await prisma.users.findMany({ where: { totp_enabled: true, totp_secret: { not: null } }, select: { id: true, username: true, totp_secret: true }, }); console.log(`Found ${users.length} users with TOTP enabled`); if (dryRun) { for (const user of users) { try { const decrypted = decrypt(user.totp_secret!, oldKey); const reEncrypted = encrypt(decrypted, newKey); decrypt(reEncrypted, newKey); // verify round-trip console.log( ` [OK] ${user.username} (id=${user.id}) — decryption and re-encryption verified`, ); } catch (e) { console.error(` [FAIL] ${user.username} (id=${user.id}) — ${e}`); } } console.log("\nDry run complete. No changes made."); return; } await prisma.$transaction(async (tx) => { for (const user of users) { const decrypted = decrypt(user.totp_secret!, oldKey); const reEncrypted = encrypt(decrypted, newKey); await tx.users.update({ where: { id: user.id }, data: { totp_secret: reEncrypted }, }); console.log(` Re-encrypted TOTP for ${user.username} (id=${user.id})`); } }); console.log("\nAll TOTP secrets re-encrypted successfully."); await prisma.$disconnect(); } main().catch((e) => { console.error(e); process.exit(1); });