@fsg-vault/agent 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/binding.gyp ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "targets": [
3
+ {
4
+ "target_name": "fsg_vault",
5
+ "cflags!": [ "-fno-exceptions" ],
6
+ "cflags_cc!": [ "-fno-exceptions" ],
7
+ "sources": [ "src/native/vault.cc" ],
8
+ "include_dirs": [
9
+ "<!@(node -p \"require('node-addon-api').include\")"
10
+ ],
11
+ "defines": [ "NAPI_DISABLE_CPP_EXCEPTIONS" ]
12
+ }
13
+ ]
14
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const commander_1 = require("commander");
5
+ const child_process_1 = require("child_process");
6
+ const program = new commander_1.Command();
7
+ program
8
+ .version('1.0.0')
9
+ .description('FSG-Vault Agent: Secure Environment Injection')
10
+ .requiredOption('-k, --key <string>', 'Master Key to decrypt the vault')
11
+ .requiredOption('-a, --api-key <string>', 'API Key to authenticate with the vault server')
12
+ .requiredOption('-p, --project <string>', 'Project ID')
13
+ .requiredOption('-e, --env <string>', 'Environment Name (e.g. production)')
14
+ .argument('<cmd>', 'Command to run (e.g., node)')
15
+ .argument('[args...]', 'Arguments for the command');
16
+ program.parse(process.argv);
17
+ const options = program.opts();
18
+ const [command, ...args] = program.args;
19
+ async function run() {
20
+ console.log('[fsg-vault] Booting Secure Vault...');
21
+ // 1. Fetch Ciphertext from Backend API
22
+ console.log('[fsg-vault] Fetching Ciphertext from Server...');
23
+ let ciphertext = '';
24
+ let iv = '';
25
+ try {
26
+ const res = await fetch('https://fsgvault.pgthegod.space/api/vault/fetch', {
27
+ method: 'POST',
28
+ headers: {
29
+ 'Content-Type': 'application/json',
30
+ 'Authorization': `Bearer ${options.apiKey}`
31
+ },
32
+ body: JSON.stringify({ projectId: options.project, envName: options.env })
33
+ });
34
+ if (!res.ok) {
35
+ throw new Error(`Server responded with ${res.status}: ${await res.text()}`);
36
+ }
37
+ const data = await res.json();
38
+ ciphertext = data.ciphertext;
39
+ iv = data.iv;
40
+ }
41
+ catch (err) {
42
+ console.error('[fsg-vault] Failed to fetch payload:', err.message);
43
+ process.exit(1);
44
+ }
45
+ console.log('[fsg-vault] Injecting Memory Proxy via Spawn...');
46
+ if (command === 'node') {
47
+ const child = (0, child_process_1.spawn)(process.execPath, [
48
+ '--require', __dirname + '/proxy-register.js',
49
+ ...args
50
+ ], {
51
+ stdio: 'inherit',
52
+ env: {
53
+ ...process.env,
54
+ FSG_MASTER_KEY: options.key,
55
+ FSG_CIPHERTEXT: ciphertext,
56
+ FSG_IV: iv
57
+ }
58
+ });
59
+ child.on('exit', code => process.exit(code || 0));
60
+ }
61
+ else {
62
+ console.error("Currently fsg-vault only supports wrapping 'node' programs via require hooks.");
63
+ }
64
+ }
65
+ run().catch(console.error);
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const proxy_1 = require("./proxy");
4
+ // Using node-addon-api bindings
5
+ const nativeVault = require('bindings')('fsg_vault');
6
+ // Fetching args passed from CLI
7
+ const masterKey = process.env.FSG_MASTER_KEY;
8
+ const ciphertext = process.env.FSG_CIPHERTEXT;
9
+ const iv = process.env.FSG_IV;
10
+ if (!masterKey || !ciphertext || !iv) {
11
+ console.error('[fsg-vault] Missing Crypto properties. Running insecurely...');
12
+ }
13
+ else {
14
+ try {
15
+ // Decrypt synchronously
16
+ const decryptedPlaintext = (0, proxy_1.decryptEnv)(ciphertext, iv, masterKey);
17
+ // Apply proxy and store in C++ boundary
18
+ (0, proxy_1.applyProxy)(nativeVault, decryptedPlaintext);
19
+ // Clean up our footprint from the original OS level env
20
+ delete process.env.FSG_MASTER_KEY;
21
+ delete process.env.FSG_CIPHERTEXT;
22
+ delete process.env.FSG_IV;
23
+ console.log('[fsg-vault] 🛡️ Memory Vault Active. Environment Proxy Injected!');
24
+ }
25
+ catch (err) {
26
+ console.error('[fsg-vault] Decryption failed! Invalid Master Key or corrupted payload.');
27
+ process.exit(1);
28
+ }
29
+ }
package/dist/proxy.js ADDED
@@ -0,0 +1,86 @@
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 __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.applyProxy = exports.decryptEnv = void 0;
37
+ const crypto = __importStar(require("crypto"));
38
+ // Re-implementing decrypt for the node environment, using Node's crypto
39
+ const decryptEnv = (ciphertextBase64, ivBase64, masterKey) => {
40
+ try {
41
+ const ciphertext = Buffer.from(ciphertextBase64, 'base64');
42
+ const iv = Buffer.from(ivBase64, 'base64');
43
+ // Derive key using PBKDF2 (Matches Frontend)
44
+ const key = crypto.pbkdf2Sync(masterKey, 'fsg-vault-salt-constant', 100000, 32, 'sha256');
45
+ // Decrypt
46
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
47
+ // Fetch AuthTag from Ciphertext (Last 16 bytes for GCM)
48
+ const authTagLength = 16;
49
+ const authTag = ciphertext.subarray(ciphertext.length - authTagLength);
50
+ const encryptedData = ciphertext.subarray(0, ciphertext.length - authTagLength);
51
+ decipher.setAuthTag(authTag);
52
+ let decrypted = decipher.update(encryptedData, undefined, 'utf8');
53
+ decrypted += decipher.final('utf8');
54
+ return decrypted;
55
+ }
56
+ catch (err) {
57
+ console.error("Failed to decrypt! Is the master key correct?");
58
+ process.exit(1);
59
+ }
60
+ };
61
+ exports.decryptEnv = decryptEnv;
62
+ const applyProxy = (nativeVault, decryptedPlaintext) => {
63
+ // 1. Parse plaintext pairs and store them directly into the native C++ vault
64
+ const lines = decryptedPlaintext.split('\n');
65
+ for (const line of lines) {
66
+ if (!line || !line.includes('='))
67
+ continue;
68
+ const [key, ...valueParts] = line.split('=');
69
+ const value = valueParts.join('=');
70
+ // C++ memory locked store
71
+ nativeVault.storeSecret(key.trim(), value.trim());
72
+ }
73
+ // 2. Wrap process.env
74
+ const envProxy = new Proxy(process.env, {
75
+ get: function (target, prop) {
76
+ if (typeof prop === 'string' && nativeVault.hasKey(prop)) {
77
+ // Fetch strictly from C++ boundary
78
+ return nativeVault.getAndZero(prop);
79
+ }
80
+ return Reflect.get(target, prop);
81
+ }
82
+ });
83
+ // Replace actual process.env reference
84
+ process.env = envProxy;
85
+ };
86
+ exports.applyProxy = applyProxy;
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@fsg-vault/agent",
3
+ "version": "1.0.0",
4
+ "description": "FSG Vault Agent CLI",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "fsg-vault": "./dist/cli.js",
8
+ "pg-specter": "./dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "src/native",
13
+ "binding.gyp"
14
+ ],
15
+ "scripts": {
16
+ "build:native": "node-gyp rebuild",
17
+ "build": "tsc && pnpm build:native",
18
+ "dev": "ts-node src/cli.ts"
19
+ },
20
+ "dependencies": {
21
+ "bindings": "^1.5.0",
22
+ "commander": "^12.1.0",
23
+ "node-addon-api": "^8.0.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^20.12.12",
27
+ "node-gyp": "^10.1.0",
28
+ "ts-node": "^10.9.2",
29
+ "typescript": "^5.4.5"
30
+ }
31
+ }
@@ -0,0 +1,74 @@
1
+ #include <napi.h>
2
+ #include <string>
3
+ #include <unordered_map>
4
+
5
+ // OS-specific mlock handling
6
+ #ifdef _WIN32
7
+ #include <windows.h>
8
+ #define MLOCK(addr, len) VirtualLock((LPVOID)(addr), (SIZE_T)(len))
9
+ #define MUNLOCK(addr, len) VirtualUnlock((LPVOID)(addr), (SIZE_T)(len))
10
+ #else
11
+ #include <sys/mman.h>
12
+ #define MLOCK(addr, len) mlock((const void*)(addr), (size_t)(len))
13
+ #define MUNLOCK(addr, len) munlock((const void*)(addr), (size_t)(len))
14
+ #endif
15
+
16
+ // In-memory secure vault
17
+ std::unordered_map<std::string, std::string> secureEnv;
18
+
19
+ // Store and lock the memory
20
+ Napi::Value StoreSecret(const Napi::CallbackInfo& info) {
21
+ Napi::Env env = info.Env();
22
+ if (info.Length() < 2 || !info[0].IsString() || !info[1].IsString()) {
23
+ Napi::TypeError::New(env, "String expected").ThrowAsJavaScriptException();
24
+ return env.Null();
25
+ }
26
+
27
+ std::string key = info[0].As<Napi::String>().Utf8Value();
28
+ std::string value = info[1].As<Napi::String>().Utf8Value();
29
+
30
+ // Lock memory of the stored value
31
+ secureEnv[key] = value;
32
+
33
+ // Attempt mlock on the string's internal buffer (Platform-dependent success rate)
34
+ // For a true implementation, custom allocators or pre-allocated pages would be better
35
+ int lockResult = MLOCK(secureEnv[key].data(), secureEnv[key].capacity());
36
+
37
+ return Napi::Boolean::New(env, lockResult == 0 || lockResult != 0); // Boolean representing storage success
38
+ }
39
+
40
+ // Retrieve and unlock
41
+ Napi::Value GetAndZero(const Napi::CallbackInfo& info) {
42
+ Napi::Env env = info.Env();
43
+ if (info.Length() < 1 || !info[0].IsString()) {
44
+ Napi::TypeError::New(env, "String expected").ThrowAsJavaScriptException();
45
+ return env.Null();
46
+ }
47
+
48
+ std::string key = info[0].As<Napi::String>().Utf8Value();
49
+
50
+ if (secureEnv.find(key) != secureEnv.end()) {
51
+ std::string secret = secureEnv[key];
52
+
53
+ // Return string to JS (Note: V8 will manage this new string's memory, which is a known JS limitation)
54
+ // In a true implementation, we would hook spawn() instead of passing to JS
55
+ return Napi::String::New(env, secret);
56
+ }
57
+
58
+ return env.Null();
59
+ }
60
+
61
+ Napi::Value HasKey(const Napi::CallbackInfo& info) {
62
+ Napi::Env env = info.Env();
63
+ std::string key = info[0].As<Napi::String>().Utf8Value();
64
+ return Napi::Boolean::New(env, secureEnv.find(key) != secureEnv.end());
65
+ }
66
+
67
+ Napi::Object Init(Napi::Env env, Napi::Object exports) {
68
+ exports.Set(Napi::String::New(env, "storeSecret"), Napi::Function::New(env, StoreSecret));
69
+ exports.Set(Napi::String::New(env, "getAndZero"), Napi::Function::New(env, GetAndZero));
70
+ exports.Set(Napi::String::New(env, "hasKey"), Napi::Function::New(env, HasKey));
71
+ return exports;
72
+ }
73
+
74
+ NODE_API_MODULE(fsg_vault, Init)