feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
const crypto = require('crypto');
|
|
|
|
|
|
|
|
|
|
const ALGORITHM = 'aes-256-gcm';
|
|
|
|
|
const IV_BYTES = 12;
|
|
|
|
|
const TAG_BYTES = 16;
|
|
|
|
|
|
2026-05-29 00:04:28 -05:00
|
|
|
// Returns a stable 256-bit key. Prefers TOKEN_ENCRYPTION_KEY env var (power-user
|
|
|
|
|
// override); otherwise auto-generates a random key on first startup and persists
|
|
|
|
|
// it in the settings table so it survives restarts without any manual config.
|
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
|
|
|
function getKey() {
|
2026-05-29 00:04:28 -05:00
|
|
|
const envRaw = process.env.TOKEN_ENCRYPTION_KEY || '';
|
|
|
|
|
if (envRaw) {
|
|
|
|
|
const buf = Buffer.from(envRaw, 'utf8');
|
|
|
|
|
if (buf.length < 32) throw new Error('TOKEN_ENCRYPTION_KEY must be at least 32 bytes');
|
|
|
|
|
return crypto.createHash('sha256').update(buf).digest();
|
|
|
|
|
}
|
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
|
|
|
|
2026-05-29 00:04:28 -05:00
|
|
|
// Lazy-require to avoid circular dependency at module load time
|
|
|
|
|
const { getSetting, setSetting } = require('../db/database');
|
|
|
|
|
let stored = getSetting('_auto_encryption_key');
|
|
|
|
|
if (!stored) {
|
|
|
|
|
stored = crypto.randomBytes(48).toString('hex');
|
|
|
|
|
setSetting('_auto_encryption_key', stored);
|
|
|
|
|
}
|
|
|
|
|
return crypto.createHash('sha256').update(stored, 'utf8').digest();
|
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
|
|
|
}
|
|
|
|
|
|
2026-05-29 00:04:28 -05:00
|
|
|
// No-op now that the key is always available — kept for call-site compatibility
|
|
|
|
|
function assertEncryptionReady() {}
|
|
|
|
|
|
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
|
|
|
function encryptSecret(plaintext) {
|
|
|
|
|
const key = getKey();
|
|
|
|
|
const iv = crypto.randomBytes(IV_BYTES);
|
|
|
|
|
const cipher = crypto.createCipheriv(ALGORITHM, key, iv, { authTagLength: TAG_BYTES });
|
|
|
|
|
const ct = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
|
|
|
|
const tag = cipher.getAuthTag();
|
|
|
|
|
return `${iv.toString('hex')}:${tag.toString('hex')}:${ct.toString('hex')}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function decryptSecret(stored) {
|
|
|
|
|
const parts = stored.split(':');
|
|
|
|
|
if (parts.length !== 3) throw new Error('Invalid encrypted secret format');
|
|
|
|
|
const [ivHex, tagHex, ctHex] = parts;
|
2026-05-29 00:04:28 -05:00
|
|
|
const key = getKey();
|
|
|
|
|
const iv = Buffer.from(ivHex, 'hex');
|
|
|
|
|
const tag = Buffer.from(tagHex, 'hex');
|
|
|
|
|
const ct = Buffer.from(ctHex, 'hex');
|
feat: SimpleFin bank sync with encrypted token storage
New services:
services/encryptionService.js — AES-256-GCM with SHA-256 derived key
services/simplefinService.js — protocol layer: claim token, fetch accounts/transactions, normalize to DB shapes
services/bankSyncService.js — orchestration: connect, sync, disconnect with encrypted access URL storage
Modified:
routes/dataSources.js — status, connect, sync, disconnect endpoints (gate on BANK_SYNC_ENABLED=true)
client/api.js — simplefinStatus, connectSimplefin, syncDataSource, deleteDataSource, dataSources
client/pages/SettingsPage.jsx — BankSyncSection with connected account info, sync/disconnect actions, setup token input
.env.example — BANK_SYNC_ENABLED, TOKEN_ENCRYPTION_KEY, SIMPLEFIN_APP_NAME
2026-05-28 21:30:20 -05:00
|
|
|
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv, { authTagLength: TAG_BYTES });
|
|
|
|
|
decipher.setAuthTag(tag);
|
|
|
|
|
return Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf8');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module.exports = { assertEncryptionReady, encryptSecret, decryptSecret };
|