titan-vault-node 0.0.0-stage → 1.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 +65 -2
- package/dist/index.d.ts +9 -0
- package/dist/index.js +84 -0
- package/package.json +25 -4
- package/src/index.ts +101 -0
- package/tsconfig.json +14 -0
package/README.md
CHANGED
|
@@ -1,3 +1,66 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 🛡️ Titan Vault (Node.js SDK)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Zero-trust, E2E encrypted, drop-in replacement for dotenv.**
|
|
4
|
+
|
|
5
|
+
Titan Vault is the easiest way to secure your application secrets. Forget about hardcoded `.env` files that get leaked. Titan Vault acts as a highly secure, encrypted environment variable injector that requires **zero friction** to use.
|
|
6
|
+
|
|
7
|
+
## 🚀 The Problem with `.env`
|
|
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.
|
|
9
|
+
|
|
10
|
+
## 💡 The Titan Vault Solution
|
|
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.
|
|
12
|
+
|
|
13
|
+
### 🔥 Killer Features
|
|
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.
|
|
18
|
+
|
|
19
|
+
## 📦 Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install titan-vault-node
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 💻 Usage
|
|
26
|
+
|
|
27
|
+
Replace your standard `dotenv.config()` with `initVault()`.
|
|
28
|
+
|
|
29
|
+
### 1. Set your App Token
|
|
30
|
+
In your deployment environment (or a local `.env`), just provide your vault's URL and your App Token:
|
|
31
|
+
|
|
32
|
+
```env
|
|
33
|
+
VAULT_URL=http://localhost:8200
|
|
34
|
+
VAULT_APP_TOKEN=my-super-secret-app-role-token
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. Initialize in your code
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { initVault } from 'titan-vault-node';
|
|
41
|
+
|
|
42
|
+
async function startServer() {
|
|
43
|
+
// Magically connects to Titan Vault, decrypts the E2E payload,
|
|
44
|
+
// and injects everything into process.env!
|
|
45
|
+
await initVault();
|
|
46
|
+
|
|
47
|
+
console.log("My secure DB password is:", process.env.DB_PASSWORD);
|
|
48
|
+
|
|
49
|
+
// Start your Express/NestJS/HTTP server...
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
startServer();
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 🚨 Telegram Webhook Setup (Server-side)
|
|
56
|
+
Want to be notified if someone touches your Honeytoken? Run your Titan Vault server with Telegram variables:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
export TELEGRAM_BOT_TOKEN="your_bot_token"
|
|
60
|
+
export TELEGRAM_CHAT_ID="your_chat_id"
|
|
61
|
+
# Start your vault...
|
|
62
|
+
```
|
|
63
|
+
If your Honeytoken is triggered, the Vault seals itself and sends a red alert to your phone instantly.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
**Built for the paranoid, designed for the developer.**
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
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.initVault = initVault;
|
|
7
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
8
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
9
|
+
async function initVault(options = {}) {
|
|
10
|
+
// Load local .env first to get base configs (like VAULT_URL or VAULT_TOKEN)
|
|
11
|
+
dotenv_1.default.config();
|
|
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;
|
|
20
|
+
}
|
|
21
|
+
console.log(`\n🔒 [Titan Vault] Conectando a ${VAULT_URL}...`);
|
|
22
|
+
// E2E Encryption: Generate ephemeral RSA keypair
|
|
23
|
+
const { publicKey, privateKey } = crypto_1.default.generateKeyPairSync('rsa', {
|
|
24
|
+
modulusLength: 2048,
|
|
25
|
+
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
26
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
|
|
27
|
+
});
|
|
28
|
+
const pubBase64 = Buffer.from(publicKey).toString('base64');
|
|
29
|
+
let attempt = 0;
|
|
30
|
+
while (attempt < MAX_RETRIES) {
|
|
31
|
+
attempt++;
|
|
32
|
+
try {
|
|
33
|
+
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
|
+
}
|
|
38
|
+
});
|
|
39
|
+
if (res.ok) {
|
|
40
|
+
const json = await res.json();
|
|
41
|
+
let finalSecretStr = "";
|
|
42
|
+
if (json.data && json.data.is_encrypted) {
|
|
43
|
+
console.log("🔐 [Titan Vault] Descifrando payload E2E híbrido (RSA-OAEP + AES-CFB)...");
|
|
44
|
+
const buffer = Buffer.from(json.data.value, 'base64');
|
|
45
|
+
const encAesKey = buffer.subarray(0, 256);
|
|
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;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
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...`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
console.warn(`❌ [Titan Vault] Error de conexión (${err.message}). Reintentando en ${RETRY_DELAY / 1000}s...`);
|
|
78
|
+
}
|
|
79
|
+
if (attempt < MAX_RETRIES) {
|
|
80
|
+
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
console.error("🚨 [Titan Vault] No se pudo conectar al vault tras múltiples intentos. Iniciando con secretos locales si existen.");
|
|
84
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "titan-vault-node",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Zero-trust, E2E encrypted, drop-in replacement for dotenv.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"vault",
|
|
12
|
+
"security",
|
|
13
|
+
"dotenv",
|
|
14
|
+
"secrets",
|
|
15
|
+
"zero-trust",
|
|
16
|
+
"e2e"
|
|
17
|
+
],
|
|
18
|
+
"author": "Titan Vault Community",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"dotenv": "^16.4.5"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"typescript": "^5.6.3",
|
|
25
|
+
"@types/node": "^22.0.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
}
|