import Database from 'better-sqlite3'; import { createDecipheriv, createHash, hkdfSync } from 'crypto'; import { join } from 'path'; const DB_PATH = process.env.DATABASE_PATH || join(process.cwd(), 'data', 'kaboot.db'); const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY; const ALGORITHM = 'aes-256-gcm'; const IV_LENGTH = 12; const AUTH_TAG_LENGTH = 16; const SALT_LENGTH = 16; if (!ENCRYPTION_KEY) { console.error('ENCRYPTION_KEY is required to migrate encrypted default_game_config values.'); process.exit(1); } const masterKey = createHash('sha256').update(ENCRYPTION_KEY).digest(); const isJsonString = (value) => { try { JSON.parse(value); return true; } catch { return false; } }; const decryptForUser = (ciphertext, userSub) => { if (!ciphertext) return null; try { const combined = Buffer.from(ciphertext, 'base64'); if (combined.length < SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH + 1) { return null; } const salt = combined.subarray(0, SALT_LENGTH); const iv = combined.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH); const authTag = combined.subarray(SALT_LENGTH + IV_LENGTH, SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH); const encrypted = combined.subarray(SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH); const key = Buffer.from( hkdfSync('sha256', masterKey, salt, `kaboot-user-data:${userSub}`, 32) ); const decipher = createDecipheriv(ALGORITHM, key, iv); decipher.setAuthTag(authTag); const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]); return decrypted.toString('utf8'); } catch { return null; } }; const db = new Database(DB_PATH); const rows = db.prepare(` SELECT id, default_game_config as defaultGameConfig FROM users WHERE default_game_config IS NOT NULL `).all(); const update = db.prepare('UPDATE users SET default_game_config = ? WHERE id = ?'); let migrated = 0; let alreadyPlaintext = 0; let failed = 0; const migrate = db.transaction(() => { for (const row of rows) { const value = row.defaultGameConfig; if (!value) continue; if (isJsonString(value)) { alreadyPlaintext += 1; continue; } const decrypted = decryptForUser(value, row.id); if (decrypted && isJsonString(decrypted)) { update.run(decrypted, row.id); migrated += 1; } else { failed += 1; } } }); migrate(); console.log(`Default config migration complete.`); console.log(`- migrated: ${migrated}`); console.log(`- already plaintext: ${alreadyPlaintext}`); console.log(`- failed: ${failed}`); if (failed > 0) { console.warn('Some entries could not be decrypted. Check ENCRYPTION_KEY and retry if needed.'); }