wolverine-ai 3.7.9 → 3.8.1
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/package.json +1 -1
- package/src/backup/backup-manager.js +15 -0
- package/src/brain/brain.js +4 -0
- package/src/core/runner.js +17 -0
- package/src/notifications/notifier.js +2 -2
- package/src/security/injection-detector.js +3 -0
- package/src/security/sandbox.js +24 -1
- package/src/security/secret-redactor.js +5 -0
- package/src/skills/vault.js +69 -0
- package/src/vault/vault-manager.js +185 -0
- package/src/vault/wallet-ops.js +167 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wolverine-ai",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.1",
|
|
4
4
|
"description": "Self-healing Node.js server framework powered by AI. Catches crashes, diagnoses errors, generates fixes, verifies, and restarts — automatically.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -32,6 +32,8 @@ const NEVER_ROLLBACK = [
|
|
|
32
32
|
"server/lib/redis.js",
|
|
33
33
|
".env",
|
|
34
34
|
".env.local",
|
|
35
|
+
".wolverine/vault/master.key",
|
|
36
|
+
".wolverine/vault/eth.vault",
|
|
35
37
|
];
|
|
36
38
|
|
|
37
39
|
class BackupManager {
|
|
@@ -78,6 +80,19 @@ class BackupManager {
|
|
|
78
80
|
});
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
// Also back up vault keys (catastrophic recovery)
|
|
84
|
+
try {
|
|
85
|
+
const vaultDir = path.join(this.projectRoot, ".wolverine", "vault");
|
|
86
|
+
for (const vaultFile of ["master.key", "eth.vault"]) {
|
|
87
|
+
const src = path.join(vaultDir, vaultFile);
|
|
88
|
+
if (fs.existsSync(src)) {
|
|
89
|
+
const dest = path.join(backupDir, ".wolverine__vault__" + vaultFile);
|
|
90
|
+
fs.copyFileSync(src, dest);
|
|
91
|
+
files.push({ original: src, relative: `.wolverine/vault/${vaultFile}`, backup: dest });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch {}
|
|
95
|
+
|
|
81
96
|
const entry = {
|
|
82
97
|
id: backupId,
|
|
83
98
|
timestamp,
|
package/src/brain/brain.js
CHANGED
|
@@ -177,6 +177,10 @@ const SEED_DOCS = [
|
|
|
177
177
|
text: "Notifications: detects human-required errors (expired keys, billing, service down, certs, permissions, disk). Classifies errors as AI-fixable vs human-required using pattern matching. Generates AI summary (CHAT_MODEL). Fires before wasting tokens on repair. Console alert + dashboard event + optional webhook. Categories: auth, billing, service, cert, permission, disk.",
|
|
178
178
|
metadata: { topic: "notifications" },
|
|
179
179
|
},
|
|
180
|
+
{
|
|
181
|
+
text: "Vault: encrypted key storage in .wolverine/vault/. AES-256-GCM encryption. master.key (32 bytes raw) encrypts eth.vault (Ethereum private key). Generated on first run if missing. Private key NEVER exists as a JS string — only Buffer, wiped after use. wallet-ops.js exposes getWalletAddress(), signTransaction(), signMessage() — all decrypt→use→wipe with generic error messages only. Injection detector blocks heal if 0x+64hex chars detected in error (key_leak_critical). Redactor scrubs all hex key patterns. Sandbox blocks agent access to vault paths. Backed up in every snapshot. Rollback-protected (NEVER_ROLLBACK list). Vault skill in src/skills/vault.js for agent discovery. Server code uses vault skill to earn/spend ETH without touching the private key.",
|
|
182
|
+
metadata: { topic: "vault-security" },
|
|
183
|
+
},
|
|
180
184
|
{
|
|
181
185
|
text: "MCP integration: connect external tools via Model Context Protocol. Configure in .wolverine/mcp.json with per-server tool allowlists. Security: arg sanitization (secrets redacted before sending to MCP servers), result injection scanning, rate limiting per server, audit logging. Tools appear as mcp__server__tool in the agent. Supports stdio and HTTP transports.",
|
|
182
186
|
metadata: { topic: "mcp" },
|
package/src/core/runner.js
CHANGED
|
@@ -194,6 +194,23 @@ class WolverineRunner {
|
|
|
194
194
|
console.log(chalk.yellow(` ⚠️ MCP init failed (non-fatal): ${err.message}`));
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
// Initialize vault (encrypted key storage)
|
|
198
|
+
try {
|
|
199
|
+
const { initVault, isVaultInitialized } = require("../vault/vault-manager");
|
|
200
|
+
const vaultResult = await initVault();
|
|
201
|
+
if (vaultResult.created) {
|
|
202
|
+
try {
|
|
203
|
+
const { getWalletAddress } = require("../vault/wallet-ops");
|
|
204
|
+
const addr = await getWalletAddress();
|
|
205
|
+
console.log(chalk.green(` 🔐 Vault initialized — wallet: ${addr}`));
|
|
206
|
+
} catch { console.log(chalk.green(" 🔐 Vault initialized")); }
|
|
207
|
+
} else if (isVaultInitialized()) {
|
|
208
|
+
console.log(chalk.gray(" 🔐 Vault: ready"));
|
|
209
|
+
}
|
|
210
|
+
} catch (err) {
|
|
211
|
+
console.log(chalk.yellow(` ⚠️ Vault init failed (non-fatal): ${err.message}`));
|
|
212
|
+
}
|
|
213
|
+
|
|
197
214
|
// Log redactor stats
|
|
198
215
|
const redactorStats = this.redactor.getStats();
|
|
199
216
|
console.log(chalk.gray(` 🔐 Secret redactor: ${redactorStats.trackedSecrets} secrets tracked from ${redactorStats.envFiles} env file(s)`));
|
|
@@ -57,9 +57,9 @@ const HUMAN_REQUIRED_PATTERNS = [
|
|
|
57
57
|
{ pattern: /EACCES/i, category: "permission", hint: "Permission denied on file system" },
|
|
58
58
|
{ pattern: /EPERM/i, category: "permission", hint: "Operation not permitted — check file/process permissions" },
|
|
59
59
|
|
|
60
|
-
// Environment
|
|
60
|
+
// Environment — only match system-level env issues, not app config files the agent can create
|
|
61
61
|
{ pattern: /not\s+set|undefined.*env|missing.*env/i, category: "env", hint: "Environment variable not configured" },
|
|
62
|
-
{ pattern: /missing.*
|
|
62
|
+
{ pattern: /missing.*(\.env|environment|env\s*var)/i, category: "env", hint: "Environment variable or .env file missing" },
|
|
63
63
|
|
|
64
64
|
// Disk
|
|
65
65
|
{ pattern: /ENOSPC/i, category: "disk", hint: "Disk space full" },
|
|
@@ -41,6 +41,9 @@ const INJECTION_PATTERNS = [
|
|
|
41
41
|
{ pattern: /fs\.(unlink|rmdir|rm)Sync/i, label: "destructive-fs" },
|
|
42
42
|
{ pattern: /rimraf/i, label: "destructive-fs" },
|
|
43
43
|
{ pattern: /rm\s+-rf/i, label: "destructive-fs" },
|
|
44
|
+
// Vault key material leak — CRITICAL: block heal entirely
|
|
45
|
+
{ pattern: /0x[0-9a-fA-F]{64}/i, label: "key-leak-critical" },
|
|
46
|
+
{ pattern: /master\.key|eth\.vault|\.wolverine\/vault/i, label: "vault-path-leak" },
|
|
44
47
|
];
|
|
45
48
|
|
|
46
49
|
/**
|
package/src/security/sandbox.js
CHANGED
|
@@ -12,11 +12,27 @@ class Sandbox {
|
|
|
12
12
|
this.rootDir = path.resolve(rootDir);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
// Paths the agent can NEVER read, write, or delete — even within the sandbox
|
|
16
|
+
static PROTECTED_PATHS = [
|
|
17
|
+
".wolverine/vault/master.key",
|
|
18
|
+
".wolverine/vault/eth.vault",
|
|
19
|
+
".wolverine/vault",
|
|
20
|
+
];
|
|
21
|
+
|
|
15
22
|
/**
|
|
16
|
-
* Resolve and validate a file path. Throws if the path escapes the sandbox
|
|
23
|
+
* Resolve and validate a file path. Throws if the path escapes the sandbox
|
|
24
|
+
* or targets a protected vault path.
|
|
17
25
|
* Returns the resolved absolute path.
|
|
18
26
|
*/
|
|
19
27
|
resolve(filePath) {
|
|
28
|
+
// Block vault access before any resolution
|
|
29
|
+
const normalized = filePath.replace(/\\/g, "/").toLowerCase();
|
|
30
|
+
for (const p of Sandbox.PROTECTED_PATHS) {
|
|
31
|
+
if (normalized.includes(p.toLowerCase())) {
|
|
32
|
+
throw new SandboxViolationError(`Access denied: "${filePath}" is a protected vault path`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
20
36
|
// Resolve relative to sandbox root
|
|
21
37
|
const resolved = path.isAbsolute(filePath)
|
|
22
38
|
? path.resolve(filePath)
|
|
@@ -32,6 +48,13 @@ class Sandbox {
|
|
|
32
48
|
);
|
|
33
49
|
}
|
|
34
50
|
|
|
51
|
+
// Double-check resolved path against vault
|
|
52
|
+
for (const p of Sandbox.PROTECTED_PATHS) {
|
|
53
|
+
if (normalizedResolved.includes(p.toLowerCase())) {
|
|
54
|
+
throw new SandboxViolationError(`Access denied: resolved path targets protected vault`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
35
58
|
// Check for symlink escape
|
|
36
59
|
if (fs.existsSync(resolved)) {
|
|
37
60
|
const real = fs.realpathSync(resolved);
|
|
@@ -40,6 +40,11 @@ const SECRET_PATTERNS = [
|
|
|
40
40
|
{ pattern: /(?:password|secret|token|key|credential|auth)['"`]?\s*[:=]\s*['"`]([a-zA-Z0-9+/=_-]{32,})['"`]/gi, label: null }, // handled specially
|
|
41
41
|
// JWT tokens
|
|
42
42
|
{ pattern: /eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/g, label: "[REDACTED_JWT]" },
|
|
43
|
+
// Vault: Ethereum private keys and encryption key material
|
|
44
|
+
{ pattern: /0x[0-9a-fA-F]{64}/g, label: "[REDACTED_PRIVATE_KEY]" },
|
|
45
|
+
{ pattern: /(?<![0-9a-fA-F])[0-9a-fA-F]{64}(?![0-9a-fA-F])/g, label: "[REDACTED_HEX_KEY]" },
|
|
46
|
+
// Vault file paths — prevent path disclosure that could guide file reads
|
|
47
|
+
{ pattern: /master\.key|eth\.vault|\.wolverine\/vault/g, label: "[VAULT_REDACTED]" },
|
|
43
48
|
];
|
|
44
49
|
|
|
45
50
|
class SecretRedactor {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vault Skill — secure wallet operations for the agent and server.
|
|
3
|
+
*
|
|
4
|
+
* Exposes wallet functionality (sign, address, status) through the
|
|
5
|
+
* skill registry so agents and server code can use the wallet WITHOUT
|
|
6
|
+
* ever touching the private key directly.
|
|
7
|
+
*
|
|
8
|
+
* The private key is decrypted in-memory, used, and wiped. Error messages
|
|
9
|
+
* are always generic to prevent key leakage through the AI repair pipeline.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const SKILL_NAME = "vault";
|
|
13
|
+
const SKILL_DESCRIPTION = "Secure Ethereum wallet — sign transactions, get address, check balance. Private key encrypted at rest, never exposed in code or errors.";
|
|
14
|
+
const SKILL_KEYWORDS = ["wallet", "ethereum", "eth", "sign", "transaction", "vault", "private key", "address", "crypto", "blockchain", "send", "transfer"];
|
|
15
|
+
const SKILL_USAGE = `
|
|
16
|
+
vault.status() — check if vault is initialized, show address
|
|
17
|
+
vault.address() — get the wallet's Ethereum address
|
|
18
|
+
vault.sign_tx(tx) — sign a transaction { to, value, chainId, nonce, gasLimit }
|
|
19
|
+
vault.sign_message(msg) — sign an arbitrary message
|
|
20
|
+
vault.public_key() — get compressed public key hex
|
|
21
|
+
|
|
22
|
+
SECURITY: The private key never leaves the vault. All operations decrypt
|
|
23
|
+
in-memory, use, and wipe. Error messages are generic — no key material
|
|
24
|
+
can leak into the AI repair pipeline.
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Execute a vault action. Called by skill registry when matched.
|
|
29
|
+
* @param {string} action — "status" | "address" | "sign_tx" | "sign_message" | "public_key"
|
|
30
|
+
* @param {object} params — action-specific parameters
|
|
31
|
+
*/
|
|
32
|
+
async function execute(action, params = {}) {
|
|
33
|
+
const { getWalletAddress, getPublicKeyHex, signMessage, signTransaction, getVaultStatus } = require("../vault/wallet-ops");
|
|
34
|
+
|
|
35
|
+
switch (action) {
|
|
36
|
+
case "status": {
|
|
37
|
+
const status = getVaultStatus();
|
|
38
|
+
if (status.initialized) {
|
|
39
|
+
try {
|
|
40
|
+
status.address = await getWalletAddress();
|
|
41
|
+
} catch {
|
|
42
|
+
status.address = null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return status;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
case "address":
|
|
49
|
+
return { address: await getWalletAddress() };
|
|
50
|
+
|
|
51
|
+
case "public_key":
|
|
52
|
+
return { publicKey: await getPublicKeyHex() };
|
|
53
|
+
|
|
54
|
+
case "sign_message":
|
|
55
|
+
if (!params.message) return { error: "message required" };
|
|
56
|
+
return { signature: await signMessage(params.message) };
|
|
57
|
+
|
|
58
|
+
case "sign_tx": {
|
|
59
|
+
if (!params.to || !params.chainId) return { error: "to and chainId required" };
|
|
60
|
+
const signed = await signTransaction(params);
|
|
61
|
+
return { signedTransaction: signed };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
default:
|
|
65
|
+
return { error: `Unknown vault action: ${action}. Use: status, address, sign_tx, sign_message, public_key` };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { SKILL_NAME, SKILL_DESCRIPTION, SKILL_KEYWORDS, SKILL_USAGE, execute };
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Vault Manager — encrypted key storage for the wolverine framework.
|
|
7
|
+
*
|
|
8
|
+
* Two files in .wolverine/vault/:
|
|
9
|
+
* master.key — 32 bytes raw AES-256 key (chmod 0600)
|
|
10
|
+
* eth.vault — JSON with AES-256-GCM encrypted Ethereum private key
|
|
11
|
+
*
|
|
12
|
+
* Design principles:
|
|
13
|
+
* - Private keys NEVER exist as JavaScript strings (only Buffers, wipe-able)
|
|
14
|
+
* - Generic errors only — wallet-ops swallows details before they reach AI
|
|
15
|
+
* - master.key is the single secret on disk — everything else is encrypted
|
|
16
|
+
* - Survives git pull, npm install, auto-update (lives in .wolverine/)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const VAULT_DIR = () => path.join(process.cwd(), ".wolverine", "vault");
|
|
20
|
+
const MASTER_KEY_PATH = () => path.join(VAULT_DIR(), "master.key");
|
|
21
|
+
const ETH_VAULT_PATH = () => path.join(VAULT_DIR(), "eth.vault");
|
|
22
|
+
|
|
23
|
+
const ALGORITHM = "aes-256-gcm";
|
|
24
|
+
const IV_LENGTH = 16;
|
|
25
|
+
const AUTH_TAG_LENGTH = 16;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Initialize the vault. Idempotent — creates keys only if missing.
|
|
29
|
+
* Called during runner startup before any server code runs.
|
|
30
|
+
*/
|
|
31
|
+
async function initVault() {
|
|
32
|
+
const vaultDir = VAULT_DIR();
|
|
33
|
+
fs.mkdirSync(vaultDir, { recursive: true });
|
|
34
|
+
|
|
35
|
+
let created = false;
|
|
36
|
+
|
|
37
|
+
// Master encryption key
|
|
38
|
+
if (!fs.existsSync(MASTER_KEY_PATH())) {
|
|
39
|
+
const masterKey = crypto.randomBytes(32);
|
|
40
|
+
fs.writeFileSync(MASTER_KEY_PATH(), masterKey);
|
|
41
|
+
try { fs.chmodSync(MASTER_KEY_PATH(), 0o600); } catch {}
|
|
42
|
+
masterKey.fill(0);
|
|
43
|
+
created = true;
|
|
44
|
+
console.log(" 🔐 Vault: master encryption key generated");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Ethereum private key (encrypted)
|
|
48
|
+
if (!fs.existsSync(ETH_VAULT_PATH())) {
|
|
49
|
+
const ethKey = crypto.randomBytes(32);
|
|
50
|
+
await encryptAndStore(ethKey);
|
|
51
|
+
ethKey.fill(0);
|
|
52
|
+
created = true;
|
|
53
|
+
console.log(" 🔐 Vault: ethereum wallet created");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { created };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if vault is fully initialized.
|
|
61
|
+
*/
|
|
62
|
+
function isVaultInitialized() {
|
|
63
|
+
return fs.existsSync(MASTER_KEY_PATH()) && fs.existsSync(ETH_VAULT_PATH());
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Encrypt a private key Buffer and write to eth.vault.
|
|
68
|
+
* Wipes the master key from memory after use.
|
|
69
|
+
*/
|
|
70
|
+
async function encryptAndStore(keyBuf) {
|
|
71
|
+
let masterKey = null;
|
|
72
|
+
try {
|
|
73
|
+
masterKey = fs.readFileSync(MASTER_KEY_PATH());
|
|
74
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
75
|
+
const cipher = crypto.createCipheriv(ALGORITHM, masterKey, iv);
|
|
76
|
+
const encrypted = Buffer.concat([cipher.update(keyBuf), cipher.final()]);
|
|
77
|
+
const authTag = cipher.getAuthTag();
|
|
78
|
+
|
|
79
|
+
const vault = {
|
|
80
|
+
version: 1,
|
|
81
|
+
algorithm: ALGORITHM,
|
|
82
|
+
iv: iv.toString("hex"),
|
|
83
|
+
authTag: authTag.toString("hex"),
|
|
84
|
+
ciphertext: encrypted.toString("hex"),
|
|
85
|
+
created: new Date().toISOString(),
|
|
86
|
+
rotated: null,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const tmpPath = ETH_VAULT_PATH() + ".tmp";
|
|
90
|
+
fs.writeFileSync(tmpPath, JSON.stringify(vault, null, 2), "utf-8");
|
|
91
|
+
fs.renameSync(tmpPath, ETH_VAULT_PATH());
|
|
92
|
+
try { fs.chmodSync(ETH_VAULT_PATH(), 0o600); } catch {}
|
|
93
|
+
} finally {
|
|
94
|
+
if (masterKey) masterKey.fill(0);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Decrypt the Ethereum private key. Returns a Buffer.
|
|
100
|
+
* CALLER MUST call .fill(0) on the returned Buffer when done.
|
|
101
|
+
*/
|
|
102
|
+
function decryptPrivateKey() {
|
|
103
|
+
if (!isVaultInitialized()) {
|
|
104
|
+
throw new Error("vault not initialized");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let masterKey = null;
|
|
108
|
+
try {
|
|
109
|
+
masterKey = fs.readFileSync(MASTER_KEY_PATH());
|
|
110
|
+
const vault = JSON.parse(fs.readFileSync(ETH_VAULT_PATH(), "utf-8"));
|
|
111
|
+
|
|
112
|
+
if (vault.version !== 1) throw new Error("unsupported vault version");
|
|
113
|
+
|
|
114
|
+
const iv = Buffer.from(vault.iv, "hex");
|
|
115
|
+
const authTag = Buffer.from(vault.authTag, "hex");
|
|
116
|
+
const ciphertext = Buffer.from(vault.ciphertext, "hex");
|
|
117
|
+
|
|
118
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, masterKey, iv);
|
|
119
|
+
decipher.setAuthTag(authTag);
|
|
120
|
+
|
|
121
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
122
|
+
} finally {
|
|
123
|
+
if (masterKey) masterKey.fill(0);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Re-encrypt with a fresh IV. Defensive measure if key material was
|
|
129
|
+
* potentially exposed in an error message.
|
|
130
|
+
*/
|
|
131
|
+
async function rotateEncryption() {
|
|
132
|
+
let keyBuf = null;
|
|
133
|
+
try {
|
|
134
|
+
keyBuf = decryptPrivateKey();
|
|
135
|
+
await encryptAndStore(keyBuf);
|
|
136
|
+
|
|
137
|
+
// Update rotated timestamp
|
|
138
|
+
const vault = JSON.parse(fs.readFileSync(ETH_VAULT_PATH(), "utf-8"));
|
|
139
|
+
vault.rotated = new Date().toISOString();
|
|
140
|
+
fs.writeFileSync(ETH_VAULT_PATH(), JSON.stringify(vault, null, 2), "utf-8");
|
|
141
|
+
} finally {
|
|
142
|
+
if (keyBuf) keyBuf.fill(0);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Export vault contents for backup. Returns raw Buffers.
|
|
148
|
+
* Caller MUST wipe masterKey after writing to backup.
|
|
149
|
+
*/
|
|
150
|
+
function exportVaultForBackup() {
|
|
151
|
+
if (!isVaultInitialized()) return null;
|
|
152
|
+
return {
|
|
153
|
+
masterKey: fs.readFileSync(MASTER_KEY_PATH()),
|
|
154
|
+
vaultFile: fs.readFileSync(ETH_VAULT_PATH(), "utf-8"),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Import vault from backup. Only used during catastrophic recovery
|
|
160
|
+
* when both vault files are missing.
|
|
161
|
+
*/
|
|
162
|
+
function importVaultFromBackup(masterKeyBuf, vaultFileStr) {
|
|
163
|
+
const vaultDir = VAULT_DIR();
|
|
164
|
+
fs.mkdirSync(vaultDir, { recursive: true });
|
|
165
|
+
|
|
166
|
+
fs.writeFileSync(MASTER_KEY_PATH(), masterKeyBuf);
|
|
167
|
+
try { fs.chmodSync(MASTER_KEY_PATH(), 0o600); } catch {}
|
|
168
|
+
|
|
169
|
+
fs.writeFileSync(ETH_VAULT_PATH(), vaultFileStr, "utf-8");
|
|
170
|
+
try { fs.chmodSync(ETH_VAULT_PATH(), 0o600); } catch {}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function getVaultPath() { return VAULT_DIR(); }
|
|
174
|
+
|
|
175
|
+
module.exports = {
|
|
176
|
+
initVault,
|
|
177
|
+
isVaultInitialized,
|
|
178
|
+
decryptPrivateKey,
|
|
179
|
+
rotateEncryption,
|
|
180
|
+
exportVaultForBackup,
|
|
181
|
+
importVaultFromBackup,
|
|
182
|
+
getVaultPath,
|
|
183
|
+
MASTER_KEY_PATH,
|
|
184
|
+
ETH_VAULT_PATH,
|
|
185
|
+
};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
const { decryptPrivateKey } = require("./vault-manager");
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wallet Operations — safe Ethereum wallet interactions.
|
|
5
|
+
*
|
|
6
|
+
* Every function follows the same pattern:
|
|
7
|
+
* 1. Decrypt private key into Buffer
|
|
8
|
+
* 2. Use it (sign, derive address, etc.)
|
|
9
|
+
* 3. Wipe Buffer in finally block
|
|
10
|
+
* 4. Catch block swallows real error, throws generic message
|
|
11
|
+
*
|
|
12
|
+
* The private key NEVER exists as a JavaScript string.
|
|
13
|
+
* Error messages are always generic — no hex, no key material, no details
|
|
14
|
+
* that could leak through the AI repair pipeline.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Derive the Ethereum address from the vault's private key.
|
|
19
|
+
* Address is not secret — safe to return as string.
|
|
20
|
+
*/
|
|
21
|
+
async function getWalletAddress() {
|
|
22
|
+
let keyBuf = null;
|
|
23
|
+
try {
|
|
24
|
+
keyBuf = decryptPrivateKey();
|
|
25
|
+
const { keccak256, getPublicKey } = _getCrypto();
|
|
26
|
+
const pubKey = getPublicKey(keyBuf);
|
|
27
|
+
const address = _pubKeyToAddress(pubKey, keccak256);
|
|
28
|
+
return address;
|
|
29
|
+
} catch {
|
|
30
|
+
throw new Error("wallet operation failed: could not derive address");
|
|
31
|
+
} finally {
|
|
32
|
+
if (keyBuf) keyBuf.fill(0);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get the compressed public key hex. Not secret.
|
|
38
|
+
*/
|
|
39
|
+
async function getPublicKeyHex() {
|
|
40
|
+
let keyBuf = null;
|
|
41
|
+
try {
|
|
42
|
+
keyBuf = decryptPrivateKey();
|
|
43
|
+
const { getPublicKey } = _getCrypto();
|
|
44
|
+
const pubKey = getPublicKey(keyBuf, true); // compressed
|
|
45
|
+
return Buffer.from(pubKey).toString("hex");
|
|
46
|
+
} catch {
|
|
47
|
+
throw new Error("wallet operation failed: could not derive public key");
|
|
48
|
+
} finally {
|
|
49
|
+
if (keyBuf) keyBuf.fill(0);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Sign an arbitrary message. Returns signature hex string.
|
|
55
|
+
* @param {string|Buffer} message — the message to sign
|
|
56
|
+
*/
|
|
57
|
+
async function signMessage(message) {
|
|
58
|
+
let keyBuf = null;
|
|
59
|
+
try {
|
|
60
|
+
keyBuf = decryptPrivateKey();
|
|
61
|
+
const { keccak256, sign } = _getCrypto();
|
|
62
|
+
const msgBuf = typeof message === "string" ? Buffer.from(message) : message;
|
|
63
|
+
// Ethereum signed message prefix
|
|
64
|
+
const prefix = Buffer.from(`\x19Ethereum Signed Message:\n${msgBuf.length}`);
|
|
65
|
+
const hash = keccak256(Buffer.concat([prefix, msgBuf]));
|
|
66
|
+
const sig = sign(hash, keyBuf);
|
|
67
|
+
return sig;
|
|
68
|
+
} catch {
|
|
69
|
+
throw new Error("wallet operation failed: could not sign message");
|
|
70
|
+
} finally {
|
|
71
|
+
if (keyBuf) keyBuf.fill(0);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Sign a transaction object. Returns signed transaction hex.
|
|
77
|
+
* @param {object} tx — { to, value, data?, nonce, gasLimit, gasPrice?, maxFeePerGas?, maxPriorityFeePerGas?, chainId }
|
|
78
|
+
*/
|
|
79
|
+
async function signTransaction(tx) {
|
|
80
|
+
// Validate BEFORE decrypting — don't touch the key for bad input
|
|
81
|
+
if (!tx || typeof tx !== "object") throw new Error("wallet operation failed: invalid transaction");
|
|
82
|
+
if (!tx.to || !tx.chainId) throw new Error("wallet operation failed: missing required fields");
|
|
83
|
+
|
|
84
|
+
let keyBuf = null;
|
|
85
|
+
try {
|
|
86
|
+
keyBuf = decryptPrivateKey();
|
|
87
|
+
// Use ethers for tx serialization + signing, but feed it a raw signer
|
|
88
|
+
const { Wallet } = require("ethers");
|
|
89
|
+
const wallet = new Wallet(keyBuf);
|
|
90
|
+
const signed = await wallet.signTransaction(tx);
|
|
91
|
+
return signed;
|
|
92
|
+
} catch {
|
|
93
|
+
throw new Error("wallet operation failed: could not sign transaction");
|
|
94
|
+
} finally {
|
|
95
|
+
if (keyBuf) keyBuf.fill(0);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get vault status — safe metadata only, no key material.
|
|
101
|
+
*/
|
|
102
|
+
function getVaultStatus() {
|
|
103
|
+
const { isVaultInitialized, getVaultPath } = require("./vault-manager");
|
|
104
|
+
const fs = require("fs");
|
|
105
|
+
const masterExists = fs.existsSync(require("./vault-manager").MASTER_KEY_PATH());
|
|
106
|
+
const ethExists = fs.existsSync(require("./vault-manager").ETH_VAULT_PATH());
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
initialized: isVaultInitialized(),
|
|
110
|
+
vaultPath: getVaultPath(),
|
|
111
|
+
masterKeyExists: masterExists,
|
|
112
|
+
ethVaultExists: ethExists,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Crypto helpers (use @noble/secp256k1 or built-in) ──
|
|
117
|
+
|
|
118
|
+
function _getCrypto() {
|
|
119
|
+
const crypto = require("crypto");
|
|
120
|
+
|
|
121
|
+
function keccak256(data) {
|
|
122
|
+
return crypto.createHash("sha3-256").update(data).digest();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getPublicKey(privKeyBuf, compressed = false) {
|
|
126
|
+
const ecdh = crypto.createECDH("secp256k1");
|
|
127
|
+
ecdh.setPrivateKey(privKeyBuf);
|
|
128
|
+
return compressed ? ecdh.getPublicKey("buffer", "compressed") : ecdh.getPublicKey("buffer", "uncompressed");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function sign(hash, privKeyBuf) {
|
|
132
|
+
const sig = crypto.sign(null, hash, {
|
|
133
|
+
key: crypto.createPrivateKey({
|
|
134
|
+
key: Buffer.concat([
|
|
135
|
+
// DER-encode secp256k1 private key
|
|
136
|
+
Buffer.from("302e0201010420", "hex"),
|
|
137
|
+
privKeyBuf,
|
|
138
|
+
Buffer.from("a00706052b8104000a", "hex"),
|
|
139
|
+
]),
|
|
140
|
+
format: "der",
|
|
141
|
+
type: "sec1",
|
|
142
|
+
}),
|
|
143
|
+
dsaEncoding: "ieee-p1363",
|
|
144
|
+
});
|
|
145
|
+
return sig.toString("hex");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { keccak256, getPublicKey, sign };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function _pubKeyToAddress(uncompressedPubKey, keccak256) {
|
|
152
|
+
// Skip the 0x04 prefix byte, hash the rest, take last 20 bytes
|
|
153
|
+
const pubBytes = uncompressedPubKey.slice(1);
|
|
154
|
+
const hash = keccak256(pubBytes);
|
|
155
|
+
const addrBytes = hash.slice(hash.length - 20);
|
|
156
|
+
const addr = "0x" + addrBytes.toString("hex");
|
|
157
|
+
// EIP-55 checksum
|
|
158
|
+
const addrLower = addr.slice(2).toLowerCase();
|
|
159
|
+
const hashHex = keccak256(Buffer.from(addrLower)).toString("hex");
|
|
160
|
+
let checksummed = "0x";
|
|
161
|
+
for (let i = 0; i < 40; i++) {
|
|
162
|
+
checksummed += parseInt(hashHex[i], 16) >= 8 ? addrLower[i].toUpperCase() : addrLower[i];
|
|
163
|
+
}
|
|
164
|
+
return checksummed;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = { getWalletAddress, getPublicKeyHex, signMessage, signTransaction, getVaultStatus };
|