fix: encrypt SMTP password at rest using AES-256-GCM
The smtp_pass setting was stored as plaintext in app_settings, exposing SMTP credentials to anyone with database read access. Apply the same encrypt_api_key/decrypt_api_key pattern already used for OIDC client secrets and API keys. A new migration transparently re-encrypts any existing plaintext value on startup; decrypt_api_key handles legacy plaintext gracefully so in-flight reads remain safe during upgrade.
This commit is contained in:
@@ -456,6 +456,13 @@ function runMigrations(db: Database.Database): void {
|
|||||||
db.prepare("UPDATE app_settings SET value = ? WHERE key = 'oidc_client_secret'").run(encrypt_api_key(row.value));
|
db.prepare("UPDATE app_settings SET value = ? WHERE key = 'oidc_client_secret'").run(encrypt_api_key(row.value));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// Encrypt any plaintext smtp_pass left in app_settings
|
||||||
|
() => {
|
||||||
|
const row = db.prepare("SELECT value FROM app_settings WHERE key = 'smtp_pass'").get() as { value: string } | undefined;
|
||||||
|
if (row?.value && !row.value.startsWith('enc:v1:')) {
|
||||||
|
db.prepare("UPDATE app_settings SET value = ? WHERE key = 'smtp_pass'").run(encrypt_api_key(row.value));
|
||||||
|
}
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (currentVersion < migrations.length) {
|
if (currentVersion < migrations.length) {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { randomBytes, createHash } from 'crypto';
|
|||||||
import { revokeUserSessions } from '../mcp';
|
import { revokeUserSessions } from '../mcp';
|
||||||
import { AuthRequest, OptionalAuthRequest, User } from '../types';
|
import { AuthRequest, OptionalAuthRequest, User } from '../types';
|
||||||
import { writeAudit, getClientIp } from '../services/auditLog';
|
import { writeAudit, getClientIp } from '../services/auditLog';
|
||||||
import { decrypt_api_key, maybe_encrypt_api_key } from '../services/apiKeyCrypto';
|
import { decrypt_api_key, maybe_encrypt_api_key, encrypt_api_key } from '../services/apiKeyCrypto';
|
||||||
import { startTripReminders } from '../scheduler';
|
import { startTripReminders } from '../scheduler';
|
||||||
|
|
||||||
authenticator.options = { window: 1 };
|
authenticator.options = { window: 1 };
|
||||||
@@ -665,6 +665,7 @@ router.put('/app-settings', authenticate, (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
// Don't save masked password
|
// Don't save masked password
|
||||||
if (key === 'smtp_pass' && val === '••••••••') continue;
|
if (key === 'smtp_pass' && val === '••••••••') continue;
|
||||||
|
if (key === 'smtp_pass') val = encrypt_api_key(val);
|
||||||
db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)").run(key, val);
|
db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)").run(key, val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import nodemailer from 'nodemailer';
|
import nodemailer from 'nodemailer';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import { db } from '../db/database';
|
import { db } from '../db/database';
|
||||||
|
import { decrypt_api_key } from './apiKeyCrypto';
|
||||||
import { logInfo, logDebug, logError } from './auditLog';
|
import { logInfo, logDebug, logError } from './auditLog';
|
||||||
|
|
||||||
// ── Types ──────────────────────────────────────────────────────────────────
|
// ── Types ──────────────────────────────────────────────────────────────────
|
||||||
@@ -32,7 +33,7 @@ function getSmtpConfig(): SmtpConfig | null {
|
|||||||
const host = process.env.SMTP_HOST || getAppSetting('smtp_host');
|
const host = process.env.SMTP_HOST || getAppSetting('smtp_host');
|
||||||
const port = process.env.SMTP_PORT || getAppSetting('smtp_port');
|
const port = process.env.SMTP_PORT || getAppSetting('smtp_port');
|
||||||
const user = process.env.SMTP_USER || getAppSetting('smtp_user');
|
const user = process.env.SMTP_USER || getAppSetting('smtp_user');
|
||||||
const pass = process.env.SMTP_PASS || getAppSetting('smtp_pass');
|
const pass = process.env.SMTP_PASS || decrypt_api_key(getAppSetting('smtp_pass')) || '';
|
||||||
const from = process.env.SMTP_FROM || getAppSetting('smtp_from');
|
const from = process.env.SMTP_FROM || getAppSetting('smtp_from');
|
||||||
if (!host || !port || !from) return null;
|
if (!host || !port || !from) return null;
|
||||||
return { host, port: parseInt(port, 10), user: user || '', pass: pass || '', from, secure: parseInt(port, 10) === 465 };
|
return { host, port: parseInt(port, 10), user: user || '', pass: pass || '', from, secure: parseInt(port, 10) === 465 };
|
||||||
|
|||||||
Reference in New Issue
Block a user