@miraj181/ipingyou 1.0.1 → 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.
@@ -20,6 +20,17 @@ let _revokeUID = null;
20
20
  /** @type {string|null} — Broker URL for revocation */
21
21
  let _brokerUrl = null;
22
22
 
23
+ /** @type {Array<() => Promise<void>|void>} — Custom cleanup hooks */
24
+ const _cleanupHooks = [];
25
+
26
+ /**
27
+ * Register a custom cleanup hook to run on shutdown.
28
+ * @param {() => Promise<void>|void} hook
29
+ */
30
+ export function addCleanupHook(hook) {
31
+ _cleanupHooks.push(hook);
32
+ }
33
+
23
34
  /**
24
35
  * Register a spawned child PID for tracking.
25
36
  * @param {number} pid
@@ -80,6 +91,15 @@ export async function cleanupAll() {
80
91
  await Promise.allSettled(kills);
81
92
  trackedPIDs.clear();
82
93
 
94
+ // Run custom cleanup hooks
95
+ for (const hook of _cleanupHooks) {
96
+ try {
97
+ await hook();
98
+ } catch (err) {
99
+ console.error(chalk.red(` Cleanup hook failed: ${err.message}`));
100
+ }
101
+ }
102
+
83
103
  // Revoke UID from broker
84
104
  if (_revokeUID && _brokerUrl) {
85
105
  try {
@@ -129,3 +149,61 @@ export function installShutdownHandlers() {
129
149
  export function getTrackedCount() {
130
150
  return trackedPIDs.size;
131
151
  }
152
+
153
+ /**
154
+ * Execute Panic Mode (Self-Destruct)
155
+ * Wipes all configs, keys, and forcefully kills associated processes.
156
+ */
157
+ import fs from 'node:fs';
158
+ import os from 'node:os';
159
+ import path from 'node:path';
160
+ import { execaCommand } from 'execa';
161
+
162
+ export async function executePanicMode() {
163
+ console.log(chalk.bold.red('\n 🚨 INITIATING SECURELINK PANIC MODE 🚨\n'));
164
+
165
+ // 1. Force kill all cloudflared & ipingyou processes
166
+ console.log(chalk.dim(' [1/4] Terminating all tunnel and host processes...'));
167
+ try {
168
+ if (process.platform === 'win32') {
169
+ await execaCommand('taskkill /F /IM cloudflared.exe', { reject: false });
170
+ await execaCommand('taskkill /F /IM sshd.exe', { reject: false });
171
+ } else {
172
+ await execaCommand('pkill -9 -f cloudflared', { reject: false });
173
+ await execaCommand('pkill -9 -f "sshd:.*@"', { reject: false });
174
+ }
175
+ } catch {}
176
+
177
+ // 2. Delete configuration and aliases
178
+ console.log(chalk.dim(' [2/4] Wiping configuration files...'));
179
+ const configPath = path.join(os.homedir(), '.ipingyou', 'config.json');
180
+ try {
181
+ if (fs.existsSync(configPath)) {
182
+ fs.unlinkSync(configPath);
183
+ }
184
+ const configDir = path.join(os.homedir(), '.ipingyou');
185
+ if (fs.existsSync(configDir)) {
186
+ fs.rmSync(configDir, { recursive: true, force: true });
187
+ }
188
+ } catch {}
189
+
190
+ // 3. Delete ephemeral keys and temp files
191
+ console.log(chalk.dim(' [3/4] Purging ephemeral keys and temporary files...'));
192
+ try {
193
+ const tmpDir = os.tmpdir();
194
+ const files = fs.readdirSync(tmpDir);
195
+ for (const file of files) {
196
+ if (file.startsWith('ipingyou_')) {
197
+ fs.unlinkSync(path.join(tmpDir, file));
198
+ }
199
+ }
200
+ } catch {}
201
+
202
+ // 4. Scrub SSH authorized_keys if we know we injected
203
+ // Note: We don't want to wipe the user's whole authorized_keys, but if we have a hook, we could.
204
+ // We'll skip scraping the actual file here unless we know the exact comment.
205
+ console.log(chalk.dim(' [4/4] Finalizing cleanup...'));
206
+
207
+ console.log(chalk.bold.green('\n ✅ Panic Mode Complete. All traces removed.\n'));
208
+ process.exit(0);
209
+ }
@@ -0,0 +1,41 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+
5
+ const CONFIG_DIR = path.join(os.homedir(), '.ipingyou');
6
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
7
+
8
+ function ensureConfig() {
9
+ if (!fs.existsSync(CONFIG_DIR)) {
10
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
11
+ }
12
+ if (!fs.existsSync(CONFIG_FILE)) {
13
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify({ aliases: {}, settings: {} }, null, 2));
14
+ }
15
+ }
16
+
17
+ export function getConfig() {
18
+ ensureConfig();
19
+ try {
20
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
21
+ } catch {
22
+ return { aliases: {}, settings: {} };
23
+ }
24
+ }
25
+
26
+ export function saveConfig(config) {
27
+ ensureConfig();
28
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
29
+ }
30
+
31
+ export function saveAlias(aliasName, data) {
32
+ const config = getConfig();
33
+ if (!config.aliases) config.aliases = {};
34
+ config.aliases[aliasName] = data;
35
+ saveConfig(config);
36
+ }
37
+
38
+ export function getAlias(aliasName) {
39
+ const config = getConfig();
40
+ return config.aliases?.[aliasName] || null;
41
+ }
package/src/lib/crypto.js CHANGED
@@ -9,43 +9,52 @@
9
9
 
