fix: encrypt OIDC client secret at rest using AES-256-GCM

The oidc_client_secret was written to app_settings as plaintext,
unlike Maps and OpenWeather API keys which are protected with
apiKeyCrypto. An attacker with read access to the SQLite file
(e.g. via a backup download) could obtain the secret and
impersonate the application with the identity provider.

- Encrypt on write in PUT /api/admin/oidc via maybe_encrypt_api_key
- Decrypt on read in GET /api/admin/oidc and in getOidcConfig()
  (oidc.ts) before passing the secret to the OIDC client library
- Add a startup migration that encrypts any existing plaintext value
  already present in the database
This commit is contained in:
jubnl
2026-04-01 04:27:50 +02:00
parent 701a8ab03a
commit bba50f038b
3 changed files with 13 additions and 3 deletions

View File

@@ -1,4 +1,5 @@
import Database from 'better-sqlite3';
import { encrypt_api_key } from '../services/apiKeyCrypto';
function runMigrations(db: Database.Database): void {
db.exec('CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL)');
@@ -448,6 +449,13 @@ function runMigrations(db: Database.Database): void {
() => {
try { db.exec('ALTER TABLE trips ADD COLUMN reminder_days INTEGER DEFAULT 3'); } catch (err: any) { if (!err.message?.includes('duplicate column name')) throw err; }
},
// Encrypt any plaintext oidc_client_secret left in app_settings
() => {
const row = db.prepare("SELECT value FROM app_settings WHERE key = 'oidc_client_secret'").get() as { value: string } | undefined;
if (row?.value && !row.value.startsWith('enc:v1:')) {
db.prepare("UPDATE app_settings SET value = ? WHERE key = 'oidc_client_secret'").run(encrypt_api_key(row.value));
}
},
];
if (currentVersion < migrations.length) {