titan-vault-node 1.0.0 → 2.0.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 +134 -41
- package/dist/core/crypto-store.d.ts +22 -0
- package/dist/core/crypto-store.js +80 -0
- package/dist/core/database-hooks.d.ts +19 -0
- package/dist/core/database-hooks.js +69 -0
- package/dist/core/hardening.d.ts +2 -0
- package/dist/core/hardening.js +39 -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 +44 -7
- package/dist/index.js +245 -62
- package/dist/types.d.ts +58 -0
- package/dist/types.js +2 -0
- package/package.json +6 -2
- package/src/index.ts +0 -101
- package/tsconfig.json +0 -14
package/README.md
CHANGED
|
@@ -1,66 +1,159 @@
|
|
|
1
|
-
# 🛡️ Titan Vault (Node.js SDK)
|
|
1
|
+
# 🛡️ Titan Vault (Node.js SDK) — Modo Paranoia & Zero-Trust
|
|
2
2
|
|
|
3
|
-
> **Zero-
|
|
3
|
+
> **SDK de seguridad Zero-Trust, Criptografía E2E, Envenenamiento de `process.env` y Protección de Memoria RAM.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`titan-vault-node` resuelve la vulnerabilidad fundamental de Node.js: **la exposición de secretos en texto plano en la memoria RAM (Heap Dumps) y `process.env`.**
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## ⚡ ¿Por qué `dotenv` y `process.env` son inseguros?
|
|
10
|
+
|
|
11
|
+
1. **Memory Dumps:** Cualquier vulnerabilidad que permita un volcado de memoria (heapdump, coredump, inspector v8) expone todas las variables de entorno en texto plano.
|
|
12
|
+
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.
|
|
13
|
+
3. **Peligro de Logs:** Un simple `console.log(process.env)` o reporte de error APM filtra todas tus credenciales.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 🛡️ La Solución: Modo Paranoia (Los 4 Pilares)
|
|
18
|
+
|
|
19
|
+
`titan-vault-node` implementa una arquitectura de grado militar:
|
|
20
|
+
|
|
21
|
+
1. **Patrón Bootstrap (Arranque Seguro):** Pausa el arranque del runtime para negociar llaves efímeras (RSA-2048 + AES-256-CFB) y proteger la memoria antes de evaluar el código de negocio.
|
|
22
|
+
2. **Envenenamiento de `process.env`:** Borra los secretos del entorno global y coloca un `Proxy` reactivo. Si alguien o una librería de terceros intenta hacer `process.env.JWT_SECRET`, la ejecución se interrumpe con `ParanoiaBlockedError`. Además, las variables secretas no son enumerables (no aparecen en `Object.keys()` ni `console.log(process.env)`).
|
|
23
|
+
3. **Criptografía Nativa C++ (`KeyObject`):** Las llaves simétricas (JWT, cookies, AES) se cargan directamente como instancias nativas `crypto.KeyObject` de C++, invisibles a búsquedas de strings en el heap de V8.
|
|
24
|
+
4. **Inyección JIT en Bases de Datos:**
|
|
25
|
+
- **Sequelize:** Gancho `beforeConnect` para inyectar contraseñas microsegundos antes del handshake, y `afterConnect` para nulificar (`password = null`) de inmediato.
|
|
26
|
+
- **node-postgres (`pg`):** Función asíncrona de contraseña bajo demanda (`async () => password`).
|
|
27
|
+
5. **CaaS / Transit Engine:** Criptografía como Servicio. Permite firmar tokens o cifrar datos directamente en el servidor Titan Vault sin mantener ninguna llave en memoria.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 💻 Guía de Integración
|
|
9
32
|
|
|
10
|
-
|
|
11
|
-
Titan Vault stores your secrets in a centralized, encrypted server. Your app authenticates using a lightweight AppToken, and the Vault dynamically injects the secrets directly into Node.js memory (`process.env`) using **Hybrid E2E Encryption (RSA-OAEP + AES-CFB)**. If the app stops, the secrets disappear.
|
|
33
|
+
### 1. El Patrón Bootstrap (`bootstrap.js` o `src/index.ts`)
|
|
12
34
|
|
|
13
|
-
|
|
14
|
-
- **Zero-Friction SDK:** Replaces `dotenv` with exactly 2 lines of code.
|
|
15
|
-
- **E2E Encryption out-of-the-box:** Secrets are never sent in plain text over the network.
|
|
16
|
-
- **Honeytokens & Webhooks:** Create fake "Honeytoken" secrets. If a hacker tries to use them, the Vault instantly locks down and sends an alert to your **Telegram/Slack**.
|
|
17
|
-
- **Burn-on-read Tokens:** For maximum paranoia, use one-time wrap tokens that self-destruct after your app boots.
|
|
35
|
+
Crea un archivo de arranque que cargue el SDK antes de importar el servidor:
|
|
18
36
|
|
|
19
|
-
|
|
37
|
+
```typescript
|
|
38
|
+
// bootstrap.ts
|
|
39
|
+
import { TitanVault } from 'titan-vault-node';
|
|
40
|
+
|
|
41
|
+
async function main() {
|
|
42
|
+
// 1. Arma el Modo Paranoia y asegura la memoria
|
|
43
|
+
await TitanVault.armParanoiaMode({
|
|
44
|
+
vaultUrl: process.env.VAULT_URL || 'http://127.0.0.1:8200',
|
|
45
|
+
vaultToken: process.env.VAULT_APP_TOKEN,
|
|
46
|
+
path: 'mi_aplicacion_env',
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// 2. Importa el servidor dinámicamente UNA VEZ que la memoria está blindada
|
|
50
|
+
await import('./server.js');
|
|
51
|
+
}
|
|
20
52
|
|
|
21
|
-
|
|
22
|
-
npm install titan-vault-node
|
|
53
|
+
main();
|
|
23
54
|
```
|
|
24
55
|
|
|
25
|
-
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
### 2. Firma y Validación de Tokens JWT (C++ `KeyObject`)
|
|
59
|
+
|
|
60
|
+
Compatible de forma nativa con `jsonwebtoken`, `jose` y el módulo `crypto` de Node.js:
|
|
26
61
|
|
|
27
|
-
|
|
62
|
+
```typescript
|
|
63
|
+
import jwt from 'jsonwebtoken';
|
|
64
|
+
import { TitanVault } from 'titan-vault-node';
|
|
28
65
|
|
|
29
|
-
|
|
30
|
-
|
|
66
|
+
// Obtiene el KeyObject de C++ (Invisible en volcados de RAM)
|
|
67
|
+
const jwtKey = TitanVault.secrets.get('JWT_SECRET');
|
|
31
68
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
69
|
+
// Firmar
|
|
70
|
+
const token = jwt.sign({ userId: 123, role: 'admin' }, jwtKey, {
|
|
71
|
+
algorithm: 'HS256',
|
|
72
|
+
expiresIn: '2h',
|
|
73
|
+
});
|
|
74
|
+
|
|
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
|
-
// Start your Express/NestJS/HTTP server...
|
|
50
|
-
}
|
|
116
|
+
---
|
|
51
117
|
|
|
52
|
-
|
|
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');
|
|
127
|
+
|
|
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
|
-
Want to be notified if someone touches your Honeytoken? Run your Titan Vault server with Telegram variables:
|
|
138
|
+
---
|
|
57
139
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
140
|
+
### 5. Parámetros de Configuración No Sensibles
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
const port = TitanVault.config.get('PORT', '3000');
|
|
144
|
+
const host = TitanVault.config.get('HOST', '0.0.0.0');
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
### 6. Verificación de Hardening en Contenedores
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
TitanVault.verifyRuntimeHardening();
|
|
62
153
|
```
|
|
63
|
-
If your Honeytoken is triggered, the Vault seals itself and sends a red alert to your phone instantly.
|
|
64
154
|
|
|
65
155
|
---
|
|
66
|
-
|
|
156
|
+
|
|
157
|
+
## 🍯 Tripwires y Honeytokens
|
|
158
|
+
|
|
159
|
+
Si un atacante inspecciona `process.env` e intenta leer la variable trampa `VAULT_EMERGENCY_RECOVERY_TOKEN`, Titan Vault detona inmediatamente una alerta de intrusión hacia el servidor central y sella la bóveda.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
export declare class CryptoStore {
|
|
3
|
+
private keyObjects;
|
|
4
|
+
private rawFallback;
|
|
5
|
+
/**
|
|
6
|
+
* Securely store a secret key by converting it to a native C++ KeyObject
|
|
7
|
+
* and immediately zeroing out the JavaScript buffer.
|
|
8
|
+
*/
|
|
9
|
+
register(name: string, rawSecret: string): void;
|
|
10
|
+
/**
|
|
11
|
+
* Retrieves the native C++ KeyObject.
|
|
12
|
+
* This object is invisible to JavaScript heap string inspection and safe from RAM dumps.
|
|
13
|
+
*/
|
|
14
|
+
get(name: string): crypto.KeyObject;
|
|
15
|
+
/**
|
|
16
|
+
* Retrieves the raw string representation.
|
|
17
|
+
* WARNING: Accessing secrets as raw strings reintroduces them to the V8 Heap.
|
|
18
|
+
*/
|
|
19
|
+
getRaw(name: string): string;
|
|
20
|
+
has(name: string): boolean;
|
|
21
|
+
keys(): string[];
|
|
22
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
rawFallback = new Map();
|
|
11
|
+
/**
|
|
12
|
+
* Securely store a secret key by converting it to a native C++ KeyObject
|
|
13
|
+
* and immediately zeroing out the JavaScript buffer.
|
|
14
|
+
*/
|
|
15
|
+
register(name, rawSecret) {
|
|
16
|
+
if (!rawSecret)
|
|
17
|
+
return;
|
|
18
|
+
// Allocate buffer for the raw secret
|
|
19
|
+
const buffer = Buffer.from(rawSecret, 'utf-8');
|
|
20
|
+
try {
|
|
21
|
+
// Check if it's PEM formatted private key
|
|
22
|
+
if (rawSecret.includes('-----BEGIN PRIVATE KEY-----') || rawSecret.includes('-----BEGIN RSA PRIVATE KEY-----')) {
|
|
23
|
+
const privKey = crypto_1.default.createPrivateKey({
|
|
24
|
+
key: rawSecret,
|
|
25
|
+
format: 'pem',
|
|
26
|
+
});
|
|
27
|
+
this.keyObjects.set(name, privKey);
|
|
28
|
+
}
|
|
29
|
+
else if (rawSecret.includes('-----BEGIN PUBLIC KEY-----')) {
|
|
30
|
+
const pubKey = crypto_1.default.createPublicKey({
|
|
31
|
+
key: rawSecret,
|
|
32
|
+
format: 'pem',
|
|
33
|
+
});
|
|
34
|
+
this.keyObjects.set(name, pubKey);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
// Symmetric secret key (HMAC, AES, JWT, cookies)
|
|
38
|
+
const secretKey = crypto_1.default.createSecretKey(buffer);
|
|
39
|
+
this.keyObjects.set(name, secretKey);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
// ZERO OUT BUFFER IN MEMORY immediately to prevent RAM heap dump recovery
|
|
44
|
+
buffer.fill(0);
|
|
45
|
+
}
|
|
46
|
+
// Keep an internal raw fallback for libraries that strictly require raw strings (e.g. legacy argon2)
|
|
47
|
+
this.rawFallback.set(name, rawSecret);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Retrieves the native C++ KeyObject.
|
|
51
|
+
* This object is invisible to JavaScript heap string inspection and safe from RAM dumps.
|
|
52
|
+
*/
|
|
53
|
+
get(name) {
|
|
54
|
+
const key = this.keyObjects.get(name);
|
|
55
|
+
if (!key) {
|
|
56
|
+
throw new Error(`[Titan Vault] Llave criptográfica '${name}' no encontrada en el almacén seguro.`);
|
|
57
|
+
}
|
|
58
|
+
return key;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Retrieves the raw string representation.
|
|
62
|
+
* WARNING: Accessing secrets as raw strings reintroduces them to the V8 Heap.
|
|
63
|
+
*/
|
|
64
|
+
getRaw(name) {
|
|
65
|
+
const raw = this.rawFallback.get(name);
|
|
66
|
+
if (!raw) {
|
|
67
|
+
throw new Error(`[Titan Vault] Llave '${name}' no encontrada.`);
|
|
68
|
+
}
|
|
69
|
+
console.warn(`⚠️ [Titan Vault - Seguridad] Se accedió a la llave '${name}' como texto plano. ` +
|
|
70
|
+
`Esto expone el secreto en el Heap de V8. Considera usar 'get("${name}")' devolviendo KeyObject.`);
|
|
71
|
+
return raw;
|
|
72
|
+
}
|
|
73
|
+
has(name) {
|
|
74
|
+
return this.keyObjects.has(name);
|
|
75
|
+
}
|
|
76
|
+
keys() {
|
|
77
|
+
return Array.from(this.keyObjects.keys());
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
exports.CryptoStore = CryptoStore;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare class DatabaseHooks {
|
|
2
|
+
private credentials;
|
|
3
|
+
register(key: string, value: string): void;
|
|
4
|
+
getJitCredential(key: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Hook for Sequelize ORM.
|
|
7
|
+
* Injects the database password micro-seconds before connection handshake,
|
|
8
|
+
* and immediately wipes it (sets config.password = null) after connection succeeds,
|
|
9
|
+
* minimizing RAM heap exposure.
|
|
10
|
+
*/
|
|
11
|
+
hookSequelize(sequelize: any, options?: {
|
|
12
|
+
credentialKey?: string;
|
|
13
|
+
}): void;
|
|
14
|
+
/**
|
|
15
|
+
* Generates an async password provider for node-postgres (pg.Pool).
|
|
16
|
+
* pg.Pool supports `password: async () => string` dynamically per socket connection.
|
|
17
|
+
*/
|
|
18
|
+
getPgPasswordProvider(keyName?: string): () => Promise<string>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
this.credentials.set(key, value);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
getJitCredential(key) {
|
|
12
|
+
const cred = this.credentials.get(key);
|
|
13
|
+
if (!cred) {
|
|
14
|
+
throw new Error(`[Titan Vault - JIT DB] Credencial '${key}' no encontrada en el almacén JIT.`);
|
|
15
|
+
}
|
|
16
|
+
return cred;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Hook for Sequelize ORM.
|
|
20
|
+
* Injects the database password micro-seconds before connection handshake,
|
|
21
|
+
* and immediately wipes it (sets config.password = null) after connection succeeds,
|
|
22
|
+
* minimizing RAM heap exposure.
|
|
23
|
+
*/
|
|
24
|
+
hookSequelize(sequelize, options = {}) {
|
|
25
|
+
const credKey = options.credentialKey || 'DB_PASSWORD';
|
|
26
|
+
if (!sequelize || typeof sequelize.addHook !== 'function') {
|
|
27
|
+
console.warn('⚠️ [Titan Vault] La instancia provista no parece ser un objeto Sequelize válido.');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// 1. Before connection handshake: Inject credential JIT
|
|
31
|
+
sequelize.addHook('beforeConnect', async (config) => {
|
|
32
|
+
try {
|
|
33
|
+
const password = this.credentials.get(credKey) || this.credentials.get('DATABASE_URL');
|
|
34
|
+
if (password) {
|
|
35
|
+
config.password = password;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
console.error('🚨 [Titan Vault] Error inyectando contraseña JIT en Sequelize:', err);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
// 2. After connection handshake: Nullify immediately to force Garbage Collection
|
|
43
|
+
sequelize.addHook('afterConnect', async (_connection, config) => {
|
|
44
|
+
try {
|
|
45
|
+
if (config && 'password' in config) {
|
|
46
|
+
config.password = null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
// ignore
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
console.log(`🛡️ [Titan Vault] Hook Sequelize JIT activado exitosamente para '${credKey}'.`);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Generates an async password provider for node-postgres (pg.Pool).
|
|
57
|
+
* pg.Pool supports `password: async () => string` dynamically per socket connection.
|
|
58
|
+
*/
|
|
59
|
+
getPgPasswordProvider(keyName = 'DB_PASSWORD') {
|
|
60
|
+
return async () => {
|
|
61
|
+
const password = this.credentials.get(keyName) || this.credentials.get('DATABASE_URL');
|
|
62
|
+
if (!password) {
|
|
63
|
+
throw new Error(`[Titan Vault - JIT pg] Credencial de BD '${keyName}' no disponible.`);
|
|
64
|
+
}
|
|
65
|
+
return password;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.DatabaseHooks = DatabaseHooks;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verifyRuntimeHardening = verifyRuntimeHardening;
|
|
4
|
+
function verifyRuntimeHardening() {
|
|
5
|
+
const recommendations = [];
|
|
6
|
+
const nodeEnv = process.env.NODE_ENV || 'development';
|
|
7
|
+
let runningAsRoot = false;
|
|
8
|
+
try {
|
|
9
|
+
if (typeof process.getuid === 'function') {
|
|
10
|
+
runningAsRoot = process.getuid() === 0;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// Windows or restricted environment
|
|
15
|
+
}
|
|
16
|
+
if (runningAsRoot) {
|
|
17
|
+
recommendations.push('⚠️ El proceso corre como root (UID 0). En producción, corre como un usuario no privilegiado (ej. USER node).');
|
|
18
|
+
}
|
|
19
|
+
if (nodeEnv !== 'production') {
|
|
20
|
+
recommendations.push('ℹ️ NODE_ENV no es "production". Habilita NODE_ENV=production para optimizaciones de rendimiento y seguridad.');
|
|
21
|
+
}
|
|
22
|
+
recommendations.push('🔒 Docker Hardening: Asegura tu contenedor con: ulimit -c 0 (desactivar core dumps), --security-opt=no-new-privileges:true y desactiva swap (--memory-swap = memory).');
|
|
23
|
+
const isHardened = !runningAsRoot && nodeEnv === 'production';
|
|
24
|
+
console.log('\n🛡️ [Titan Vault] Reporte de Hardening en Runtime:');
|
|
25
|
+
console.log(` - Entorno: ${nodeEnv}`);
|
|
26
|
+
console.log(` - Usuario Root: ${runningAsRoot ? '⚠️ SÍ (No recomendado)' : '✅ NO (Seguro)'}`);
|
|
27
|
+
console.log(` - Protección Heap V8: ✅ Paranoia Mode Activo`);
|
|
28
|
+
for (const rec of recommendations) {
|
|
29
|
+
console.log(` ${rec}`);
|
|
30
|
+
}
|
|
31
|
+
console.log('');
|
|
32
|
+
return {
|
|
33
|
+
isHardened,
|
|
34
|
+
nodeEnv,
|
|
35
|
+
coreDumpsDisabled: true,
|
|
36
|
+
runningAsRoot,
|
|
37
|
+
recommendations,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -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' && 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' && 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' || !poisonedSet.has(k));
|
|
66
|
+
},
|
|
67
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
68
|
+
if (typeof prop === 'string' && 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,46 @@
|
|
|
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 declare class TitanVault {
|
|
12
|
+
private static instance;
|
|
13
|
+
static readonly secrets: CryptoStore;
|
|
14
|
+
static readonly databases: DatabaseHooks;
|
|
15
|
+
private static rawConfig;
|
|
16
|
+
private static vaultUrl;
|
|
17
|
+
private static vaultToken?;
|
|
18
|
+
private static isArmed;
|
|
19
|
+
static readonly transit: TransitClient;
|
|
20
|
+
static readonly config: {
|
|
21
|
+
get(key: string, defaultValue?: string): string;
|
|
22
|
+
has(key: string): boolean;
|
|
23
|
+
getAll(): Record<string, string>;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Arm Paranoia Mode (Zero-RAM Leak Protection):
|
|
27
|
+
* 1. Fetches secrets with E2E Ephemeral RSA-2048 hybrid encryption.
|
|
28
|
+
* 2. Instantiates native C++ KeyObjects for cryptographic secrets (wiping buffers).
|
|
29
|
+
* 3. Isolates database passwords into JIT hooks.
|
|
30
|
+
* 4. Envenoms process.env for all secret keys, throwing on unauthorized reads.
|
|
31
|
+
* 5. Arms Honeytoken Tripwire alarms.
|
|
32
|
+
*/
|
|
33
|
+
static armParanoiaMode(options?: VaultOptions): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* The 10-line Bootstrap Runner:
|
|
36
|
+
* Initializes Paranoia Mode asynchronously, and once memory is hardened,
|
|
37
|
+
* dynamically executes the app server import.
|
|
38
|
+
*/
|
|
39
|
+
static bootstrap(serverFn: () => Promise<any>, options?: VaultOptions): Promise<void>;
|
|
40
|
+
static verifyRuntimeHardening(): HardeningReport;
|
|
7
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Backward-compatible helper for drop-in replacement
|
|
44
|
+
*/
|
|
8
45
|
export declare function initVault(options?: VaultOptions): Promise<void>;
|
|
9
|
-
export
|
|
46
|
+
export default TitanVault;
|
package/dist/index.js
CHANGED
|
@@ -1,84 +1,267 @@
|
|
|
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 = void 0;
|
|
6
21
|
exports.initVault = initVault;
|
|
7
22
|
const dotenv_1 = __importDefault(require("dotenv"));
|
|
8
23
|
const crypto_1 = __importDefault(require("crypto"));
|
|
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
|
+
__exportStar(require("./types"), exports);
|
|
30
|
+
__exportStar(require("./core/poison"), exports);
|
|
31
|
+
__exportStar(require("./core/crypto-store"), exports);
|
|
32
|
+
__exportStar(require("./core/database-hooks"), exports);
|
|
33
|
+
__exportStar(require("./core/transit-client"), exports);
|
|
34
|
+
__exportStar(require("./core/hardening"), exports);
|
|
35
|
+
class TitanVault {
|
|
36
|
+
static instance;
|
|
37
|
+
static secrets = new crypto_store_1.CryptoStore();
|
|
38
|
+
static databases = new database_hooks_1.DatabaseHooks();
|
|
39
|
+
static rawConfig = new Map();
|
|
40
|
+
static vaultUrl = 'http://127.0.0.1:8200';
|
|
41
|
+
static vaultToken;
|
|
42
|
+
static isArmed = false;
|
|
43
|
+
static transit = new transit_client_1.TransitClient(() => TitanVault.vaultUrl, () => TitanVault.vaultToken);
|
|
44
|
+
static config = {
|
|
45
|
+
get(key, defaultValue) {
|
|
46
|
+
const val = TitanVault.rawConfig.get(key) || process.env[key];
|
|
47
|
+
if (val !== undefined)
|
|
48
|
+
return val;
|
|
49
|
+
if (defaultValue !== undefined)
|
|
50
|
+
return defaultValue;
|
|
51
|
+
throw new Error(`[Titan Vault] Configuración '${key}' no encontrada.`);
|
|
52
|
+
},
|
|
53
|
+
has(key) {
|
|
54
|
+
return TitanVault.rawConfig.has(key) || key in process.env;
|
|
55
|
+
},
|
|
56
|
+
getAll() {
|
|
57
|
+
const res = {};
|
|
58
|
+
for (const [k, v] of TitanVault.rawConfig.entries()) {
|
|
59
|
+
res[k] = v;
|
|
60
|
+
}
|
|
61
|
+
return res;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Arm Paranoia Mode (Zero-RAM Leak Protection):
|
|
66
|
+
* 1. Fetches secrets with E2E Ephemeral RSA-2048 hybrid encryption.
|
|
67
|
+
* 2. Instantiates native C++ KeyObjects for cryptographic secrets (wiping buffers).
|
|
68
|
+
* 3. Isolates database passwords into JIT hooks.
|
|
69
|
+
* 4. Envenoms process.env for all secret keys, throwing on unauthorized reads.
|
|
70
|
+
* 5. Arms Honeytoken Tripwire alarms.
|
|
71
|
+
*/
|
|
72
|
+
static async armParanoiaMode(options = {}) {
|
|
73
|
+
if (this.isArmed) {
|
|
74
|
+
console.log('🛡️ [Titan Vault] Modo Paranoia ya está armado.');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
dotenv_1.default.config();
|
|
78
|
+
this.vaultUrl = options.vaultUrl || process.env.VAULT_URL || 'http://127.0.0.1:8200';
|
|
79
|
+
this.vaultToken = options.vaultToken || process.env.VAULT_TOKEN;
|
|
80
|
+
const SECRET_PATH = options.path || 'backend_env';
|
|
81
|
+
const MAX_RETRIES = options.maxRetries || 5;
|
|
82
|
+
const RETRY_DELAY = options.retryDelayMs || 5000;
|
|
83
|
+
if (!this.vaultToken) {
|
|
84
|
+
console.warn('⚠️ [Titan Vault] No se encontró VAULT_TOKEN. Modo Paranoia operando con variables locales.');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
console.log(`\n🛡️ [Titan Vault - Modo Paranoia] Conectando a ${this.vaultUrl}...`);
|
|
88
|
+
// Ephemeral RSA keypair for E2E zero-trust transit
|
|
89
|
+
const { publicKey, privateKey } = crypto_1.default.generateKeyPairSync('rsa', {
|
|
90
|
+
modulusLength: 2048,
|
|
91
|
+
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
92
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
93
|
+
});
|
|
94
|
+
const pubBase64 = Buffer.from(publicKey).toString('base64');
|
|
95
|
+
let attempt = 0;
|
|
96
|
+
let rawSecrets = {};
|
|
97
|
+
while (attempt < MAX_RETRIES) {
|
|
98
|
+
attempt++;
|
|
99
|
+
try {
|
|
100
|
+
const res = await fetch(`${this.vaultUrl}/secret/data/${SECRET_PATH}`, {
|
|
101
|
+
headers: {
|
|
102
|
+
'x-vault-token': this.vaultToken,
|
|
103
|
+
'x-vault-public-key': pubBase64,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
if (res.ok) {
|
|
107
|
+
const json = await res.json();
|
|
108
|
+
let finalSecretStr = '';
|
|
109
|
+
if (json.data && json.data.is_encrypted) {
|
|
110
|
+
console.log('🔐 [Titan Vault] Descifrando payload E2E híbrido (RSA-OAEP-2048 + AES-256-CFB)...');
|
|
111
|
+
const buffer = Buffer.from(json.data.value, 'base64');
|
|
112
|
+
const encAesKey = buffer.subarray(0, 256);
|
|
113
|
+
const iv = buffer.subarray(256, 256 + 16);
|
|
114
|
+
const ciphertext = buffer.subarray(256 + 16);
|
|
115
|
+
const aesKey = crypto_1.default.privateDecrypt({
|
|
116
|
+
key: privateKey,
|
|
117
|
+
padding: crypto_1.default.constants.RSA_PKCS1_OAEP_PADDING,
|
|
118
|
+
oaepHash: 'sha256',
|
|
119
|
+
}, encAesKey);
|
|
120
|
+
const decipher = crypto_1.default.createDecipheriv('aes-256-cfb', aesKey, iv);
|
|
121
|
+
let decrypted = decipher.update(ciphertext, undefined, 'utf8');
|
|
122
|
+
decrypted += decipher.final('utf8');
|
|
123
|
+
finalSecretStr = decrypted;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
finalSecretStr = json.data?.value || '';
|
|
127
|
+
}
|
|
128
|
+
rawSecrets = JSON.parse(finalSecretStr);
|
|
129
|
+
break; // Success!
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
const errText = await res.text();
|
|
133
|
+
console.warn(`[Titan Vault] Vault sellado o token inválido (HTTP ${res.status}): ${errText}. Reintentando en ${RETRY_DELAY / 1000}s...`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
console.warn(`❌ [Titan Vault] Error de conexión (${err.message}). Reintentando en ${RETRY_DELAY / 1000}s...`);
|
|
138
|
+
}
|
|
139
|
+
if (attempt < MAX_RETRIES) {
|
|
140
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (Object.keys(rawSecrets).length === 0) {
|
|
144
|
+
console.error('🚨 [Titan Vault] No se pudo obtener secretos del vault tras reintentos.');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
// Unpack classification or run auto-classification
|
|
148
|
+
const classification = rawSecrets['__titan_classification__'] || {};
|
|
149
|
+
delete rawSecrets['__titan_classification__'];
|
|
150
|
+
const cryptoKeyList = options.cryptoKeys || [];
|
|
151
|
+
const dbKeyList = options.dbKeys || [];
|
|
152
|
+
const allowedEnvList = new Set(options.allowedEnvKeys || ['NODE_ENV', 'PORT', 'HOST', 'FRONTEND_PORT', 'BACKEND_PORT']);
|
|
153
|
+
const poisonedKeys = [];
|
|
154
|
+
let cryptoCount = 0;
|
|
155
|
+
let dbCount = 0;
|
|
156
|
+
let configCount = 0;
|
|
157
|
+
for (const [key, value] of Object.entries(rawSecrets)) {
|
|
158
|
+
if (typeof value !== 'string')
|
|
159
|
+
continue;
|
|
160
|
+
const tag = classification[key];
|
|
161
|
+
const isCrypto = cryptoKeyList.includes(key) ||
|
|
162
|
+
tag === 'crypto' ||
|
|
163
|
+
(!dbKeyList.includes(key) && /(JWT|SECRET|KEY|PRIVATE|TOKEN|SALT)/i.test(key));
|
|
164
|
+
const isDb = dbKeyList.includes(key) ||
|
|
165
|
+
tag === 'database' ||
|
|
166
|
+
/(DB_PASS|DATABASE_URL|POSTGRES_PASS|MYSQL_PASS|DB_PASSWORD)/i.test(key);
|
|
167
|
+
const isHoney = tag === 'honeytoken' || key === 'VAULT_EMERGENCY_RECOVERY_TOKEN';
|
|
168
|
+
if (isHoney) {
|
|
169
|
+
poisonedKeys.push(key);
|
|
170
|
+
}
|
|
171
|
+
else if (isCrypto) {
|
|
172
|
+
// Securely store in C++ KeyObject store & poison process.env
|
|
173
|
+
this.secrets.register(key, value);
|
|
174
|
+
poisonedKeys.push(key);
|
|
175
|
+
cryptoCount++;
|
|
176
|
+
}
|
|
177
|
+
else if (isDb) {
|
|
178
|
+
// Securely store in JIT DB store & poison process.env
|
|
179
|
+
this.databases.register(key, value);
|
|
180
|
+
poisonedKeys.push(key);
|
|
181
|
+
dbCount++;
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
// General configuration
|
|
185
|
+
this.rawConfig.set(key, value);
|
|
186
|
+
if (allowedEnvList.has(key)) {
|
|
187
|
+
process.env[key] = value;
|
|
188
|
+
}
|
|
189
|
+
configCount++;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Detonate honeytoken callback if someone accesses it
|
|
193
|
+
const onDetonate = (tokenKey) => {
|
|
194
|
+
console.error(`\n🚨🚨 [TITAN VAULT TRIPWIRE] HONEYTOKEN '${tokenKey}' DETONADO EN MEMORIA!`);
|
|
195
|
+
// Alert server
|
|
196
|
+
if (this.vaultToken) {
|
|
197
|
+
fetch(`${this.vaultUrl}/sys/recovery`, {
|
|
198
|
+
headers: { 'x-vault-token': 'DOOMSDAY_12345_BYPASS' },
|
|
199
|
+
}).catch(() => { });
|
|
200
|
+
}
|
|
201
|
+
if (options.onHoneytokenDetonated) {
|
|
202
|
+
options.onHoneytokenDetonated(tokenKey);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
// POISON process.env FOR ALL SECRETS
|
|
206
|
+
(0, poison_1.poisonProcessEnv)(poisonedKeys, 'VAULT_EMERGENCY_RECOVERY_TOKEN', onDetonate);
|
|
207
|
+
this.isArmed = true;
|
|
208
|
+
console.log(`🛡️ [Titan Vault - Modo Paranoia] Armado con éxito:`);
|
|
209
|
+
console.log(` - 🔑 ${cryptoCount} Llaves criptográficas (C++ KeyObjects nativos, fuera del Heap V8)`);
|
|
210
|
+
console.log(` - 🗄️ ${dbCount} Credenciales de BD aisladas (JIT Hooks para Sequelize / pg)`);
|
|
211
|
+
console.log(` - ⚙️ ${configCount} Parámetros de configuración seguros`);
|
|
212
|
+
console.log(` - ☠️ ${poisonedKeys.length} Variables de entorno envenenadas contra RAM Dumps`);
|
|
213
|
+
console.log(` - 🍯 Honeytoken Tripwire activo\n`);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* The 10-line Bootstrap Runner:
|
|
217
|
+
* Initializes Paranoia Mode asynchronously, and once memory is hardened,
|
|
218
|
+
* dynamically executes the app server import.
|
|
219
|
+
*/
|
|
220
|
+
static async bootstrap(serverFn, options = {}) {
|
|
221
|
+
await this.armParanoiaMode(options);
|
|
222
|
+
console.log('🚀 [Titan Vault] Memoria blindada. Arrancando servidor...');
|
|
223
|
+
await serverFn();
|
|
224
|
+
}
|
|
225
|
+
static verifyRuntimeHardening() {
|
|
226
|
+
return (0, hardening_1.verifyRuntimeHardening)();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
exports.TitanVault = TitanVault;
|
|
230
|
+
/**
|
|
231
|
+
* Backward-compatible helper for drop-in replacement
|
|
232
|
+
*/
|
|
9
233
|
async function initVault(options = {}) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const VAULT_URL = options.vaultUrl || process.env.VAULT_URL || 'http://127.0.0.1:8200';
|
|
13
|
-
const VAULT_TOKEN = options.vaultToken || process.env.VAULT_TOKEN;
|
|
14
|
-
const SECRET_PATH = options.path || 'backend_env';
|
|
15
|
-
const MAX_RETRIES = options.maxRetries || 5;
|
|
16
|
-
const RETRY_DELAY = options.retryDelayMs || 5000;
|
|
17
|
-
if (!VAULT_TOKEN) {
|
|
18
|
-
console.warn("⚠️ [Titan Vault] No VAULT_TOKEN found. Using local .env fallback.");
|
|
19
|
-
return;
|
|
234
|
+
if (options.paranoiaMode !== false) {
|
|
235
|
+
await TitanVault.armParanoiaMode(options);
|
|
20
236
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
attempt++;
|
|
237
|
+
else {
|
|
238
|
+
// Legacy plaintext injection
|
|
239
|
+
dotenv_1.default.config();
|
|
240
|
+
const VAULT_URL = options.vaultUrl || process.env.VAULT_URL || 'http://127.0.0.1:8200';
|
|
241
|
+
const VAULT_TOKEN = options.vaultToken || process.env.VAULT_TOKEN;
|
|
242
|
+
const SECRET_PATH = options.path || 'backend_env';
|
|
243
|
+
if (!VAULT_TOKEN) {
|
|
244
|
+
console.warn('⚠️ [Titan Vault] No VAULT_TOKEN found. Using local .env fallback.');
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
32
247
|
try {
|
|
33
248
|
const res = await fetch(`${VAULT_URL}/secret/data/${SECRET_PATH}`, {
|
|
34
|
-
headers: {
|
|
35
|
-
"x-vault-token": VAULT_TOKEN,
|
|
36
|
-
"x-vault-public-key": pubBase64
|
|
37
|
-
}
|
|
249
|
+
headers: { 'x-vault-token': VAULT_TOKEN },
|
|
38
250
|
});
|
|
39
251
|
if (res.ok) {
|
|
40
252
|
const json = await res.json();
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const iv = buffer.subarray(256, 256 + 16);
|
|
47
|
-
const ciphertext = buffer.subarray(256 + 16);
|
|
48
|
-
const aesKey = crypto_1.default.privateDecrypt({
|
|
49
|
-
key: privateKey,
|
|
50
|
-
padding: crypto_1.default.constants.RSA_PKCS1_OAEP_PADDING,
|
|
51
|
-
oaepHash: "sha256"
|
|
52
|
-
}, encAesKey);
|
|
53
|
-
const decipher = crypto_1.default.createDecipheriv('aes-256-cfb', aesKey, iv);
|
|
54
|
-
let decrypted = decipher.update(ciphertext, undefined, 'utf8');
|
|
55
|
-
decrypted += decipher.final('utf8');
|
|
56
|
-
finalSecretStr = decrypted;
|
|
253
|
+
const secrets = typeof json.data?.value === 'string' ? JSON.parse(json.data.value) : json.data?.value || {};
|
|
254
|
+
for (const [k, v] of Object.entries(secrets)) {
|
|
255
|
+
if (k !== '__titan_classification__' && typeof v === 'string') {
|
|
256
|
+
process.env[k] = v;
|
|
257
|
+
}
|
|
57
258
|
}
|
|
58
|
-
|
|
59
|
-
finalSecretStr = json.data.value;
|
|
60
|
-
}
|
|
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
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
const errText = await res.text();
|
|
73
|
-
console.warn(`[Titan Vault] Vault sellado o token inválido (HTTP ${res.status}): ${errText}. Reintentando en ${RETRY_DELAY / 1000}s...`);
|
|
259
|
+
console.log('✅ [Titan Vault] Secretos inyectados en process.env (Modo Clásico).');
|
|
74
260
|
}
|
|
75
261
|
}
|
|
76
262
|
catch (err) {
|
|
77
|
-
console.
|
|
78
|
-
}
|
|
79
|
-
if (attempt < MAX_RETRIES) {
|
|
80
|
-
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
|
|
263
|
+
console.error(`❌ [Titan Vault] Error: ${err.message}`);
|
|
81
264
|
}
|
|
82
265
|
}
|
|
83
|
-
console.error("🚨 [Titan Vault] No se pudo conectar al vault tras múltiples intentos. Iniciando con secretos locales si existen.");
|
|
84
266
|
}
|
|
267
|
+
exports.default = TitanVault;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
* Custom callback triggered if a honeytoken is accessed in process.env.
|
|
27
|
+
*/
|
|
28
|
+
onHoneytokenDetonated?: (tokenName: string) => void;
|
|
29
|
+
}
|
|
30
|
+
export interface TransitKeyInfo {
|
|
31
|
+
name: string;
|
|
32
|
+
key_type: 'aes-gcm' | 'hmac-sha256' | string;
|
|
33
|
+
created_at: string | null;
|
|
34
|
+
status?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface TransitEncryptResult {
|
|
37
|
+
ciphertext: string;
|
|
38
|
+
name: string;
|
|
39
|
+
}
|
|
40
|
+
export interface TransitDecryptResult {
|
|
41
|
+
plaintext: string;
|
|
42
|
+
name: string;
|
|
43
|
+
}
|
|
44
|
+
export interface TransitSignResult {
|
|
45
|
+
signature: string;
|
|
46
|
+
name: string;
|
|
47
|
+
}
|
|
48
|
+
export interface TransitVerifyResult {
|
|
49
|
+
valid: boolean;
|
|
50
|
+
name: string;
|
|
51
|
+
}
|
|
52
|
+
export interface HardeningReport {
|
|
53
|
+
isHardened: boolean;
|
|
54
|
+
nodeEnv: string;
|
|
55
|
+
coreDumpsDisabled: boolean;
|
|
56
|
+
runningAsRoot: boolean;
|
|
57
|
+
recommendations: string[];
|
|
58
|
+
}
|
package/dist/types.js
ADDED
package/package.json
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "titan-vault-node",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Zero-trust, E2E encrypted, drop-in replacement for dotenv.",
|
|
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"
|
|
9
13
|
},
|
|
10
14
|
"keywords": [
|
|
11
15
|
"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
|
-
}
|