10
10
  import crypto from 'node:crypto';
11
11
 
12
- const DEFAULT_HEX_KEY = 'b374a26d71590483815c467a99623e1b7db95f269c2889279a32c4530fc4159f';
13
-
14
12
  /**
15
- * Get the encryption key as a Buffer.
16
- * Reads from env var SECRET_KEY, falls back to the default dev key.
13
+ * Derive a 256-bit encryption key from a password and salt using PBKDF2.
14
+ * @param {string} password
15
+ * @param {Buffer} salt
16
+ * @returns {Buffer}
17
17
  */
18
- export function getKey() {
19
- const hex = process.env.SECRET_KEY || DEFAULT_HEX_KEY;
20
- return Buffer.from(hex, 'hex');
18
+ export function deriveKey(password, salt) {
19
+ // Use 100,000 iterations for strong security
20
+ return crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256');
21
21
  }
22
22
 
23
23
  /**
24
- * Encrypt a plaintext string with AES-256-CBC.
24
+ * Encrypt a plaintext string with AES-256-CBC using a password.
25
25
  * @param {string} plaintext
26
- * @returns {{ iv: string, ciphertext: string }}
26
+ * @param {string} password
27
+ * @returns {{ iv: string, ciphertext: string, salt: string }}
27
28
  */
28
- export function encrypt(plaintext) {
29
- const key = getKey();
29
+ export function encrypt(plaintext, password) {
30
+ const salt = crypto.randomBytes(16);
31
+ const key = deriveKey(password, salt);
30
32
  const iv = crypto.randomBytes(16);
33
+
31
34
  const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
32
35
  let enc = cipher.update(plaintext, 'utf8', 'base64');
33
36
  enc += cipher.final('base64');
37
+
34
38
  return {
35
39
  iv: iv.toString('hex'),
36
40
  ciphertext: enc,
41
+ salt: salt.toString('hex')
37
42
  };
38
43
  }
39
44
 
40
45
  /**
41
- * Decrypt a ciphertext with AES-256-CBC.
46
+ * Decrypt a ciphertext with AES-256-CBC using a password and salt.
42
47
  * @param {string} ivHex — 32-char hex IV
43
48
  * @param {string} cipherBase64
49
+ * @param {string} password
50
+ * @param {string} saltHex
44
51
  * @returns {string}
45
52
  */
46
- export function decrypt(ivHex, cipherBase64) {
47
- const key = getKey();
53
+ export function decrypt(ivHex, cipherBase64, password, saltHex) {
54
+ const salt = Buffer.from(saltHex, 'hex');
55
+ const key = deriveKey(password, salt);
48
56
  const iv = Buffer.from(ivHex, 'hex');
57
+
49
58
  const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
50
59
  let dec = decipher.update(cipherBase64, 'base64', 'utf8');
51
60
  dec += decipher.final('utf8');