titan-vault-node 1.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -41
- package/dist/core/classify.d.ts +9 -0
- package/dist/core/classify.js +26 -0
- package/dist/core/crypto-store.d.ts +18 -0
- package/dist/core/crypto-store.js +67 -0
- package/dist/core/database-hooks.d.ts +17 -0
- package/dist/core/database-hooks.js +51 -0
- package/dist/core/hardening.d.ts +2 -0
- package/dist/core/hardening.js +70 -0
- package/dist/core/poison.d.ts +5 -0
- package/dist/core/poison.js +75 -0
- package/dist/core/transit-client.d.ts +13 -0
- package/dist/core/transit-client.js +103 -0
- package/dist/index.d.ts +45 -7
- package/dist/index.js +272 -64
- package/dist/types.d.ts +67 -0
- package/dist/types.js +2 -0
- package/package.json +8 -3
- package/src/index.ts +0 -101
- package/tsconfig.json +0 -14
package/README.md
CHANGED
|
@@ -1,66 +1,168 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Titan Vault (Node.js SDK)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Cliente de la bóveda local. Pide `GET /secret/data/{path}` con una llave RSA-2048 efímera, descifra la respuesta híbrida (RSA-OAEP-SHA256 + AES-256-CFB) y reparte cada campo:
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- `JWT_SECRET` y el resto de llaves quedan como `crypto.KeyObject`. El SDK no guarda otra copia en string. `getRaw()` sí crea un string, y el JSON descifrado vive en el heap hasta que V8 lo recolecta.
|
|
6
|
+
- `DB_PASSWORD` y las demás contraseñas de base de datos se entregan solo cuando Sequelize o `pg` piden la conexión.
|
|
7
|
+
- `DATABASE_URL`, `REDIS_URL`, `PORT` y el resto se leen con `TitanVault.config.get`. No entran en `process.env`, salvo `NODE_ENV`, `PORT`, `HOST`, `FRONTEND_PORT` y `BACKEND_PORT`.
|
|
8
|
+
- `VAULT_EMERGENCY_RECOVERY_TOKEN` no se puede leer. Si el payload trae el cebo, esa lectura llama a `GET /sys/recovery` con el valor que inyectó la bóveda.
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
If a hacker gets read access to your server (LFI, directory traversal, etc.), they can read your `.env` file and steal all your database passwords and API keys.
|
|
10
|
+
Si la bóveda no responde, o devuelve el secreto en claro después de recibir `x-vault-public-key`, `armParanoiaMode` lanza `VaultConnectionError` y el proceso no debe seguir. El token se lee de `vaultToken`, `VAULT_TOKEN` o `VAULT_APP_TOKEN`.
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## ⚡ ¿Por qué `dotenv` y `process.env` son inseguros?
|
|
15
|
+
|
|
16
|
+
1. **Memory Dumps:** Cualquier vulnerabilidad que permita un volcado de memoria (heapdump, coredump, inspector v8) expone todas las variables de entorno en texto plano.
|
|
17
|
+
2. **Inmutabilidad V8:** En Node.js, `process.env` almacena strings inmutables. El recolector de basura (GC) no puede sobreescribir con ceros esas cadenas en memoria.
|
|
18
|
+
3. **Peligro de Logs:** Un simple `console.log(process.env)` o reporte de error APM filtra todas tus credenciales.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Qué hace el modo paranoia
|
|
12
23
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
1. **Arranque.** `bootstrap` espera a `armParanoiaMode`. Si esa llamada lanza, no importa el servidor.
|
|
25
|
+
2. **`process.env`.** Un proxy lanza `ParanoiaBlockedError` al leer o escribir un secreto, y lo oculta de `Object.keys` y `console.log(process.env)`.
|
|
26
|
+
3. **`KeyObject`.** `TitanVault.secrets.get('JWT_SECRET')` sirve para `jsonwebtoken` y `crypto`. La búsqueda de strings en el heap de V8 todavía puede encontrar el valor que produjo `JSON.parse` hasta que el recolector lo suelte.
|
|
27
|
+
4. **Base de datos.** `hookSequelize` pone la contraseña en `beforeConnect` y la quita del config en `afterConnect`. `getPgPasswordProvider` la devuelve en cada conexión. El driver conserva el string que necesita el handshake.
|
|
28
|
+
5. **Transit.** `encrypt`, `decrypt`, `sign` y `verify` llaman a `/transit/*` en la bóveda. La llave no sale del servidor.
|
|
18
29
|
|
|
19
|
-
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 💻 Guía de Integración
|
|
33
|
+
|
|
34
|
+
### 1. El Patrón Bootstrap (`bootstrap.js` o `src/index.ts`)
|
|
35
|
+
|
|
36
|
+
Crea un archivo de arranque que cargue el SDK antes de importar el servidor:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// bootstrap.ts
|
|
40
|
+
import { TitanVault } from 'titan-vault-node';
|
|
41
|
+
|
|
42
|
+
async function main() {
|
|
43
|
+
// Lanza VaultConnectionError si la bóveda no responde.
|
|
44
|
+
await TitanVault.armParanoiaMode({
|
|
45
|
+
vaultUrl: process.env.VAULT_URL || 'http://127.0.0.1:8200',
|
|
46
|
+
vaultToken: process.env.VAULT_APP_TOKEN,
|
|
47
|
+
path: 'mi_aplicacion_env',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// 2. Importa el servidor dinámicamente UNA VEZ que la memoria está blindada
|
|
51
|
+
await import('./server.js');
|
|
52
|
+
}
|
|
20
53
|
|
|
21
|
-
|
|
22
|
-
npm install titan-vault-node
|
|
54
|
+
main();
|
|
23
55
|
```
|
|
24
56
|
|
|
25
|
-
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
### 2. Firma y Validación de Tokens JWT (C++ `KeyObject`)
|
|
60
|
+
|
|
61
|
+
Compatible de forma nativa con `jsonwebtoken`, `jose` y el módulo `crypto` de Node.js:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import jwt from 'jsonwebtoken';
|
|
65
|
+
import { TitanVault } from 'titan-vault-node';
|
|
26
66
|
|
|
27
|
-
|
|
67
|
+
const jwtKey = TitanVault.secrets.get('JWT_SECRET');
|
|
28
68
|
|
|
29
|
-
|
|
30
|
-
|
|
69
|
+
// Firmar
|
|
70
|
+
const token = jwt.sign({ userId: 123, role: 'admin' }, jwtKey, {
|
|
71
|
+
algorithm: 'HS256',
|
|
72
|
+
expiresIn: '2h',
|
|
73
|
+
});
|
|
31
74
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
VAULT_APP_TOKEN=my-super-secret-app-role-token
|
|
75
|
+
// Verificar
|
|
76
|
+
const payload = jwt.verify(token, jwtKey);
|
|
35
77
|
```
|
|
36
78
|
|
|
37
|
-
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
### 3. Conexión Segura a Base de Datos (JIT)
|
|
38
82
|
|
|
83
|
+
#### Con Sequelize:
|
|
39
84
|
```typescript
|
|
40
|
-
import {
|
|
85
|
+
import { Sequelize } from 'sequelize';
|
|
86
|
+
import { TitanVault } from 'titan-vault-node';
|
|
87
|
+
|
|
88
|
+
export const sequelize = new Sequelize({
|
|
89
|
+
dialect: 'postgres',
|
|
90
|
+
host: TitanVault.config.get('DB_HOST'),
|
|
91
|
+
database: TitanVault.config.get('DB_NAME'),
|
|
92
|
+
username: TitanVault.config.get('DB_USER'),
|
|
93
|
+
// No pongas password aquí
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Registra el inyector JIT
|
|
97
|
+
TitanVault.databases.hookSequelize(sequelize, {
|
|
98
|
+
credentialKey: 'DB_PASSWORD',
|
|
99
|
+
});
|
|
100
|
+
```
|
|
41
101
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
102
|
+
#### Con `pg` (node-postgres):
|
|
103
|
+
```typescript
|
|
104
|
+
import pg from 'pg';
|
|
105
|
+
import { TitanVault } from 'titan-vault-node';
|
|
106
|
+
|
|
107
|
+
export const pool = new pg.Pool({
|
|
108
|
+
host: TitanVault.config.get('DB_HOST'),
|
|
109
|
+
user: TitanVault.config.get('DB_USER'),
|
|
110
|
+
database: TitanVault.config.get('DB_NAME'),
|
|
111
|
+
// Inyección JIT asíncrona por conexión
|
|
112
|
+
password: TitanVault.databases.getPgPasswordProvider('DB_PASSWORD'),
|
|
113
|
+
});
|
|
114
|
+
```
|
|
46
115
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
### 4. Criptografía como Servicio (Transit Engine / CaaS)
|
|
119
|
+
|
|
120
|
+
Para máxima seguridad, puedes delegar el cifrado y la firma directamente a Titan Vault (cero llaves en tu app):
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { TitanVault } from 'titan-vault-node';
|
|
124
|
+
|
|
125
|
+
// Cifrar datos con AES-256-GCM en el Vault
|
|
126
|
+
const ciphertext = await TitanVault.transit.encrypt('mi-llave', 'Texto Ultra Confidencial');
|
|
51
127
|
|
|
52
|
-
|
|
128
|
+
// Descifrar en el Vault
|
|
129
|
+
const plaintext = await TitanVault.transit.decrypt('mi-llave', ciphertext);
|
|
130
|
+
|
|
131
|
+
// Firmar datos con HMAC-SHA256 en el Vault
|
|
132
|
+
const signature = await TitanVault.transit.sign('mi-firma-jwt', 'payload.a.firmar');
|
|
133
|
+
|
|
134
|
+
// Verificar firma en el Vault
|
|
135
|
+
const isValid = await TitanVault.transit.verify('mi-firma-jwt', 'payload.a.firmar', signature);
|
|
53
136
|
```
|
|
54
137
|
|
|
55
|
-
|
|
56
|
-
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
### 5. Configuración
|
|
141
|
+
|
|
142
|
+
`DATABASE_URL` y `REDIS_URL` salen de aquí. No son contraseñas y no se copian a `process.env`.
|
|
57
143
|
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
144
|
+
```typescript
|
|
145
|
+
const databaseUrl = TitanVault.config.get('DATABASE_URL');
|
|
146
|
+
const port = TitanVault.config.get('PORT', '3000');
|
|
147
|
+
const host = TitanVault.config.get('HOST', '0.0.0.0');
|
|
62
148
|
```
|
|
63
|
-
If your Honeytoken is triggered, the Vault seals itself and sends a red alert to your phone instantly.
|
|
64
149
|
|
|
65
150
|
---
|
|
66
|
-
|
|
151
|
+
|
|
152
|
+
### 6. Reporte de runtime
|
|
153
|
+
|
|
154
|
+
`verifyRuntimeHardening()` dice si el proceso es root, si `NODE_ENV` es `production`, si el límite blando de core dumps es 0 (`/proc/self/limits`) y si el modo paranoia llegó a armarse. No cambia esos límites.
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
TitanVault.verifyRuntimeHardening();
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Honeytoken
|
|
163
|
+
|
|
164
|
+
En la bóveda, la trampa está apagada hasta definir `TITAN_INJECT_HONEYTOKEN=1` y `TITAN_HONEYTOKEN_VALUE`. El campo por defecto es `VAULT_EMERGENCY_RECOVERY_TOKEN` (`TITAN_HONEYTOKEN_FIELD` lo cambia en los dos lados).
|
|
165
|
+
|
|
166
|
+
Leer `process.env.VAULT_EMERGENCY_RECOVERY_TOKEN` lanza `ParanoiaBlockedError`. Si el payload traía el cebo, el SDK llama a `GET /sys/recovery` con ese valor en `x-vault-token`. La bóveda sella cuando el header coincide. Sin cebo en el payload, la lectura se bloquea y la bóveda sigue abierta.
|
|
167
|
+
|
|
168
|
+
`TitanVault.flushTripwire()` espera a que esa petición termine.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { VaultOptions } from '../types';
|
|
2
|
+
/** Same default field the vault injects (`TITAN_HONEYTOKEN_FIELD`). */
|
|
3
|
+
export declare const HONEYTOKEN_FIELD = "VAULT_EMERGENCY_RECOVERY_TOKEN";
|
|
4
|
+
export type SecretClass = 'honeytoken' | 'crypto' | 'database' | 'config';
|
|
5
|
+
/**
|
|
6
|
+
* Classification shared with the vault.
|
|
7
|
+
* `DATABASE_URL` is configuration the app reads. Only password fields go to the JIT store.
|
|
8
|
+
*/
|
|
9
|
+
export declare function classifyKey(key: string, tag: string | undefined, options?: VaultOptions): SecretClass;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HONEYTOKEN_FIELD = void 0;
|
|
4
|
+
exports.classifyKey = classifyKey;
|
|
5
|
+
/** Same default field the vault injects (`TITAN_HONEYTOKEN_FIELD`). */
|
|
6
|
+
exports.HONEYTOKEN_FIELD = 'VAULT_EMERGENCY_RECOVERY_TOKEN';
|
|
7
|
+
// Keep in sync with titan_vault/core.py `_classify_key`.
|
|
8
|
+
const DB_PASSWORD_RE = /(DB_PASS|POSTGRES_PASS|MYSQL_PASS|DB_PASSWORD|POSTGRES_PASSWORD|MYSQL_PASSWORD)/i;
|
|
9
|
+
const CRYPTO_RE = /(JWT|SECRET|KEY|PRIVATE|TOKEN|SALT)/i;
|
|
10
|
+
/**
|
|
11
|
+
* Classification shared with the vault.
|
|
12
|
+
* `DATABASE_URL` is configuration the app reads. Only password fields go to the JIT store.
|
|
13
|
+
*/
|
|
14
|
+
function classifyKey(key, tag, options = {}) {
|
|
15
|
+
const honeyField = options.honeytokenField || exports.HONEYTOKEN_FIELD;
|
|
16
|
+
if (tag === 'honeytoken' || key === honeyField)
|
|
17
|
+
return 'honeytoken';
|
|
18
|
+
if ((options.cryptoKeys || []).includes(key) || tag === 'crypto')
|
|
19
|
+
return 'crypto';
|
|
20
|
+
if ((options.dbKeys || []).includes(key) || tag === 'database' || DB_PASSWORD_RE.test(key)) {
|
|
21
|
+
return 'database';
|
|
22
|
+
}
|
|
23
|
+
if (CRYPTO_RE.test(key))
|
|
24
|
+
return 'crypto';
|
|
25
|
+
return 'config';
|
|
26
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
export declare class CryptoStore {
|
|
3
|
+
private keyObjects;
|
|
4
|
+
/**
|
|
5
|
+
* Store the secret as a KeyObject and drop the temporary byte buffer.
|
|
6
|
+
* The original JavaScript string from JSON.parse is immutable and stays
|
|
7
|
+
* until V8 collects it. This store does not keep a second copy.
|
|
8
|
+
*/
|
|
9
|
+
register(name: string, rawSecret: string): void;
|
|
10
|
+
get(name: string): crypto.KeyObject;
|
|
11
|
+
/**
|
|
12
|
+
* Materialize the secret as a string. That string lives on the V8 heap
|
|
13
|
+
* until it is collected. Prefer `get()` when the library accepts a KeyObject.
|
|
14
|
+
*/
|
|
15
|
+
getRaw(name: string): string;
|
|
16
|
+
has(name: string): boolean;
|
|
17
|
+
keys(): string[];
|
|
18
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.CryptoStore = void 0;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
class CryptoStore {
|
|
9
|
+
keyObjects = new Map();
|
|
10
|
+
/**
|
|
11
|
+
* Store the secret as a KeyObject and drop the temporary byte buffer.
|
|
12
|
+
* The original JavaScript string from JSON.parse is immutable and stays
|
|
13
|
+
* until V8 collects it. This store does not keep a second copy.
|
|
14
|
+
*/
|
|
15
|
+
register(name, rawSecret) {
|
|
16
|
+
if (!rawSecret)
|
|
17
|
+
return;
|
|
18
|
+
const buffer = Buffer.from(rawSecret, 'utf-8');
|
|
19
|
+
try {
|
|
20
|
+
if (rawSecret.includes('-----BEGIN PRIVATE KEY-----') || rawSecret.includes('-----BEGIN RSA PRIVATE KEY-----')) {
|
|
21
|
+
this.keyObjects.set(name, crypto_1.default.createPrivateKey({ key: rawSecret, format: 'pem' }));
|
|
22
|
+
}
|
|
23
|
+
else if (rawSecret.includes('-----BEGIN PUBLIC KEY-----')) {
|
|
24
|
+
this.keyObjects.set(name, crypto_1.default.createPublicKey({ key: rawSecret, format: 'pem' }));
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
this.keyObjects.set(name, crypto_1.default.createSecretKey(buffer));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
buffer.fill(0);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
get(name) {
|
|
35
|
+
const key = this.keyObjects.get(name);
|
|
36
|
+
if (!key) {
|
|
37
|
+
throw new Error(`[Titan Vault] Llave criptográfica '${name}' no encontrada en el almacén seguro.`);
|
|
38
|
+
}
|
|
39
|
+
return key;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Materialize the secret as a string. That string lives on the V8 heap
|
|
43
|
+
* until it is collected. Prefer `get()` when the library accepts a KeyObject.
|
|
44
|
+
*/
|
|
45
|
+
getRaw(name) {
|
|
46
|
+
const key = this.get(name);
|
|
47
|
+
console.warn(`⚠️ [Titan Vault] '${name}' se materializó como texto. ` +
|
|
48
|
+
`Ese string queda en el heap de V8 hasta que el recolector lo libere. ` +
|
|
49
|
+
`Usa get() cuando la librería acepte un KeyObject.`);
|
|
50
|
+
if (key.type === 'secret') {
|
|
51
|
+
const exported = key.export();
|
|
52
|
+
return Buffer.isBuffer(exported) ? exported.toString('utf8') : Buffer.from(exported).toString('utf8');
|
|
53
|
+
}
|
|
54
|
+
const exported = key.export({
|
|
55
|
+
type: key.type === 'private' ? 'pkcs8' : 'spki',
|
|
56
|
+
format: 'pem',
|
|
57
|
+
});
|
|
58
|
+
return typeof exported === 'string' ? exported : exported.toString('utf8');
|
|
59
|
+
}
|
|
60
|
+
has(name) {
|
|
61
|
+
return this.keyObjects.has(name);
|
|
62
|
+
}
|
|
63
|
+
keys() {
|
|
64
|
+
return Array.from(this.keyObjects.keys());
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
exports.CryptoStore = CryptoStore;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare class DatabaseHooks {
|
|
2
|
+
private credentials;
|
|
3
|
+
register(key: string, value: string): void;
|
|
4
|
+
getJitCredential(key: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Inject the password on `beforeConnect` and clear `config.password` after.
|
|
7
|
+
* The driver still receives a JavaScript string for the handshake.
|
|
8
|
+
*/
|
|
9
|
+
hookSequelize(sequelize: any, options?: {
|
|
10
|
+
credentialKey?: string;
|
|
11
|
+
}): void;
|
|
12
|
+
/**
|
|
13
|
+
* Password function for `pg.Pool`. node-postgres calls it per connection.
|
|
14
|
+
*/
|
|
15
|
+
getPgPasswordProvider(keyName?: string): () => Promise<string>;
|
|
16
|
+
private materialize;
|
|
17
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DatabaseHooks = void 0;
|
|
4
|
+
class DatabaseHooks {
|
|
5
|
+
credentials = new Map();
|
|
6
|
+
register(key, value) {
|
|
7
|
+
if (!key || !value)
|
|
8
|
+
return;
|
|
9
|
+
const existing = this.credentials.get(key);
|
|
10
|
+
if (existing)
|
|
11
|
+
existing.fill(0);
|
|
12
|
+
this.credentials.set(key, Buffer.from(value, 'utf8'));
|
|
13
|
+
}
|
|
14
|
+
getJitCredential(key) {
|
|
15
|
+
return this.materialize(key);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Inject the password on `beforeConnect` and clear `config.password` after.
|
|
19
|
+
* The driver still receives a JavaScript string for the handshake.
|
|
20
|
+
*/
|
|
21
|
+
hookSequelize(sequelize, options = {}) {
|
|
22
|
+
const credKey = options.credentialKey || 'DB_PASSWORD';
|
|
23
|
+
if (!sequelize || typeof sequelize.addHook !== 'function') {
|
|
24
|
+
console.warn('⚠️ [Titan Vault] La instancia provista no parece ser un objeto Sequelize válido.');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
sequelize.addHook('beforeConnect', async (config) => {
|
|
28
|
+
config.password = this.materialize(credKey);
|
|
29
|
+
});
|
|
30
|
+
sequelize.addHook('afterConnect', async (_connection, config) => {
|
|
31
|
+
if (config && 'password' in config) {
|
|
32
|
+
config.password = null;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
console.log(`🛡️ [Titan Vault] Hook Sequelize JIT activado para '${credKey}'.`);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Password function for `pg.Pool`. node-postgres calls it per connection.
|
|
39
|
+
*/
|
|
40
|
+
getPgPasswordProvider(keyName = 'DB_PASSWORD') {
|
|
41
|
+
return async () => this.materialize(keyName);
|
|
42
|
+
}
|
|
43
|
+
materialize(key) {
|
|
44
|
+
const buf = this.credentials.get(key);
|
|
45
|
+
if (!buf) {
|
|
46
|
+
throw new Error(`[Titan Vault - JIT DB] Credencial '${key}' no encontrada en el almacén JIT.`);
|
|
47
|
+
}
|
|
48
|
+
return buf.toString('utf8');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.DatabaseHooks = DatabaseHooks;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.verifyRuntimeHardening = verifyRuntimeHardening;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
function readCoreDumpSoftLimit() {
|
|
9
|
+
try {
|
|
10
|
+
const text = fs_1.default.readFileSync('/proc/self/limits', 'utf8');
|
|
11
|
+
const line = text.split('\n').find((entry) => entry.toLowerCase().startsWith('max core file size'));
|
|
12
|
+
if (!line)
|
|
13
|
+
return null;
|
|
14
|
+
const cols = line.split(/\s{2,}/).map((part) => part.trim());
|
|
15
|
+
if (!cols[1])
|
|
16
|
+
return null;
|
|
17
|
+
return cols[1] === '0';
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function verifyRuntimeHardening(paranoiaArmed) {
|
|
24
|
+
const recommendations = [];
|
|
25
|
+
const nodeEnv = process.env.NODE_ENV || 'development';
|
|
26
|
+
let runningAsRoot = false;
|
|
27
|
+
try {
|
|
28
|
+
if (typeof process.getuid === 'function') {
|
|
29
|
+
runningAsRoot = process.getuid() === 0;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Windows or a restricted environment.
|
|
34
|
+
}
|
|
35
|
+
const coreDumpsDisabled = readCoreDumpSoftLimit();
|
|
36
|
+
if (runningAsRoot) {
|
|
37
|
+
recommendations.push('El proceso corre como root (UID 0). En producción usa un usuario sin privilegios.');
|
|
38
|
+
}
|
|
39
|
+
if (nodeEnv !== 'production') {
|
|
40
|
+
recommendations.push('NODE_ENV no es "production".');
|
|
41
|
+
}
|
|
42
|
+
if (coreDumpsDisabled === null) {
|
|
43
|
+
recommendations.push('No se pudo leer el límite de core dumps. En Linux revisa /proc/self/limits (Max core file size).');
|
|
44
|
+
}
|
|
45
|
+
else if (!coreDumpsDisabled) {
|
|
46
|
+
recommendations.push('Los core dumps están permitidos. Un ulimit -c 0 evita que un volcado escriba la memoria a disco.');
|
|
47
|
+
}
|
|
48
|
+
if (!paranoiaArmed) {
|
|
49
|
+
recommendations.push('El modo paranoia no está armado. Los secretos no pasaron por el proxy de process.env.');
|
|
50
|
+
}
|
|
51
|
+
const isHardened = !runningAsRoot && nodeEnv === 'production' && coreDumpsDisabled === true && paranoiaArmed;
|
|
52
|
+
console.log('\n🛡️ [Titan Vault] Reporte de hardening:');
|
|
53
|
+
console.log(` - Entorno: ${nodeEnv}`);
|
|
54
|
+
console.log(` - Usuario root: ${runningAsRoot ? 'sí' : 'no'}`);
|
|
55
|
+
console.log(` - Core dumps desactivados: ${coreDumpsDisabled === null ? 'desconocido' : coreDumpsDisabled ? 'sí' : 'no'}`);
|
|
56
|
+
console.log(` - Modo paranoia armado: ${paranoiaArmed ? 'sí' : 'no'}`);
|
|
57
|
+
for (const rec of recommendations) {
|
|
58
|
+
console.log(` ${rec}`);
|
|
59
|
+
}
|
|
60
|
+
console.log('');
|
|
61
|
+
return {
|
|
62
|
+
isHardened,
|
|
63
|
+
nodeEnv,
|
|
64
|
+
coreDumpsDisabled: coreDumpsDisabled === true,
|
|
65
|
+
coreDumpLimitKnown: coreDumpsDisabled !== null,
|
|
66
|
+
runningAsRoot,
|
|
67
|
+
paranoiaArmed,
|
|
68
|
+
recommendations,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare class ParanoiaBlockedError extends Error {
|
|
2
|
+
readonly key: string;
|
|
3
|
+
constructor(key: string, reason?: string);
|
|
4
|
+
}
|
|
5
|
+
export declare function poisonProcessEnv(secretKeys: string[], honeytokenKey?: string, onHoneytokenDetonated?: (key: string) => void): void;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ParanoiaBlockedError = void 0;
|
|
4
|
+
exports.poisonProcessEnv = poisonProcessEnv;
|
|
5
|
+
class ParanoiaBlockedError extends Error {
|
|
6
|
+
key;
|
|
7
|
+
constructor(key, reason) {
|
|
8
|
+
super(`🚨 [Titan Vault - Modo Paranoia] Acceso bloqueado a 'process.env.${key}'. ` +
|
|
9
|
+
(reason ||
|
|
10
|
+
`Esta variable fue clasificada como secreto crítico y ha sido envenenada para prevenir Memory Dumps / Heap Dumps. ` +
|
|
11
|
+
`Usa 'TitanVault.secrets.get("${key}")' (KeyObject) o hooks JIT de Base de Datos.`));
|
|
12
|
+
this.name = 'ParanoiaBlockedError';
|
|
13
|
+
this.key = key;
|
|
14
|
+
Object.setPrototypeOf(this, ParanoiaBlockedError.prototype);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.ParanoiaBlockedError = ParanoiaBlockedError;
|
|
18
|
+
function poisonProcessEnv(secretKeys, honeytokenKey, onHoneytokenDetonated) {
|
|
19
|
+
const poisonedSet = new Set(secretKeys);
|
|
20
|
+
const honeyKey = honeytokenKey || 'VAULT_EMERGENCY_RECOVERY_TOKEN';
|
|
21
|
+
// 1. Delete existing raw values from underlying process.env
|
|
22
|
+
for (const key of secretKeys) {
|
|
23
|
+
try {
|
|
24
|
+
delete process.env[key];
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// ignore
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// 2. Wrap process.env in a Proxy
|
|
31
|
+
const targetEnv = process.env;
|
|
32
|
+
const envProxy = new Proxy(targetEnv, {
|
|
33
|
+
get(target, prop, receiver) {
|
|
34
|
+
if (typeof prop === 'string') {
|
|
35
|
+
if (prop === honeyKey) {
|
|
36
|
+
if (onHoneytokenDetonated) {
|
|
37
|
+
try {
|
|
38
|
+
onHoneytokenDetonated(prop);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// ignore
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
throw new ParanoiaBlockedError(prop, `HONEYTOKEN DETONADO. Intento de acceso a '${prop}' detectado. Alerta de intrusión emitida.`);
|
|
45
|
+
}
|
|
46
|
+
if (poisonedSet.has(prop)) {
|
|
47
|
+
throw new ParanoiaBlockedError(prop);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return Reflect.get(target, prop, receiver);
|
|
51
|
+
},
|
|
52
|
+
set(target, prop, value, receiver) {
|
|
53
|
+
if (typeof prop === 'string' && (prop === honeyKey || poisonedSet.has(prop))) {
|
|
54
|
+
throw new ParanoiaBlockedError(prop, `No se permite mutar 'process.env.${prop}' en Modo Paranoia.`);
|
|
55
|
+
}
|
|
56
|
+
return Reflect.set(target, prop, value, receiver);
|
|
57
|
+
},
|
|
58
|
+
has(target, prop) {
|
|
59
|
+
if (typeof prop === 'string' && (prop === honeyKey || poisonedSet.has(prop))) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return Reflect.has(target, prop);
|
|
63
|
+
},
|
|
64
|
+
ownKeys(target) {
|
|
65
|
+
return Reflect.ownKeys(target).filter((k) => typeof k !== 'string' || (k !== honeyKey && !poisonedSet.has(k)));
|
|
66
|
+
},
|
|
67
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
68
|
+
if (typeof prop === 'string' && (prop === honeyKey || poisonedSet.has(prop))) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
return Reflect.getOwnPropertyDescriptor(target, prop);
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
process.env = envProxy;
|
|
75
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { TransitKeyInfo } from '../types';
|
|
2
|
+
export declare class TransitClient {
|
|
3
|
+
private getBaseUrl;
|
|
4
|
+
private getToken;
|
|
5
|
+
constructor(getBaseUrl: () => string, getToken: () => string | undefined);
|
|
6
|
+
private getHeaders;
|
|
7
|
+
createKey(name: string, keyType?: 'aes-gcm' | 'hmac-sha256'): Promise<TransitKeyInfo>;
|
|
8
|
+
listKeys(): Promise<TransitKeyInfo[]>;
|
|
9
|
+
encrypt(name: string, plaintext: string): Promise<string>;
|
|
10
|
+
decrypt(name: string, ciphertext: string): Promise<string>;
|
|
11
|
+
sign(name: string, data: string): Promise<string>;
|
|
12
|
+
verify(name: string, data: string, signature: string): Promise<boolean>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TransitClient = void 0;
|
|
4
|
+
class TransitClient {
|
|
5
|
+
getBaseUrl;
|
|
6
|
+
getToken;
|
|
7
|
+
constructor(getBaseUrl, getToken) {
|
|
8
|
+
this.getBaseUrl = getBaseUrl;
|
|
9
|
+
this.getToken = getToken;
|
|
10
|
+
}
|
|
11
|
+
getHeaders() {
|
|
12
|
+
const token = this.getToken();
|
|
13
|
+
if (!token) {
|
|
14
|
+
throw new Error('[Titan Vault - Transit] Token de Vault no configurado.');
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
'Content-Type': 'application/json',
|
|
18
|
+
'x-vault-token': token,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
async createKey(name, keyType = 'aes-gcm') {
|
|
22
|
+
const url = `${this.getBaseUrl()}/transit/keys/${encodeURIComponent(name)}`;
|
|
23
|
+
const res = await fetch(url, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: this.getHeaders(),
|
|
26
|
+
body: JSON.stringify({ key_type: keyType }),
|
|
27
|
+
});
|
|
28
|
+
if (!res.ok) {
|
|
29
|
+
const err = await res.text();
|
|
30
|
+
throw new Error(`[Titan Vault - Transit] Error creando llave '${name}': ${err}`);
|
|
31
|
+
}
|
|
32
|
+
return res.json();
|
|
33
|
+
}
|
|
34
|
+
async listKeys() {
|
|
35
|
+
const url = `${this.getBaseUrl()}/transit/keys`;
|
|
36
|
+
const res = await fetch(url, {
|
|
37
|
+
method: 'GET',
|
|
38
|
+
headers: this.getHeaders(),
|
|
39
|
+
});
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
const err = await res.text();
|
|
42
|
+
throw new Error(`[Titan Vault - Transit] Error listando llaves: ${err}`);
|
|
43
|
+
}
|
|
44
|
+
return res.json();
|
|
45
|
+
}
|
|
46
|
+
async encrypt(name, plaintext) {
|
|
47
|
+
const url = `${this.getBaseUrl()}/transit/encrypt/${encodeURIComponent(name)}`;
|
|
48
|
+
const res = await fetch(url, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: this.getHeaders(),
|
|
51
|
+
body: JSON.stringify({ plaintext }),
|
|
52
|
+
});
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
const err = await res.text();
|
|
55
|
+
throw new Error(`[Titan Vault - Transit] Error cifrando con '${name}': ${err}`);
|
|
56
|
+
}
|
|
57
|
+
const json = (await res.json());
|
|
58
|
+
return json.ciphertext;
|
|
59
|
+
}
|
|
60
|
+
async decrypt(name, ciphertext) {
|
|
61
|
+
const url = `${this.getBaseUrl()}/transit/decrypt/${encodeURIComponent(name)}`;
|
|
62
|
+
const res = await fetch(url, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: this.getHeaders(),
|
|
65
|
+
body: JSON.stringify({ ciphertext }),
|
|
66
|
+
});
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
const err = await res.text();
|
|
69
|
+
throw new Error(`[Titan Vault - Transit] Error descifrando con '${name}': ${err}`);
|
|
70
|
+
}
|
|
71
|
+
const json = (await res.json());
|
|
72
|
+
return json.plaintext;
|
|
73
|
+
}
|
|
74
|
+
async sign(name, data) {
|
|
75
|
+
const url = `${this.getBaseUrl()}/transit/sign/${encodeURIComponent(name)}`;
|
|
76
|
+
const res = await fetch(url, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: this.getHeaders(),
|
|
79
|
+
body: JSON.stringify({ data }),
|
|
80
|
+
});
|
|
81
|
+
if (!res.ok) {
|
|
82
|
+
const err = await res.text();
|
|
83
|
+
throw new Error(`[Titan Vault - Transit] Error firmando con '${name}': ${err}`);
|
|
84
|
+
}
|
|
85
|
+
const json = (await res.json());
|
|
86
|
+
return json.signature;
|
|
87
|
+
}
|
|
88
|
+
async verify(name, data, signature) {
|
|
89
|
+
const url = `${this.getBaseUrl()}/transit/verify/${encodeURIComponent(name)}`;
|
|
90
|
+
const res = await fetch(url, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: this.getHeaders(),
|
|
93
|
+
body: JSON.stringify({ data, signature }),
|
|
94
|
+
});
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
const err = await res.text();
|
|
97
|
+
throw new Error(`[Titan Vault - Transit] Error verificando firma con '${name}': ${err}`);
|
|
98
|
+
}
|
|
99
|
+
const json = (await res.json());
|
|
100
|
+
return json.valid;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
exports.TransitClient = TransitClient;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,47 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { VaultOptions, HardeningReport } from './types';
|
|
2
|
+
import { CryptoStore } from './core/crypto-store';
|
|
3
|
+
import { DatabaseHooks } from './core/database-hooks';
|
|
4
|
+
import { TransitClient } from './core/transit-client';
|
|
5
|
+
export * from './types';
|
|
6
|
+
export * from './core/poison';
|
|
7
|
+
export * from './core/crypto-store';
|
|
8
|
+
export * from './core/database-hooks';
|
|
9
|
+
export * from './core/transit-client';
|
|
10
|
+
export * from './core/hardening';
|
|
11
|
+
export * from './core/classify';
|
|
12
|
+
export declare class VaultConnectionError extends Error {
|
|
13
|
+
constructor(message: string);
|
|
7
14
|
}
|
|
15
|
+
export declare class TitanVault {
|
|
16
|
+
private static instance;
|
|
17
|
+
static readonly secrets: CryptoStore;
|
|
18
|
+
static readonly databases: DatabaseHooks;
|
|
19
|
+
private static rawConfig;
|
|
20
|
+
private static vaultUrl;
|
|
21
|
+
private static vaultToken?;
|
|
22
|
+
private static isArmed;
|
|
23
|
+
private static baitValues;
|
|
24
|
+
private static tripwireInflight;
|
|
25
|
+
static readonly transit: TransitClient;
|
|
26
|
+
static readonly config: {
|
|
27
|
+
get(key: string, defaultValue?: string): string;
|
|
28
|
+
has(key: string): boolean;
|
|
29
|
+
getAll(): Record<string, string>;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Fetch the app path with an ephemeral RSA-2048 key, decrypt the hybrid
|
|
33
|
+
* payload, and block secret reads on process.env.
|
|
34
|
+
* Throws if the token is missing or the vault does not return ciphertext.
|
|
35
|
+
*/
|
|
36
|
+
static armParanoiaMode(options?: VaultOptions): Promise<void>;
|
|
37
|
+
/** Resolves when the in-flight honeytoken alert has finished or failed. */
|
|
38
|
+
static flushTripwire(): Promise<void>;
|
|
39
|
+
static bootstrap(serverFn: () => Promise<any>, options?: VaultOptions): Promise<void>;
|
|
40
|
+
static verifyRuntimeHardening(): HardeningReport;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Classic mode (`paranoiaMode: false`) copies string values into process.env.
|
|
44
|
+
* The default calls armParanoiaMode and throws if the vault is unreachable.
|
|
45
|
+
*/
|
|
8
46
|
export declare function initVault(options?: VaultOptions): Promise<void>;
|
|
9
|
-
export
|
|
47
|
+
export default TitanVault;
|
package/dist/index.js
CHANGED
|
@@ -1,84 +1,292 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
2
16
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
17
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
18
|
};
|
|
5
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.TitanVault = exports.VaultConnectionError = void 0;
|
|
6
21
|
exports.initVault = initVault;
|
|
7
22
|
const dotenv_1 = __importDefault(require("dotenv"));
|
|
8
23
|
const crypto_1 = __importDefault(require("crypto"));
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
const poison_1 = require("./core/poison");
|
|
25
|
+
const crypto_store_1 = require("./core/crypto-store");
|
|
26
|
+
const database_hooks_1 = require("./core/database-hooks");
|
|
27
|
+
const transit_client_1 = require("./core/transit-client");
|
|
28
|
+
const hardening_1 = require("./core/hardening");
|
|
29
|
+
const classify_1 = require("./core/classify");
|
|
30
|
+
__exportStar(require("./types"), exports);
|
|
31
|
+
__exportStar(require("./core/poison"), exports);
|
|
32
|
+
__exportStar(require("./core/crypto-store"), exports);
|
|
33
|
+
__exportStar(require("./core/database-hooks"), exports);
|
|
34
|
+
__exportStar(require("./core/transit-client"), exports);
|
|
35
|
+
__exportStar(require("./core/hardening"), exports);
|
|
36
|
+
__exportStar(require("./core/classify"), exports);
|
|
37
|
+
class VaultConnectionError extends Error {
|
|
38
|
+
constructor(message) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = 'VaultConnectionError';
|
|
41
|
+
Object.setPrototypeOf(this, VaultConnectionError.prototype);
|
|
20
42
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
}
|
|
44
|
+
exports.VaultConnectionError = VaultConnectionError;
|
|
45
|
+
function resolveToken(explicit) {
|
|
46
|
+
return explicit || process.env.VAULT_TOKEN || process.env.VAULT_APP_TOKEN;
|
|
47
|
+
}
|
|
48
|
+
function resolveUrl(explicit) {
|
|
49
|
+
return explicit || process.env.VAULT_URL || 'http://127.0.0.1:8200';
|
|
50
|
+
}
|
|
51
|
+
function decryptHybrid(privateKey, payloadB64) {
|
|
52
|
+
const buffer = Buffer.from(payloadB64, 'base64');
|
|
53
|
+
const modulusLength = privateKey.asymmetricKeyDetails?.modulusLength;
|
|
54
|
+
if (!modulusLength) {
|
|
55
|
+
throw new VaultConnectionError('No se pudo leer el tamaño de la llave RSA efímera.');
|
|
56
|
+
}
|
|
57
|
+
const keyBytes = modulusLength / 8;
|
|
58
|
+
if (buffer.length < keyBytes + 16) {
|
|
59
|
+
throw new VaultConnectionError('El payload cifrado es más corto que una respuesta RSA-OAEP + AES-256-CFB.');
|
|
60
|
+
}
|
|
61
|
+
const encAesKey = buffer.subarray(0, keyBytes);
|
|
62
|
+
const iv = buffer.subarray(keyBytes, keyBytes + 16);
|
|
63
|
+
const ciphertext = buffer.subarray(keyBytes + 16);
|
|
64
|
+
const aesKey = crypto_1.default.privateDecrypt({
|
|
65
|
+
key: privateKey,
|
|
66
|
+
padding: crypto_1.default.constants.RSA_PKCS1_OAEP_PADDING,
|
|
67
|
+
oaepHash: 'sha256',
|
|
68
|
+
}, encAesKey);
|
|
69
|
+
try {
|
|
70
|
+
const decipher = crypto_1.default.createDecipheriv('aes-256-cfb', aesKey, iv);
|
|
71
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
72
|
+
return decrypted.toString('utf8');
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
aesKey.fill(0);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
class TitanVault {
|
|
79
|
+
static instance;
|
|
80
|
+
static secrets = new crypto_store_1.CryptoStore();
|
|
81
|
+
static databases = new database_hooks_1.DatabaseHooks();
|
|
82
|
+
static rawConfig = new Map();
|
|
83
|
+
static vaultUrl = 'http://127.0.0.1:8200';
|
|
84
|
+
static vaultToken;
|
|
85
|
+
static isArmed = false;
|
|
86
|
+
static baitValues = new Map();
|
|
87
|
+
static tripwireInflight = Promise.resolve();
|
|
88
|
+
static transit = new transit_client_1.TransitClient(() => TitanVault.vaultUrl, () => TitanVault.vaultToken);
|
|
89
|
+
static config = {
|
|
90
|
+
get(key, defaultValue) {
|
|
91
|
+
if (TitanVault.rawConfig.has(key)) {
|
|
92
|
+
return TitanVault.rawConfig.get(key);
|
|
93
|
+
}
|
|
94
|
+
let envVal;
|
|
95
|
+
try {
|
|
96
|
+
envVal = process.env[key];
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
if (err instanceof Error && err.name === 'ParanoiaBlockedError') {
|
|
100
|
+
throw new Error(`[Titan Vault] '${key}' es un secreto. Usa TitanVault.secrets, TitanVault.databases o el campo de configuración correspondiente.`);
|
|
37
101
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
if (envVal !== undefined)
|
|
105
|
+
return envVal;
|
|
106
|
+
if (defaultValue !== undefined)
|
|
107
|
+
return defaultValue;
|
|
108
|
+
throw new Error(`[Titan Vault] Configuración '${key}' no encontrada.`);
|
|
109
|
+
},
|
|
110
|
+
has(key) {
|
|
111
|
+
return TitanVault.rawConfig.has(key) || key in process.env;
|
|
112
|
+
},
|
|
113
|
+
getAll() {
|
|
114
|
+
const res = {};
|
|
115
|
+
for (const [k, v] of TitanVault.rawConfig.entries()) {
|
|
116
|
+
res[k] = v;
|
|
117
|
+
}
|
|
118
|
+
return res;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Fetch the app path with an ephemeral RSA-2048 key, decrypt the hybrid
|
|
123
|
+
* payload, and block secret reads on process.env.
|
|
124
|
+
* Throws if the token is missing or the vault does not return ciphertext.
|
|
125
|
+
*/
|
|
126
|
+
static async armParanoiaMode(options = {}) {
|
|
127
|
+
if (this.isArmed) {
|
|
128
|
+
console.log('🛡️ [Titan Vault] Modo paranoia ya está armado.');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
dotenv_1.default.config();
|
|
132
|
+
this.vaultUrl = resolveUrl(options.vaultUrl);
|
|
133
|
+
this.vaultToken = resolveToken(options.vaultToken);
|
|
134
|
+
const secretPath = options.path || 'backend_env';
|
|
135
|
+
const maxRetries = options.maxRetries ?? 5;
|
|
136
|
+
const retryDelay = options.retryDelayMs ?? 5000;
|
|
137
|
+
const honeyField = options.honeytokenField || classify_1.HONEYTOKEN_FIELD;
|
|
138
|
+
if (!this.vaultToken) {
|
|
139
|
+
throw new VaultConnectionError('Falta el token. Pasa vaultToken o define VAULT_TOKEN / VAULT_APP_TOKEN. El proceso no arranca sin la bóveda.');
|
|
140
|
+
}
|
|
141
|
+
console.log(`\n🛡️ [Titan Vault] Conectando a ${this.vaultUrl}...`);
|
|
142
|
+
const { publicKey, privateKey } = crypto_1.default.generateKeyPairSync('rsa', {
|
|
143
|
+
modulusLength: 2048,
|
|
144
|
+
});
|
|
145
|
+
const pubPem = publicKey.export({ type: 'spki', format: 'pem' });
|
|
146
|
+
const pubBase64 = Buffer.from(pubPem).toString('base64');
|
|
147
|
+
let rawSecrets = null;
|
|
148
|
+
let lastError = 'sin respuesta';
|
|
149
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
150
|
+
try {
|
|
151
|
+
const res = await fetch(`${this.vaultUrl}/secret/data/${secretPath}`, {
|
|
152
|
+
headers: {
|
|
153
|
+
'x-vault-token': this.vaultToken,
|
|
154
|
+
'x-vault-public-key': pubBase64,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
if (!res.ok) {
|
|
158
|
+
lastError = `HTTP ${res.status}: ${await res.text()}`;
|
|
159
|
+
console.warn(`[Titan Vault] ${lastError}. Reintento ${attempt}/${maxRetries}.`);
|
|
57
160
|
}
|
|
58
161
|
else {
|
|
59
|
-
|
|
162
|
+
const json = await res.json();
|
|
163
|
+
if (!json.data?.is_encrypted || typeof json.data.value !== 'string') {
|
|
164
|
+
throw new VaultConnectionError('La bóveda devolvió el secreto en claro después de recibir x-vault-public-key.');
|
|
165
|
+
}
|
|
166
|
+
console.log('🔐 [Titan Vault] Descifrando payload híbrido (RSA-OAEP-SHA256 + AES-256-CFB)...');
|
|
167
|
+
rawSecrets = JSON.parse(decryptHybrid(privateKey, json.data.value));
|
|
168
|
+
break;
|
|
60
169
|
}
|
|
61
|
-
const secrets = JSON.parse(finalSecretStr);
|
|
62
|
-
// Inject into process.env
|
|
63
|
-
let injectedCount = 0;
|
|
64
|
-
for (const [key, value] of Object.entries(secrets)) {
|
|
65
|
-
process.env[key] = value;
|
|
66
|
-
injectedCount++;
|
|
67
|
-
}
|
|
68
|
-
console.log(`✅ [Titan Vault] ${injectedCount} secretos inyectados en memoria (Zero-Trust).`);
|
|
69
|
-
return; // Success!
|
|
70
170
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
171
|
+
catch (err) {
|
|
172
|
+
if (err instanceof VaultConnectionError)
|
|
173
|
+
throw err;
|
|
174
|
+
lastError = err?.message || String(err);
|
|
175
|
+
console.warn(`❌ [Titan Vault] ${lastError}. Reintento ${attempt}/${maxRetries}.`);
|
|
74
176
|
}
|
|
177
|
+
if (attempt < maxRetries) {
|
|
178
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (!rawSecrets) {
|
|
182
|
+
throw new VaultConnectionError(`No se pudieron leer los secretos de '${secretPath}' en ${this.vaultUrl}. ${lastError}`);
|
|
75
183
|
}
|
|
76
|
-
|
|
77
|
-
|
|
184
|
+
const classification = rawSecrets['__titan_classification__'] || {};
|
|
185
|
+
delete rawSecrets['__titan_classification__'];
|
|
186
|
+
const allowedEnvList = new Set(options.allowedEnvKeys || ['NODE_ENV', 'PORT', 'HOST', 'FRONTEND_PORT', 'BACKEND_PORT']);
|
|
187
|
+
const poisonedKeys = [];
|
|
188
|
+
this.baitValues = new Map();
|
|
189
|
+
let cryptoCount = 0;
|
|
190
|
+
let dbCount = 0;
|
|
191
|
+
let configCount = 0;
|
|
192
|
+
for (const [key, value] of Object.entries(rawSecrets)) {
|
|
193
|
+
if (typeof value !== 'string')
|
|
194
|
+
continue;
|
|
195
|
+
const kind = (0, classify_1.classifyKey)(key, classification[key], options);
|
|
196
|
+
if (kind === 'honeytoken') {
|
|
197
|
+
this.baitValues.set(key, value);
|
|
198
|
+
poisonedKeys.push(key);
|
|
199
|
+
}
|
|
200
|
+
else if (kind === 'crypto') {
|
|
201
|
+
this.secrets.register(key, value);
|
|
202
|
+
poisonedKeys.push(key);
|
|
203
|
+
cryptoCount++;
|
|
204
|
+
}
|
|
205
|
+
else if (kind === 'database') {
|
|
206
|
+
this.databases.register(key, value);
|
|
207
|
+
poisonedKeys.push(key);
|
|
208
|
+
dbCount++;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
this.rawConfig.set(key, value);
|
|
212
|
+
if (allowedEnvList.has(key)) {
|
|
213
|
+
process.env[key] = value;
|
|
214
|
+
}
|
|
215
|
+
configCount++;
|
|
216
|
+
}
|
|
78
217
|
}
|
|
79
|
-
if (
|
|
80
|
-
|
|
218
|
+
if (!poisonedKeys.includes(honeyField)) {
|
|
219
|
+
poisonedKeys.push(honeyField);
|
|
81
220
|
}
|
|
221
|
+
const onDetonate = (tokenKey) => {
|
|
222
|
+
console.error(`\n🚨 [Titan Vault] Honeytoken '${tokenKey}' leído desde process.env.`);
|
|
223
|
+
const bait = this.baitValues.get(tokenKey);
|
|
224
|
+
if (bait) {
|
|
225
|
+
this.tripwireInflight = fetch(`${this.vaultUrl}/sys/recovery`, {
|
|
226
|
+
headers: { 'x-vault-token': bait },
|
|
227
|
+
}).then(() => undefined, () => undefined);
|
|
228
|
+
}
|
|
229
|
+
options.onHoneytokenDetonated?.(tokenKey);
|
|
230
|
+
};
|
|
231
|
+
(0, poison_1.poisonProcessEnv)(poisonedKeys, honeyField, onDetonate);
|
|
232
|
+
this.isArmed = true;
|
|
233
|
+
console.log('🛡️ [Titan Vault] Modo paranoia armado:');
|
|
234
|
+
console.log(` - ${cryptoCount} llaves en KeyObject (sin copia string en el SDK)`);
|
|
235
|
+
console.log(` - ${dbCount} contraseñas de base de datos para hooks JIT`);
|
|
236
|
+
console.log(` - ${configCount} valores de configuración, fuera de process.env salvo la lista permitida`);
|
|
237
|
+
console.log(` - ${poisonedKeys.length} nombres bloqueados en process.env`);
|
|
238
|
+
console.log(this.baitValues.size
|
|
239
|
+
? ' - trampa armada: leerla llama a GET /sys/recovery con el valor inyectado por la bóveda'
|
|
240
|
+
: ' - trampa local activa, sin cebo en el payload (la bóveda no se sella)');
|
|
241
|
+
}
|
|
242
|
+
/** Resolves when the in-flight honeytoken alert has finished or failed. */
|
|
243
|
+
static flushTripwire() {
|
|
244
|
+
return this.tripwireInflight;
|
|
245
|
+
}
|
|
246
|
+
static async bootstrap(serverFn, options = {}) {
|
|
247
|
+
await this.armParanoiaMode(options);
|
|
248
|
+
console.log('🚀 [Titan Vault] Secretos cargados. Arrancando servidor...');
|
|
249
|
+
await serverFn();
|
|
250
|
+
}
|
|
251
|
+
static verifyRuntimeHardening() {
|
|
252
|
+
return (0, hardening_1.verifyRuntimeHardening)(this.isArmed);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
exports.TitanVault = TitanVault;
|
|
256
|
+
/**
|
|
257
|
+
* Classic mode (`paranoiaMode: false`) copies string values into process.env.
|
|
258
|
+
* The default calls armParanoiaMode and throws if the vault is unreachable.
|
|
259
|
+
*/
|
|
260
|
+
async function initVault(options = {}) {
|
|
261
|
+
if (options.paranoiaMode !== false) {
|
|
262
|
+
await TitanVault.armParanoiaMode(options);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
dotenv_1.default.config();
|
|
266
|
+
const vaultUrl = resolveUrl(options.vaultUrl);
|
|
267
|
+
const vaultToken = resolveToken(options.vaultToken);
|
|
268
|
+
const secretPath = options.path || 'backend_env';
|
|
269
|
+
const honeyField = options.honeytokenField || classify_1.HONEYTOKEN_FIELD;
|
|
270
|
+
if (!vaultToken) {
|
|
271
|
+
console.warn('⚠️ [Titan Vault] Sin token. Se mantienen las variables de .env.');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const res = await fetch(`${vaultUrl}/secret/data/${secretPath}`, {
|
|
275
|
+
headers: { 'x-vault-token': vaultToken },
|
|
276
|
+
});
|
|
277
|
+
if (!res.ok) {
|
|
278
|
+
throw new VaultConnectionError(`Modo clásico: la bóveda respondió HTTP ${res.status} al leer '${secretPath}'.`);
|
|
279
|
+
}
|
|
280
|
+
const json = await res.json();
|
|
281
|
+
const secrets = typeof json.data?.value === 'string' ? JSON.parse(json.data.value) : json.data?.value || {};
|
|
282
|
+
const classification = secrets['__titan_classification__'] || {};
|
|
283
|
+
for (const [key, value] of Object.entries(secrets)) {
|
|
284
|
+
if (key === '__titan_classification__' || typeof value !== 'string')
|
|
285
|
+
continue;
|
|
286
|
+
if ((0, classify_1.classifyKey)(key, classification[key], options) === 'honeytoken' || key === honeyField)
|
|
287
|
+
continue;
|
|
288
|
+
process.env[key] = value;
|
|
82
289
|
}
|
|
83
|
-
console.
|
|
290
|
+
console.log('✅ [Titan Vault] Secretos copiados a process.env (modo clásico).');
|
|
84
291
|
}
|
|
292
|
+
exports.default = TitanVault;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export type SecretType = 'crypto' | 'database' | 'config' | 'honeytoken';
|
|
2
|
+
export interface VaultOptions {
|
|
3
|
+
vaultUrl?: string;
|
|
4
|
+
vaultToken?: string;
|
|
5
|
+
path?: string;
|
|
6
|
+
maxRetries?: number;
|
|
7
|
+
retryDelayMs?: number;
|
|
8
|
+
/**
|
|
9
|
+
* If true, activates Paranoia Mode (process.env poisoning, C++ KeyObject storage, JIT DB credentials).
|
|
10
|
+
* Defaults to true in armParanoiaMode().
|
|
11
|
+
*/
|
|
12
|
+
paranoiaMode?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Keys explicitly marked as cryptographic secrets (instantiated as C++ KeyObjects).
|
|
15
|
+
*/
|
|
16
|
+
cryptoKeys?: string[];
|
|
17
|
+
/**
|
|
18
|
+
* Keys explicitly marked as database credentials (accessible only via JIT DB hooks).
|
|
19
|
+
*/
|
|
20
|
+
dbKeys?: string[];
|
|
21
|
+
/**
|
|
22
|
+
* Keys that are safe to expose in process.env (e.g. PORT, NODE_ENV).
|
|
23
|
+
*/
|
|
24
|
+
allowedEnvKeys?: string[];
|
|
25
|
+
/**
|
|
26
|
+
* Field the vault injects when `TITAN_HONEYTOKEN_FIELD` is set.
|
|
27
|
+
* Defaults to `VAULT_EMERGENCY_RECOVERY_TOKEN`.
|
|
28
|
+
*/
|
|
29
|
+
honeytokenField?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Custom callback triggered if a honeytoken is accessed in process.env.
|
|
32
|
+
*/
|
|
33
|
+
onHoneytokenDetonated?: (tokenName: string) => void;
|
|
34
|
+
}
|
|
35
|
+
export interface TransitKeyInfo {
|
|
36
|
+
name: string;
|
|
37
|
+
key_type: 'aes-gcm' | 'hmac-sha256' | string;
|
|
38
|
+
created_at: string | null;
|
|
39
|
+
status?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface TransitEncryptResult {
|
|
42
|
+
ciphertext: string;
|
|
43
|
+
name: string;
|
|
44
|
+
}
|
|
45
|
+
export interface TransitDecryptResult {
|
|
46
|
+
plaintext: string;
|
|
47
|
+
name: string;
|
|
48
|
+
}
|
|
49
|
+
export interface TransitSignResult {
|
|
50
|
+
signature: string;
|
|
51
|
+
name: string;
|
|
52
|
+
}
|
|
53
|
+
export interface TransitVerifyResult {
|
|
54
|
+
valid: boolean;
|
|
55
|
+
name: string;
|
|
56
|
+
}
|
|
57
|
+
export interface HardeningReport {
|
|
58
|
+
isHardened: boolean;
|
|
59
|
+
nodeEnv: string;
|
|
60
|
+
/** True only when the soft core limit was read and is 0. */
|
|
61
|
+
coreDumpsDisabled: boolean;
|
|
62
|
+
/** False when `/proc/self/limits` could not be read. */
|
|
63
|
+
coreDumpLimitKnown: boolean;
|
|
64
|
+
runningAsRoot: boolean;
|
|
65
|
+
paranoiaArmed: boolean;
|
|
66
|
+
recommendations: string[];
|
|
67
|
+
}
|
package/dist/types.js
ADDED
package/package.json
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "titan-vault-node",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "Client for Titan Vault. Decrypts the hybrid secret payload, blocks secret reads on process.env, and calls transit on the vault.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
7
10
|
"scripts": {
|
|
8
|
-
"build": "tsc"
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"prepare": "npm run build",
|
|
13
|
+
"test": "npm run build && node --test test/harmony.test.mjs"
|
|
9
14
|
},
|
|
10
15
|
"keywords": [
|
|
11
16
|
"vault",
|
package/src/index.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import dotenv from 'dotenv';
|
|
2
|
-
import crypto from 'crypto';
|
|
3
|
-
|
|
4
|
-
interface VaultOptions {
|
|
5
|
-
vaultUrl?: string;
|
|
6
|
-
vaultToken?: string;
|
|
7
|
-
path?: string;
|
|
8
|
-
maxRetries?: number;
|
|
9
|
-
retryDelayMs?: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export async function initVault(options: VaultOptions = {}) {
|
|
13
|
-
// Load local .env first to get base configs (like VAULT_URL or VAULT_TOKEN)
|
|
14
|
-
dotenv.config();
|
|
15
|
-
|
|
16
|
-
const VAULT_URL = options.vaultUrl || process.env.VAULT_URL || 'http://127.0.0.1:8200';
|
|
17
|
-
const VAULT_TOKEN = options.vaultToken || process.env.VAULT_TOKEN;
|
|
18
|
-
const SECRET_PATH = options.path || 'backend_env';
|
|
19
|
-
const MAX_RETRIES = options.maxRetries || 5;
|
|
20
|
-
const RETRY_DELAY = options.retryDelayMs || 5000;
|
|
21
|
-
|
|
22
|
-
if (!VAULT_TOKEN) {
|
|
23
|
-
console.warn("⚠️ [Titan Vault] No VAULT_TOKEN found. Using local .env fallback.");
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
console.log(`\n🔒 [Titan Vault] Conectando a ${VAULT_URL}...`);
|
|
28
|
-
|
|
29
|
-
// E2E Encryption: Generate ephemeral RSA keypair
|
|
30
|
-
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
|
|
31
|
-
modulusLength: 2048,
|
|
32
|
-
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
33
|
-
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const pubBase64 = Buffer.from(publicKey).toString('base64');
|
|
37
|
-
let attempt = 0;
|
|
38
|
-
|
|
39
|
-
while (attempt < MAX_RETRIES) {
|
|
40
|
-
attempt++;
|
|
41
|
-
try {
|
|
42
|
-
const res = await fetch(`${VAULT_URL}/secret/data/${SECRET_PATH}`, {
|
|
43
|
-
headers: {
|
|
44
|
-
"x-vault-token": VAULT_TOKEN,
|
|
45
|
-
"x-vault-public-key": pubBase64
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
if (res.ok) {
|
|
50
|
-
const json = await res.json();
|
|
51
|
-
let finalSecretStr = "";
|
|
52
|
-
|
|
53
|
-
if (json.data && json.data.is_encrypted) {
|
|
54
|
-
console.log("🔐 [Titan Vault] Descifrando payload E2E híbrido (RSA-OAEP + AES-CFB)...");
|
|
55
|
-
const buffer = Buffer.from(json.data.value, 'base64');
|
|
56
|
-
|
|
57
|
-
const encAesKey = buffer.subarray(0, 256);
|
|
58
|
-
const iv = buffer.subarray(256, 256 + 16);
|
|
59
|
-
const ciphertext = buffer.subarray(256 + 16);
|
|
60
|
-
|
|
61
|
-
const aesKey = crypto.privateDecrypt({
|
|
62
|
-
key: privateKey,
|
|
63
|
-
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
|
|
64
|
-
oaepHash: "sha256"
|
|
65
|
-
}, encAesKey);
|
|
66
|
-
|
|
67
|
-
const decipher = crypto.createDecipheriv('aes-256-cfb', aesKey, iv);
|
|
68
|
-
let decrypted = decipher.update(ciphertext, undefined, 'utf8');
|
|
69
|
-
decrypted += decipher.final('utf8');
|
|
70
|
-
|
|
71
|
-
finalSecretStr = decrypted;
|
|
72
|
-
} else {
|
|
73
|
-
finalSecretStr = json.data.value;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const secrets = JSON.parse(finalSecretStr);
|
|
77
|
-
|
|
78
|
-
// Inject into process.env
|
|
79
|
-
let injectedCount = 0;
|
|
80
|
-
for (const [key, value] of Object.entries(secrets)) {
|
|
81
|
-
process.env[key] = value as string;
|
|
82
|
-
injectedCount++;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
console.log(`✅ [Titan Vault] ${injectedCount} secretos inyectados en memoria (Zero-Trust).`);
|
|
86
|
-
return; // Success!
|
|
87
|
-
} else {
|
|
88
|
-
const errText = await res.text();
|
|
89
|
-
console.warn(`[Titan Vault] Vault sellado o token inválido (HTTP ${res.status}): ${errText}. Reintentando en ${RETRY_DELAY/1000}s...`);
|
|
90
|
-
}
|
|
91
|
-
} catch (err: any) {
|
|
92
|
-
console.warn(`❌ [Titan Vault] Error de conexión (${err.message}). Reintentando en ${RETRY_DELAY/1000}s...`);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (attempt < MAX_RETRIES) {
|
|
96
|
-
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
console.error("🚨 [Titan Vault] No se pudo conectar al vault tras múltiples intentos. Iniciando con secretos locales si existen.");
|
|
101
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "CommonJS",
|
|
5
|
-
"moduleResolution": "node",
|
|
6
|
-
"declaration": true,
|
|
7
|
-
"outDir": "./dist",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"forceConsistentCasingInFileNames": true
|
|
12
|
-
},
|
|
13
|
-
"include": ["src/**/*"]
|
|
14
|
-
}
|