mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-02 15:09:26 +00:00
10597d213a
Add console.warn/console.error logging to 22 silent catch blocks across 10 files. Errors in cleanup, migrations, SSO, fleet snapshots, shutdown, and validation are now visible in logs. ENOENT guards added to file-system catches to distinguish missing files from permission errors. No control flow changes.
78 lines
2.9 KiB
TypeScript
78 lines
2.9 KiB
TypeScript
import crypto from 'crypto';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
const ALGORITHM = 'aes-256-gcm';
|
|
const KEY_LENGTH = 32; // 256 bits
|
|
const IV_LENGTH = 12; // NIST SP 800-38D recommended length for GCM
|
|
const ENCRYPTED_PREFIX = 'enc:';
|
|
|
|
export class CryptoService {
|
|
private static instance: CryptoService;
|
|
private key: Buffer;
|
|
|
|
private constructor() {
|
|
const dataDir = process.env.DATA_DIR || path.join(process.cwd(), 'data');
|
|
const keyPath = path.join(dataDir, 'encryption.key');
|
|
|
|
if (fs.existsSync(keyPath)) {
|
|
this.key = Buffer.from(fs.readFileSync(keyPath, 'utf-8').trim(), 'hex');
|
|
// Self-heal permissive file permissions (no-op on Windows)
|
|
try {
|
|
const mode = fs.statSync(keyPath).mode & 0o777;
|
|
if (mode !== 0o600) {
|
|
console.warn(`[CryptoService] Fixing permissive key file permissions (was 0o${mode.toString(8)}, set to 0o600)`);
|
|
fs.chmodSync(keyPath, 0o600);
|
|
}
|
|
} catch (e) {
|
|
// chmod not supported on this platform (e.g. Windows) — skip
|
|
console.warn('[CryptoService] Could not enforce key file permissions (platform may not support chmod):', (e as Error).message);
|
|
}
|
|
} else {
|
|
this.key = crypto.randomBytes(KEY_LENGTH);
|
|
if (!fs.existsSync(dataDir)) {
|
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
}
|
|
fs.writeFileSync(keyPath, this.key.toString('hex'), { mode: 0o600 });
|
|
}
|
|
}
|
|
|
|
public static getInstance(): CryptoService {
|
|
if (!CryptoService.instance) {
|
|
CryptoService.instance = new CryptoService();
|
|
}
|
|
return CryptoService.instance;
|
|
}
|
|
|
|
public encrypt(plaintext: string): string {
|
|
if (!plaintext) return plaintext;
|
|
|
|
const iv = crypto.randomBytes(IV_LENGTH);
|
|
const cipher = crypto.createCipheriv(ALGORITHM, this.key, iv);
|
|
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]);
|
|
const authTag = cipher.getAuthTag();
|
|
|
|
return `${ENCRYPTED_PREFIX}${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted.toString('hex')}`;
|
|
}
|
|
|
|
public decrypt(ciphertext: string): string {
|
|
if (!ciphertext || !this.isEncrypted(ciphertext)) return ciphertext;
|
|
|
|
const payload = ciphertext.slice(ENCRYPTED_PREFIX.length);
|
|
const [ivHex, authTagHex, encryptedHex] = payload.split(':');
|
|
|
|
const iv = Buffer.from(ivHex, 'hex');
|
|
const authTag = Buffer.from(authTagHex, 'hex');
|
|
const encrypted = Buffer.from(encryptedHex, 'hex');
|
|
|
|
const decipher = crypto.createDecipheriv(ALGORITHM, this.key, iv);
|
|
decipher.setAuthTag(authTag);
|
|
|
|
return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf-8');
|
|
}
|
|
|
|
public isEncrypted(value: string): boolean {
|
|
return value.startsWith(ENCRYPTED_PREFIX);
|
|
}
|
|
}
|