Soubor jako uloziste, kdyz neni databaze
Mockup se k databazi nedostane, takze pribyl treti rezim: JSON soubor. Prezije restart procesu i containeru, ale ne redeploy - filesystem containeru je docasny. Je to mezistupen, ne nahrada databaze, a tak je to i napsane v portalu. | Rezim | Kdy | Restart | Redeploy | | -------- | --------------------------------- | ------- | -------- | | postgres | DATABASE_URL i SECRETS_KEY | prezije | prezije | | file | neni DB, ale je DATA_DIR | prezije | ne | | memory | ani jedno, nebo nejde zapsat | ne | ne | Rozhodnuti zustava na jednom miste (src/data/connectorStore.ts). Pridano: - src/data/snapshot.ts: atomicky zapis (.tmp a prejmenovani), slucovani zapisu a dokonceni rozepsaneho zapisu pri SIGTERM. Bez atomickeho zapisu by pad uprostred nechal polovicni JSON, ktery se pri startu nenacte. Rozbity soubor se prejmenuje na .broken a jede se dal - aplikace, ktera nenastartuje, je pro AppFactory nefunkcni sluzba - src/data/connectors/local.ts: jeden kod pro pamet i soubor, lisi se jen tim, kam se zapisuje. Nahrazuje memory.ts, dve implementace by se casem rozesly - klic k sifrovani se mimo databazi vygeneruje do DATA_DIR/secrets.key s pravy 0600, takze sifrovani funguje bez nastaveni. Chrani to proti nahodnemu precteni JSONu, ne proti pristupu k disku - klic lezi vedle dat a je to tak napsane i v portalu. U databaze se negeneruje vubec: kdo ma zalohu tabulky, ma i klic ze stejneho stroje - DATA_DIR v konfiguraci, data/ v .gitignore a .dockerignore - hlaska v portalu rozlisuje tri nasledky: pamet, soubor a databaze Overeno bez databaze: konektor s vyplnenymi udaji prezil restart, v JSONu jsou hodnoty sifrovane a plaintext v nem neni. Pote s databazi: rezim postgres funguje dal a klic vedle dat se nevygeneroval. Kontejner i data/ po overeni smazany. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
61cf29878f
commit
6e3d0640ff
+72
-8
@@ -12,6 +12,8 @@
|
||||
*/
|
||||
|
||||
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { config } from '../config.js';
|
||||
|
||||
/** Obalka, ktera se uklada do JSONB. */
|
||||
@@ -30,21 +32,83 @@ const ALGORITHM = 'aes-256-gcm';
|
||||
const KEY_VERSION = 1;
|
||||
|
||||
/**
|
||||
* Klic z konfigurace, srovnany na 32 bajtu.
|
||||
* Odkud je klic.
|
||||
*
|
||||
* SHA-256 z hodnoty promenne, aby fungoval jakkoliv dlouhy retezec. Neni to
|
||||
* derivace hesla (na to by patril scrypt), ale `SECRETS_KEY` ma byt nahodny
|
||||
* klic, ne heslo - a to je v dokumentaci napsane.
|
||||
* `env` ze `SECRETS_KEY`. Klic je oddeleny od dat, takze zaloha souboru
|
||||
* nebo tabulky sama nestaci. Tohle je jediny rezim vhodny do provozu.
|
||||
* `file` vygenerovany klic v datove slozce vedle dat. Chrani proti nahodnemu
|
||||
* precteni JSONu, **ne** proti nekomu, kdo ma pristup k disku - klic
|
||||
* lezi hned vedle. Je to rezim pro mockup.
|
||||
* `none` neni cim sifrovat, tajne hodnoty se neukladaji.
|
||||
*/
|
||||
export type SecretsKeySource = 'env' | 'file' | 'none';
|
||||
|
||||
let material: string | null = null;
|
||||
let source: SecretsKeySource = 'none';
|
||||
|
||||
/**
|
||||
* Nastavi klic. Vola se jednou pri startu, pred vyberem uloziste.
|
||||
*
|
||||
* `allowKeyFile` je zamerne parametr, ne automatika: v databazovem rezimu se
|
||||
* klic vedle dat generovat nesmi, tam ma prijit ze secretu.
|
||||
*/
|
||||
export function initSecrets(options: { allowKeyFile: boolean; dir: string }): SecretsKeySource {
|
||||
if (config.secretsKey !== '') {
|
||||
material = config.secretsKey;
|
||||
source = 'env';
|
||||
return source;
|
||||
}
|
||||
|
||||
if (!options.allowKeyFile) {
|
||||
material = null;
|
||||
source = 'none';
|
||||
return source;
|
||||
}
|
||||
|
||||
const file = path.join(options.dir, 'secrets.key');
|
||||
try {
|
||||
if (fs.existsSync(file)) {
|
||||
material = fs.readFileSync(file, 'utf8').trim();
|
||||
} else {
|
||||
fs.mkdirSync(options.dir, { recursive: true });
|
||||
material = randomBytes(32).toString('base64url');
|
||||
// 0600: cist smi jen vlastnik procesu. Na Windows se prava ignoruji.
|
||||
fs.writeFileSync(file, material, { encoding: 'utf8', mode: 0o600 });
|
||||
console.info(`[secrets] vygenerovan klic v ${file}`);
|
||||
}
|
||||
source = 'file';
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'[secrets] klic v souboru nelze pouzit, tajne hodnoty se nebudou ukladat:',
|
||||
err instanceof Error ? err.message : err,
|
||||
);
|
||||
material = null;
|
||||
source = 'none';
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
export function secretsKeySource(): SecretsKeySource {
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Klic srovnany na 32 bajtu.
|
||||
*
|
||||
* SHA-256 z hodnoty, aby fungoval jakkoliv dlouhy retezec. Neni to derivace
|
||||
* hesla (na to by patril scrypt), ale klic ma byt nahodny retezec, ne heslo -
|
||||
* a to je v dokumentaci napsane.
|
||||
*/
|
||||
function key(): Buffer {
|
||||
if (config.secretsKey === '') {
|
||||
throw new Error('SECRETS_KEY neni nastavena, pristupove udaje nelze sifrovat.');
|
||||
if (material === null) {
|
||||
throw new Error('Klic pro sifrovani neni nastaveny, pristupove udaje nelze sifrovat.');
|
||||
}
|
||||
return createHash('sha256').update(config.secretsKey).digest();
|
||||
return createHash('sha256').update(material).digest();
|
||||
}
|
||||
|
||||
export function canSealSecrets(): boolean {
|
||||
return config.secretsKey !== '';
|
||||
return material !== null;
|
||||
}
|
||||
|
||||
export function seal(value: string): SealedValue {
|
||||
|
||||
Reference in New Issue
Block a user