/** * Rotates the AES-256-GCM master key used to encrypt stored mailbox * credentials. Decrypts every stored IMAP/SMTP/CalDAV password under the * old key and re-encrypts under a newly generated one, in a single * transaction, then prints the new key so it can be written into * /etc/ai-chief-of-staff/secrets.env. * * Usage: OLD_KEY= npx tsx scripts/rotate-credential-key.ts * (reads the current key from CREDENTIALS_ENCRYPTION_KEY if OLD_KEY is unset) */ import crypto from "node:crypto"; import Database from "better-sqlite3"; import path from "node:path"; import { rotateCredential } from "../src/lib/crypto/credentials"; const dataDir = process.env.DATA_DIR ?? path.join(process.cwd(), "data"); const oldKeyB64 = process.env.OLD_KEY ?? process.env.CREDENTIALS_ENCRYPTION_KEY; if (!oldKeyB64) { console.error("Set OLD_KEY (or CREDENTIALS_ENCRYPTION_KEY) to the current base64-encoded key before running."); process.exit(1); } const oldKey = Buffer.from(oldKeyB64, "base64"); if (oldKey.length !== 32) { console.error("OLD_KEY must decode to exactly 32 bytes."); process.exit(1); } const newKey = crypto.randomBytes(32); const sqlite = new Database(path.join(dataDir, "db.sqlite")); const rows = sqlite .prepare( "SELECT id, imap_password_enc, smtp_password_enc, caldav_password_enc FROM mailbox_credentials", ) .all() as Array<{ id: string; imap_password_enc: string | null; smtp_password_enc: string | null; caldav_password_enc: string | null; }>; const update = sqlite.prepare( "UPDATE mailbox_credentials SET imap_password_enc = ?, smtp_password_enc = ?, caldav_password_enc = ? WHERE id = ?", ); const rotateAll = sqlite.transaction(() => { for (const row of rows) { const imap = row.imap_password_enc ? rotateCredential(row.imap_password_enc, oldKey, newKey) : null; const smtp = row.smtp_password_enc ? rotateCredential(row.smtp_password_enc, oldKey, newKey) : null; const caldav = row.caldav_password_enc ? rotateCredential(row.caldav_password_enc, oldKey, newKey) : null; update.run(imap, smtp, caldav, row.id); } }); rotateAll(); console.log(`Rotated ${rows.length} mailbox_credentials row(s).`); console.log("\nNew key (update CREDENTIALS_ENCRYPTION_KEY in /etc/ai-chief-of-staff/secrets.env, then restart the service):\n"); console.log(newKey.toString("base64"));