kaboot/scripts/migrate-default-config.js

100 lines
2.6 KiB
JavaScript

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.');
}