feat: security audit fixes

This commit is contained in:
ZOMBIIIIIII
2026-05-13 00:17:32 +03:00
parent e87d178d71
commit 1498ed3431
31 changed files with 2198 additions and 339 deletions

View File

@@ -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);