feat: security audit fixes
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import app from './app';
|
||||
import { env, initEnv } from './config/env';
|
||||
import { refreshAllKeys, startKeyRotation, stopKeyRotation } from './services/key-rotation.service';
|
||||
import { isCryptoReady } from './services/crypto.service';
|
||||
import { isCryptoReady, decryptMnemonic, encryptMnemonic } from './services/crypto.service';
|
||||
import { db } from './config/database';
|
||||
import { logger } from './lib/logger';
|
||||
|
||||
// Global error handlers — иначе unhandled errors идут в stderr без sanitization (leak secrets)
|
||||
@@ -18,7 +19,11 @@ async function main() {
|
||||
logger.info(`Wallet service instance started with id ${logger.instanceId}`);
|
||||
|
||||
await initEnv();
|
||||
await refreshAllKeys();
|
||||
const refreshResult = await refreshAllKeys();
|
||||
if (!refreshResult.ok) {
|
||||
logger.error(`Initial Vault refresh failed: ${refreshResult.reason}. Refusing to start.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Custodial: без master-key сервис не может расшифровать ни одну мнемонику — fail fast.
|
||||
if (!isCryptoReady()) {
|
||||
@@ -26,6 +31,11 @@ async function main() {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Integrity self-test: если в БД уже есть encrypted_mnemonic, master-key должен их декриптить.
|
||||
// Иначе мы стартовали с НЕПРАВИЛЬНЫМ ключом (например после потери Vault dev-mode state) —
|
||||
// и любой /send или /sign будет тихо валиться с GCM auth error. Лучше упасть сразу.
|
||||
await runCryptoIntegritySelfTest();
|
||||
|
||||
startKeyRotation();
|
||||
|
||||
const server = app.listen(env.port, () => {
|
||||
@@ -44,6 +54,59 @@ async function main() {
|
||||
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||
}
|
||||
|
||||
async function runCryptoIntegritySelfTest(): Promise<void> {
|
||||
// H4 — Two checks:
|
||||
// (a) AES round-trip ALWAYS (даже на пустой DB): encrypt PROBE → decrypt → equal.
|
||||
// Защита от corrupted master-key buffer (wrong size, zeros, etc.)
|
||||
// (b) Если в БД есть encrypted_mnemonic — пытаемся декриптить (sample 3 rows).
|
||||
// Защита от master-key drift (Vault rotation accidental).
|
||||
|
||||
// (a) AES round-trip
|
||||
try {
|
||||
const probe = 'self-test-PROBE-string-with-some-length-for-realism';
|
||||
const ct = encryptMnemonic(probe);
|
||||
const pt = decryptMnemonic(ct);
|
||||
if (pt !== probe) {
|
||||
logger.error('Crypto self-test: AES round-trip MISMATCH — master-key buffer corrupt');
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (err: any) {
|
||||
logger.error(`Crypto self-test: AES round-trip failed: ${err?.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// (b) Existing-blob compatibility
|
||||
try {
|
||||
const rows = await db('users')
|
||||
.whereNotNull('encrypted_mnemonic')
|
||||
.select('encrypted_mnemonic')
|
||||
.limit(3);
|
||||
|
||||
if (rows.length === 0) {
|
||||
logger.info('Crypto self-test: PASSED (round-trip OK, no existing blobs to check)');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const row of rows) {
|
||||
try {
|
||||
decryptMnemonic(row.encrypted_mnemonic);
|
||||
} catch (err: any) {
|
||||
logger.error(
|
||||
'Crypto self-test: FAILED — master-key DOES NOT match stored encrypted_mnemonic. ' +
|
||||
'Likely cause: Vault state was lost (dev-mode in-memory) and a different key was seeded. ' +
|
||||
'Recovery: restore seed-cache.env from .seed-backup/ OR wipe DB with `docker compose down -v`. ' +
|
||||
`Underlying: ${err?.message}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
logger.info(`Crypto self-test: PASSED (round-trip OK + ${rows.length} existing blob(s) decryptable)`);
|
||||
} catch (err: any) {
|
||||
logger.error(`Crypto self-test: could not query DB: ${err?.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
logger.error(`Failed to start: ${err.message}`);
|
||||
process.exit(1);
|
||||
|
||||
Reference in New Issue
Block a user