@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 +14 -0
- package/dist/cli.js +65 -0
- package/dist/proxy-register.js +29 -0
- package/dist/proxy.js +86 -0
- package/package.json +31 -0
- package/src/native/vault.cc +74 -0
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)
|