@miraj181/ipingyou 1.0.2 → 2.0.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/README.md +75 -102
- package/package.json +8 -3
- package/src/cli.js +84 -4
- package/src/lib/chat.js +333 -0
- package/src/lib/cleanup.js +78 -0
- package/src/lib/config.js +41 -0
- package/src/lib/crypto.js +23 -14
- package/src/lib/platform.js +415 -53
- package/src/modes/client.js +451 -58
- package/src/modes/host.js +416 -119
package/src/lib/chat.js
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { WebSocketServer } from 'ws';
|
|
3
|
+
import open from 'open';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
|
|
6
|
+
const HTML_CONTENT = `
|
|
7
|
+
<!DOCTYPE html>
|
|
8
|
+
<html lang="en">
|
|
9
|
+
<head>
|
|
10
|
+
<meta charset="UTF-8">
|
|
11
|
+
<title>iPingYou — Secure Chat Room</title>
|
|
12
|
+
<style>
|
|
13
|
+
:root {
|
|
14
|
+
--bg: #0f172a;
|
|
15
|
+
--bg-panel: #1e293b;
|
|
16
|
+
--text: #f8fafc;
|
|
17
|
+
--primary: #38bdf8;
|
|
18
|
+
--accent: #818cf8;
|
|
19
|
+
--danger: #ef4444;
|
|
20
|
+
--border: #334155;
|
|
21
|
+
}
|
|
22
|
+
body {
|
|
23
|
+
margin: 0; padding: 0; font-family: 'Inter', system-ui, sans-serif;
|
|
24
|
+
background: var(--bg); color: var(--text); height: 100vh; display: flex; flex-direction: column;
|
|
25
|
+
}
|
|
26
|
+
header {
|
|
27
|
+
background: var(--bg-panel); padding: 1rem 2rem; border-bottom: 1px solid var(--border);
|
|
28
|
+
display: flex; justify-content: space-between; align-items: center; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
|
29
|
+
}
|
|
30
|
+
h1 { margin: 0; font-size: 1.25rem; font-weight: 600; display: flex; align-items: center; gap: 0.5rem; }
|
|
31
|
+
.badge { background: var(--primary); color: #000; padding: 0.2rem 0.5rem; border-radius: 999px; font-size: 0.8rem; font-weight: bold; }
|
|
32
|
+
.leave-btn {
|
|
33
|
+
background: var(--danger); color: white; border: none; padding: 0.5rem 1rem; border-radius: 0.5rem;
|
|
34
|
+
font-weight: bold; cursor: pointer; transition: opacity 0.2s;
|
|
35
|
+
}
|
|
36
|
+
.leave-btn:hover { opacity: 0.8; }
|
|
37
|
+
main {
|
|
38
|
+
flex: 1; display: flex; overflow: hidden;
|
|
39
|
+
}
|
|
40
|
+
.sidebar {
|
|
41
|
+
width: 250px; background: var(--bg-panel); border-right: 1px solid var(--border);
|
|
42
|
+
padding: 1rem; overflow-y: auto;
|
|
43
|
+
}
|
|
44
|
+
.chat-area {
|
|
45
|
+
flex: 1; display: flex; flex-direction: column; background: var(--bg);
|
|
46
|
+
}
|
|
47
|
+
.messages {
|
|
48
|
+
flex: 1; padding: 1.5rem; overflow-y: auto; display: flex; flex-direction: column; gap: 1rem;
|
|
49
|
+
}
|
|
50
|
+
.message { max-width: 70%; padding: 0.8rem 1rem; border-radius: 1rem; line-height: 1.4; animation: popIn 0.3s ease-out; }
|
|
51
|
+
.message.system { max-width: 100%; align-self: center; background: transparent; color: #94a3b8; font-size: 0.9rem; font-style: italic; text-align: center; }
|
|
52
|
+
.message.self { align-self: flex-end; background: var(--primary); color: #000; border-bottom-right-radius: 0.25rem; }
|
|
53
|
+
.message.other { align-self: flex-start; background: var(--bg-panel); border-bottom-left-radius: 0.25rem; }
|
|
54
|
+
.message-header { font-size: 0.75rem; margin-bottom: 0.25rem; opacity: 0.8; }
|
|
55
|
+
.input-area {
|
|
56
|
+
padding: 1rem; background: var(--bg-panel); border-top: 1px solid var(--border); display: flex; gap: 1rem;
|
|
57
|
+
}
|
|
58
|
+
input[type="text"] {
|
|
59
|
+
flex: 1; background: var(--bg); border: 1px solid var(--border); color: var(--text);
|
|
60
|
+
padding: 0.75rem 1rem; border-radius: 0.5rem; outline: none; transition: border-color 0.2s;
|
|
61
|
+
}
|
|
62
|
+
input[type="text"]:focus { border-color: var(--primary); }
|
|
63
|
+
button.send {
|
|
64
|
+
background: var(--accent); color: white; border: none; padding: 0 1.5rem; border-radius: 0.5rem;
|
|
65
|
+
font-weight: bold; cursor: pointer; transition: transform 0.1s, opacity 0.2s;
|
|
66
|
+
}
|
|
67
|
+
button.send:active { transform: scale(0.95); }
|
|
68
|
+
@keyframes popIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
|
69
|
+
|
|
70
|
+
.user-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.5rem; }
|
|
71
|
+
.user-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem; border-radius: 0.5rem; background: var(--bg); }
|
|
72
|
+
.user-item::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: #22c55e; }
|
|
73
|
+
</style>
|
|
74
|
+
</head>
|
|
75
|
+
<body>
|
|
76
|
+
<header>
|
|
77
|
+
<h1>💬 SecureLink Chat <span class="badge" id="conn-count">0 connected</span></h1>
|
|
78
|
+
<button class="leave-btn" id="leave-btn">Leave Room</button>
|
|
79
|
+
</header>
|
|
80
|
+
<main>
|
|
81
|
+
<div class="sidebar">
|
|
82
|
+
<h3 style="margin-top:0; font-size: 0.9rem; color: #94a3b8; text-transform: uppercase;">Participants</h3>
|
|
83
|
+
<ul class="user-list" id="users"></ul>
|
|
84
|
+
</div>
|
|
85
|
+
<div class="chat-area">
|
|
86
|
+
<div class="messages" id="msgs"></div>
|
|
87
|
+
<form class="input-area" id="chat-form">
|
|
88
|
+
<input type="text" id="msg-input" placeholder="Type a secure message..." autocomplete="off" disabled>
|
|
89
|
+
<button type="submit" class="send" id="send-btn" disabled>Send</button>
|
|
90
|
+
</form>
|
|
91
|
+
</div>
|
|
92
|
+
</main>
|
|
93
|
+
|
|
94
|
+
<script>
|
|
95
|
+
const isHost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
|
|
96
|
+
let username = isHost ? 'Host' : prompt('Enter your name for the chat:', 'Client_' + Math.floor(Math.random()*1000));
|
|
97
|
+
if (!username) username = 'Anonymous';
|
|
98
|
+
|
|
99
|
+
const sessionPassword = window.location.hash.substring(1);
|
|
100
|
+
if (!sessionPassword) {
|
|
101
|
+
document.body.innerHTML = '<h2 style="text-align:center; margin-top:20vh; color:red;">Fatal: Missing session password in URL hash. Cannot decrypt E2E chat.</h2>';
|
|
102
|
+
throw new Error("Missing password");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
106
|
+
const ws = new WebSocket(wsProtocol + '//' + window.location.host);
|
|
107
|
+
|
|
108
|
+
const msgs = document.getElementById('msgs');
|
|
109
|
+
const form = document.getElementById('chat-form');
|
|
110
|
+
const input = document.getElementById('msg-input');
|
|
111
|
+
const sendBtn = document.getElementById('send-btn');
|
|
112
|
+
const leaveBtn = document.getElementById('leave-btn');
|
|
113
|
+
const usersList = document.getElementById('users');
|
|
114
|
+
const connCount = document.getElementById('conn-count');
|
|
115
|
+
|
|
116
|
+
// ─── Web Crypto E2E AES-GCM ──────────────────────────────────
|
|
117
|
+
const enc = new TextEncoder();
|
|
118
|
+
const dec = new TextDecoder();
|
|
119
|
+
|
|
120
|
+
function buf2hex(buffer) { return [...new Uint8Array(buffer)].map(x => x.toString(16).padStart(2, '0')).join(''); }
|
|
121
|
+
function hex2buf(hexString) { return new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16))); }
|
|
122
|
+
|
|
123
|
+
async function deriveKey(password, saltBuffer) {
|
|
124
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
125
|
+
"raw", enc.encode(password), {name: "PBKDF2"}, false, ["deriveBits", "deriveKey"]
|
|
126
|
+
);
|
|
127
|
+
return crypto.subtle.deriveKey(
|
|
128
|
+
{ name: "PBKDF2", salt: saltBuffer, iterations: 100000, hash: "SHA-256" },
|
|
129
|
+
keyMaterial, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function encryptPayload(obj) {
|
|
134
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
135
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
136
|
+
const key = await deriveKey(sessionPassword, salt);
|
|
137
|
+
const ciphertextBuffer = await crypto.subtle.encrypt(
|
|
138
|
+
{ name: "AES-GCM", iv: iv }, key, enc.encode(JSON.stringify(obj))
|
|
139
|
+
);
|
|
140
|
+
return {
|
|
141
|
+
salt: buf2hex(salt),
|
|
142
|
+
iv: buf2hex(iv),
|
|
143
|
+
ciphertext: buf2hex(ciphertextBuffer)
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function decryptPayload(encObj) {
|
|
148
|
+
try {
|
|
149
|
+
const salt = hex2buf(encObj.salt);
|
|
150
|
+
const iv = hex2buf(encObj.iv);
|
|
151
|
+
const ciphertext = hex2buf(encObj.ciphertext);
|
|
152
|
+
const key = await deriveKey(sessionPassword, salt);
|
|
153
|
+
const decryptedBuffer = await crypto.subtle.decrypt(
|
|
154
|
+
{ name: "AES-GCM", iv: iv }, key, ciphertext
|
|
155
|
+
);
|
|
156
|
+
return JSON.parse(dec.decode(decryptedBuffer));
|
|
157
|
+
} catch (e) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ─── UI Logic ────────────────────────────────────────────────
|
|
163
|
+
function appendMessage(msg) {
|
|
164
|
+
const div = document.createElement('div');
|
|
165
|
+
if (msg.type === 'system') {
|
|
166
|
+
div.className = 'message system';
|
|
167
|
+
div.textContent = msg.text;
|
|
168
|
+
} else {
|
|
169
|
+
div.className = 'message ' + (msg.sender === username ? 'self' : 'other');
|
|
170
|
+
div.innerHTML = '<div class="message-header">' + msg.sender + ' • ' + msg.time + '</div><div>' + msg.text + '</div>';
|
|
171
|
+
}
|
|
172
|
+
msgs.appendChild(div);
|
|
173
|
+
msgs.scrollTop = msgs.scrollHeight;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function updateUsers(users) {
|
|
177
|
+
usersList.innerHTML = '';
|
|
178
|
+
users.forEach(u => {
|
|
179
|
+
const li = document.createElement('li');
|
|
180
|
+
li.className = 'user-item';
|
|
181
|
+
li.textContent = u;
|
|
182
|
+
usersList.appendChild(li);
|
|
183
|
+
});
|
|
184
|
+
connCount.textContent = users.length + ' connected';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
ws.onopen = async () => {
|
|
188
|
+
input.disabled = false;
|
|
189
|
+
sendBtn.disabled = false;
|
|
190
|
+
input.focus();
|
|
191
|
+
|
|
192
|
+
const encPayload = await encryptPayload({ type: 'join', sender: username });
|
|
193
|
+
ws.send(JSON.stringify({ type: 'e2e', payload: encPayload }));
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
ws.onmessage = async (event) => {
|
|
197
|
+
const data = JSON.parse(event.data);
|
|
198
|
+
if (data.type === 'state') {
|
|
199
|
+
updateUsers(data.users);
|
|
200
|
+
} else if (data.type === 'close') {
|
|
201
|
+
appendMessage({ type: 'system', text: 'The Host has closed the chat room. You may leave now.' });
|
|
202
|
+
ws.close();
|
|
203
|
+
input.disabled = true;
|
|
204
|
+
sendBtn.disabled = true;
|
|
205
|
+
} else if (data.type === 'e2e') {
|
|
206
|
+
// Decrypt the incoming E2E message
|
|
207
|
+
const decrypted = await decryptPayload(data.payload);
|
|
208
|
+
if (decrypted) {
|
|
209
|
+
if (decrypted.type === 'join') {
|
|
210
|
+
appendMessage({ type: 'system', text: \`\${decrypted.sender} has joined the chat (E2E Encrypted)\` });
|
|
211
|
+
} else if (decrypted.type === 'chat') {
|
|
212
|
+
appendMessage(decrypted);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
appendMessage(data);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
ws.onclose = () => {
|
|
221
|
+
appendMessage({ type: 'system', text: 'Connection closed.' });
|
|
222
|
+
input.disabled = true;
|
|
223
|
+
sendBtn.disabled = true;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
form.onsubmit = async (e) => {
|
|
227
|
+
e.preventDefault();
|
|
228
|
+
const text = input.value.trim();
|
|
229
|
+
if (!text) return;
|
|
230
|
+
|
|
231
|
+
const msgObj = { type: 'chat', sender: username, text: text, time: new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}) };
|
|
232
|
+
const encPayload = await encryptPayload(msgObj);
|
|
233
|
+
|
|
234
|
+
ws.send(JSON.stringify({ type: 'e2e', payload: encPayload }));
|
|
235
|
+
input.value = '';
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
leaveBtn.onclick = () => {
|
|
239
|
+
if (isHost) {
|
|
240
|
+
if(confirm('Are you sure you want to close the chat room for everyone?')) {
|
|
241
|
+
ws.send(JSON.stringify({ type: 'host_close' }));
|
|
242
|
+
window.close();
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
ws.close();
|
|
246
|
+
window.close();
|
|
247
|
+
document.body.innerHTML = '<h2 style="text-align:center; margin-top:20vh;">You have left the chat. You can close this tab.</h2>';
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
</script>
|
|
251
|
+
</body>
|
|
252
|
+
</html>
|
|
253
|
+
`;
|
|
254
|
+
|
|
255
|
+
export async function startChatServer(onClose) {
|
|
256
|
+
return new Promise((resolve) => {
|
|
257
|
+
const server = http.createServer((req, res) => {
|
|
258
|
+
if (req.url === '/') {
|
|
259
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
260
|
+
res.end(HTML_CONTENT);
|
|
261
|
+
} else {
|
|
262
|
+
res.writeHead(404);
|
|
263
|
+
res.end('Not found');
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const wss = new WebSocketServer({ server });
|
|
268
|
+
const clients = new Map(); // ws -> username
|
|
269
|
+
|
|
270
|
+
function broadcastState() {
|
|
271
|
+
const users = Array.from(clients.values());
|
|
272
|
+
const payload = JSON.stringify({ type: 'state', users });
|
|
273
|
+
for (const client of wss.clients) {
|
|
274
|
+
if (client.readyState === 1) client.send(payload);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function broadcastMsg(msg) {
|
|
279
|
+
const payload = JSON.stringify(msg);
|
|
280
|
+
for (const client of wss.clients) {
|
|
281
|
+
if (client.readyState === 1) client.send(payload);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
wss.on('connection', (ws) => {
|
|
286
|
+
ws.on('message', (message) => {
|
|
287
|
+
try {
|
|
288
|
+
const data = JSON.parse(message);
|
|
289
|
+
|
|
290
|
+
if (data.type === 'e2e') {
|
|
291
|
+
// E2E messages just get forwarded to all clients
|
|
292
|
+
broadcastMsg(data);
|
|
293
|
+
} else if (data.type === 'join_event') {
|
|
294
|
+
// For updating participant count dynamically if we wanted to extract sender,
|
|
295
|
+
// but since E2E hides sender, we rely on connection count
|
|
296
|
+
} else if (data.type === 'host_close') {
|
|
297
|
+
broadcastMsg({ type: 'close' });
|
|
298
|
+
server.close();
|
|
299
|
+
if (onClose) onClose();
|
|
300
|
+
}
|
|
301
|
+
} catch (e) {
|
|
302
|
+
// ignore invalid parse
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
ws.on('close', () => {
|
|
307
|
+
// Since we can't read the encrypted username, we just update state
|
|
308
|
+
clients.delete(ws);
|
|
309
|
+
broadcastState();
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// When connection opens, just assign a generic ID to count them
|
|
314
|
+
wss.on('connection', (ws) => {
|
|
315
|
+
clients.set(ws, `User_${Math.floor(Math.random()*1000)}`);
|
|
316
|
+
broadcastState();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
server.listen(0, '127.0.0.1', () => {
|
|
320
|
+
const port = server.address().port;
|
|
321
|
+
resolve({ port, server });
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export async function openLocalChatUI(port, password) {
|
|
327
|
+
try {
|
|
328
|
+
const chatUrl = `http://localhost:${port}#${password}`;
|
|
329
|
+
await open(chatUrl);
|
|
330
|
+
} catch {
|
|
331
|
+
console.log(chalk.dim(` Unable to auto-open browser. Visit http://localhost:${port}#${password}`));
|
|
332
|
+
}
|
|
333
|
+
}
|
package/src/lib/cleanup.js
CHANGED
|
@@ -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
|
-
*
|
|
16
|
-
*
|
|
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
|
|
19
|
-
|
|
20
|
-
return
|
|
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
|
-
* @
|
|
26
|
+
* @param {string} password
|
|
27
|
+
* @returns {{ iv: string, ciphertext: string, salt: string }}
|
|
27
28
|
*/
|
|
28
|
-
export function encrypt(plaintext) {
|
|
29
|
-
const
|
|
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
|
|
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');
|