dev1/backend/shared/db/withEncryption.js

159 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ────────────────────────────────────────────────────────────────
Dropin replacement for mysql2 that transparently encrypts /
decrypts selected columns, using helpers above.
---------------------------------------------------------------- */
import mysql from 'mysql2/promise';
import {
encrypt,
decrypt,
isEncrypted,
initEncryption
} from '../crypto/encryption.js';
/* ── map of columns that must be protected ─────────────────── */
const TABLE_MAP = {
user_profile : [
'username', 'firstname', 'lastname', 'email', 'phone_e164',
'zipcode', 'stripe_customer_id',
'interest_inventory_answers', 'riasec_scores',
'career_priorities', 'career_list'
],
financial_profiles : [
'current_salary','additional_income','monthly_expenses',
'monthly_debt_payments','retirement_savings','emergency_fund',
'retirement_contribution','emergency_contribution',
'extra_cash_emergency_pct','extra_cash_retirement_pct'
],
career_profiles : [
'planned_monthly_expenses','planned_monthly_debt_payments',
'planned_monthly_retirement_contribution','planned_monthly_emergency_contribution',
'planned_surplus_emergency_pct','planned_surplus_retirement_pct',
'planned_additional_income','career_goals','desired_retirement_income_monthly'
],
college_profiles : [
'selected_school','selected_program','annual_financial_aid',
'existing_college_debt','tuition','tuition_paid','loan_deferral_until_graduation',
'loan_term','interest_rate','extra_payment','expected_salary'
],
milestones : ['title','description','date','progress'],
tasks : ['title','description','due_date','status'],
reminders : ['phone_e164','message_body'],
milestone_impacts : ['amount','impact_type'],
ai_risk_analysis : ['reasoning','risk_level'],
ai_generated_ksa : ['knowledge_json','abilities_json','skills_json'],
context_cache : ['ctx_text']
};
/* ── initialise KMS unwrap once ─────────────────────────────── */
async function ensureCryptoReady () { await initEncryption(); }
/* ── mysql connection pool (uses env injected by docker) ────── */
export const pool = mysql.createPool({
host : process.env.DB_HOST,
port : process.env.DB_PORT,
user : process.env.DB_USER,
password : process.env.DB_PASSWORD,
database : process.env.DB_NAME,
waitForConnections : true,
connectionLimit : 5,
ssl : {
ca : process.env.DB_SSL_CA,
key : process.env.DB_SSL_KEY,
cert : process.env.DB_SSL_CERT
}
});
/* ── tiny helpers to parse SQL (works for *your* queries) ───── */
function extractTables (sql) {
return [...new Set(
[...sql.matchAll(/\b(?:from|join|into|update)\s+`?([a-z0-9_]+)`?/ig)]
.map(m => m[1].toLowerCase())
)];
}
function extractColumn(sql, paramIndex) {
const normalized = sql.replace(/\s+/g, ' ').toLowerCase();
// INSERT INTO table (col1, col2, ...) VALUES (?, ?, ...)
if (normalized.includes('insert into')) {
const m = normalized.match(/\(\s*([^)]+?)\s*\)\s*values/i);
if (!m || !m[1]) {
console.warn(`[DAO] INSERT column extraction failed for param ${paramIndex}`);
return null;
}
const colList = m[1].split(',').map(s => s.replace(/`/g, '').trim());
const col = colList[paramIndex] ?? null;
console.log(`[DAO] Param ${paramIndex} maps to column: ${col}`);
return col;
}
// UPDATE table SET col1 = ?, col2 = ? WHERE ...
if (normalized.includes('update')) {
const m = normalized.match(/set\s+(.*?)\s*(where|$)/);
if (!m || !m[1]) {
console.warn(`[DAO] UPDATE column extraction failed for param ${paramIndex}`);
return null;
}
const colList = m[1].split(',').map(s =>
s.split('=')[0].replace(/`/g, '').trim()
);
const col = colList[paramIndex] ?? null;
console.log(`[DAO] Param ${paramIndex} maps to column: ${col}`);
return col;
}
// SELECT ... WHERE col = ?
if (normalized.includes('where')) {
const m = normalized.match(/where\s+([a-z0-9_]+)\s*=/i);
const col = m?.[1]?.trim() ?? null;
console.log(`[DAO] Param ${paramIndex} maps to column (WHERE clause): ${col}`);
return col;
}
console.log(`[DAO] No column mapping for param ${paramIndex} — unsupported SQL`);
return null;
}
function decryptRow (row, tables) {
for (const t of tables) {
const encSet = new Set((TABLE_MAP[t] ?? []).map(c => c.toLowerCase()));
for (const k of Object.keys(row)) {
if (!encSet.has(k.toLowerCase())) continue;
const val = row[k];
if (val != null && isEncrypted(val)) row[k] = decrypt(val);
}
}
}
/* ─────────────────────────────────────────────────────────────
Replacement for pool.execute / pool.query
──────────────────────────────────────────────────────────── */
export async function exec (sql, params = []) {
await ensureCryptoReady();
const tables = extractTables(sql);
const encryptNeeded = (col) => tables.some(t => TABLE_MAP[t]?.includes(col));
const encParams = params.map((v, i) => {
const col = extractColumn(sql, i);
return (col && encryptNeeded(col) && v != null && !isEncrypted(v))
? encrypt(v)
: v;
});
const [rows, fields] = await pool.execute(sql, encParams);
if (Array.isArray(rows)) {
for (const row of rows) decryptRow(row, tables);
}
return [rows, fields];
}
/* ── mysqllike façade so existing code keeps working ───────── */
export const query = exec;
export const getConnection = () => pool.getConnection();
export const end = () => pool.end();
export const poolRaw = pool; // escape hatch