the-invisible-billion-cli 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/LICENSE +21 -0
- package/README.md +371 -0
- package/bin/ib.js +90 -0
- package/package.json +28 -0
- package/src/cli/commands/file.js +20 -0
- package/src/cli/commands/scan.js +50 -0
- package/src/cli/commands/send.js +113 -0
- package/src/cli/commands/start.js +126 -0
- package/src/cli/commands/status.js +43 -0
- package/src/cli/commands/stop.js +15 -0
- package/src/cli/commands/sync.js +31 -0
- package/src/cli/ipc.js +127 -0
- package/src/crypto/encrypt.js +100 -0
- package/src/crypto/keys.js +95 -0
- package/src/daemon/epidemic.js +103 -0
- package/src/daemon/index.js +683 -0
- package/src/daemon/network.js +84 -0
- package/src/daemon/tcp.js +200 -0
- package/src/daemon/udp.js +282 -0
- package/src/db/cleanup.js +39 -0
- package/src/db/index.js +74 -0
- package/src/db/messages.js +213 -0
- package/src/identity/index.js +92 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const { runCommand, sendCommand } = require('../ipc');
|
|
5
|
+
const { encrypt } = require('../../crypto/encrypt');
|
|
6
|
+
const { loadIdentity } = require('../../identity/index');
|
|
7
|
+
const { loadOrCreateKeypair } = require('../../crypto/keys');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* ib send <destination> "<message>"
|
|
11
|
+
*
|
|
12
|
+
* 1. Validate identity format
|
|
13
|
+
* 2. Ask daemon for recipient's public key via get-peer-key IPC
|
|
14
|
+
* 3a. Key found → encrypt with hybrid AES+RSA, send pre-encrypted payload
|
|
15
|
+
* 3b. Key not found → store as pending (daemon will encrypt when peer discovered)
|
|
16
|
+
* 4. Daemon stores first, then attempts live TCP delivery immediately
|
|
17
|
+
*/
|
|
18
|
+
async function sendCommand_(destination, messageText) {
|
|
19
|
+
if (!destination || !messageText) {
|
|
20
|
+
console.error('Usage: ib send <identity> "<message>"');
|
|
21
|
+
console.error('Example: ib send raj@a3f2 "bhai notes bhej"');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Validate identity format: username@xxxx (4 alphanumeric chars)
|
|
26
|
+
if (!/^[a-z0-9_-]+@[a-z0-9]{4}$/.test(destination)) {
|
|
27
|
+
console.error(`❌ Invalid identity format: "${destination}"`);
|
|
28
|
+
console.error(' Expected: username@xxxx (e.g. raj@a3f2)');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Load sender identity
|
|
33
|
+
let senderIdentity;
|
|
34
|
+
try {
|
|
35
|
+
senderIdentity = loadIdentity().identity;
|
|
36
|
+
} catch {
|
|
37
|
+
console.error('❌ Not initialised. Run: ib start');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── 1. Try to get recipient public key from daemon's peer table ──────────
|
|
42
|
+
let encryptedPayload;
|
|
43
|
+
let recipientPublicKey = null;
|
|
44
|
+
|
|
45
|
+
// Sending to ourselves? Use our own public key.
|
|
46
|
+
if (destination === senderIdentity) {
|
|
47
|
+
const { publicKey } = loadOrCreateKeypair();
|
|
48
|
+
recipientPublicKey = publicKey;
|
|
49
|
+
} else {
|
|
50
|
+
// Ask the daemon (it has the peer table from UDP discovery)
|
|
51
|
+
try {
|
|
52
|
+
const res = await sendCommand({ type: 'get-peer-key', identity: destination });
|
|
53
|
+
if (res.ok && res.publicKey) {
|
|
54
|
+
recipientPublicKey = res.publicKey;
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Daemon may be unreachable — handled below
|
|
58
|
+
console.error('❌ Daemon is not running. Start it with: ib start');
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── 2. Encrypt the message ───────────────────────────────────────────────
|
|
64
|
+
if (recipientPublicKey) {
|
|
65
|
+
try {
|
|
66
|
+
const blob = encrypt(messageText, recipientPublicKey);
|
|
67
|
+
encryptedPayload = JSON.stringify(blob);
|
|
68
|
+
console.log(`🔐 Message encrypted for ${destination}`);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error('❌ Encryption failed:', err.message);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
// Peer's public key not yet known — store with pending flag
|
|
75
|
+
// Daemon will encrypt + re-deliver once it discovers the peer via UDP
|
|
76
|
+
console.log(`⚠️ ${destination} not yet on network — storing for epidemic delivery.`);
|
|
77
|
+
encryptedPayload = JSON.stringify({
|
|
78
|
+
__pending_encryption: true,
|
|
79
|
+
__phase1_plaintext: messageText,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── 3. Send to daemon (store-first, then live delivery attempt) ──────────
|
|
84
|
+
const messageId = crypto.randomUUID();
|
|
85
|
+
const ttl = Date.now() + 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
86
|
+
|
|
87
|
+
await runCommand(
|
|
88
|
+
{
|
|
89
|
+
type: 'send',
|
|
90
|
+
id: messageId,
|
|
91
|
+
from_identity: senderIdentity,
|
|
92
|
+
destination,
|
|
93
|
+
payload: encryptedPayload,
|
|
94
|
+
ttl,
|
|
95
|
+
},
|
|
96
|
+
(res) => {
|
|
97
|
+
if (!res.ok) {
|
|
98
|
+
console.error('❌ Failed to queue message:', res.error);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const isDelivered = res.status === 'delivered';
|
|
103
|
+
console.log(`\n📤 Message ${isDelivered ? 'delivered!' : 'queued!'}`);
|
|
104
|
+
console.log(` To: ${destination}`);
|
|
105
|
+
console.log(` Message: "${messageText}"`);
|
|
106
|
+
console.log(` ID: ${messageId}`);
|
|
107
|
+
console.log(` Status: ${isDelivered ? '✅ Delivered' : '⏳ Undelivered (will deliver when peer connects)'}`);
|
|
108
|
+
console.log(`\n Track it: ib status ${messageId}\n`);
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { sendCommand: sendCommand_ };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ib start — Start the background daemon.
|
|
5
|
+
*
|
|
6
|
+
* Handles:
|
|
7
|
+
* 1. First-time identity + keypair setup (interactive)
|
|
8
|
+
* 2. Crash recovery (stale PID file detection)
|
|
9
|
+
* 3. Idempotent: if daemon already running, says so and exits
|
|
10
|
+
* 4. Spawns daemon detached with windowsHide: true
|
|
11
|
+
* 5. Waits up to 3 s for daemon IPC to become ready, then confirms
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
const { spawn } = require('child_process');
|
|
20
|
+
const { loadOrCreate } = require('../../identity/index');
|
|
21
|
+
const { loadOrCreateKeypair } = require('../../crypto/keys');
|
|
22
|
+
const { isDaemonRunning } = require('../ipc');
|
|
23
|
+
|
|
24
|
+
const IB_DIR = path.join(os.homedir(), '.ib');
|
|
25
|
+
const PID_FILE = path.join(IB_DIR, 'daemon.pid');
|
|
26
|
+
const DAEMON_ENTRY = path.resolve(__dirname, '../../daemon/index.js');
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if a process with the given PID is alive.
|
|
30
|
+
* Cross-platform: signal 0 on unix, tasklist on Windows.
|
|
31
|
+
*/
|
|
32
|
+
function isProcessAlive(pid) {
|
|
33
|
+
try {
|
|
34
|
+
// kill(pid, 0) does not send a signal — just checks existence
|
|
35
|
+
process.kill(pid, 0);
|
|
36
|
+
return true;
|
|
37
|
+
} catch (err) {
|
|
38
|
+
// EPERM means process exists but we can't signal it — still alive
|
|
39
|
+
return err.code === 'EPERM';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Remove a stale PID file if the process is not running.
|
|
45
|
+
* Returns true if a stale PID was cleaned up.
|
|
46
|
+
*/
|
|
47
|
+
function cleanStalePid() {
|
|
48
|
+
if (!fs.existsSync(PID_FILE)) return false;
|
|
49
|
+
|
|
50
|
+
const raw = fs.readFileSync(PID_FILE, 'utf8').trim();
|
|
51
|
+
const pid = parseInt(raw, 10);
|
|
52
|
+
|
|
53
|
+
if (!pid || isNaN(pid)) {
|
|
54
|
+
fs.unlinkSync(PID_FILE);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!isProcessAlive(pid)) {
|
|
59
|
+
console.log(`⚠️ Found stale PID file (PID ${pid} is not running). Cleaning up...`);
|
|
60
|
+
fs.unlinkSync(PID_FILE);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return false; // Process is alive — daemon is running
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Wait for IPC to become responsive. Polls every 200ms up to maxMs.
|
|
69
|
+
*/
|
|
70
|
+
async function waitForDaemon(maxMs = 3000) {
|
|
71
|
+
const start = Date.now();
|
|
72
|
+
while (Date.now() - start < maxMs) {
|
|
73
|
+
if (await isDaemonRunning()) return true;
|
|
74
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function startCommand() {
|
|
80
|
+
// ── 1. First-time setup (identity + keypair) ───────────────────────────────
|
|
81
|
+
// Must happen BEFORE daemon spawn because daemon reads identity from disk.
|
|
82
|
+
const identity = await loadOrCreate();
|
|
83
|
+
loadOrCreateKeypair(); // generate if not exists, silent if already there
|
|
84
|
+
|
|
85
|
+
// ── 2. Crash recovery ──────────────────────────────────────────────────────
|
|
86
|
+
const wasStale = cleanStalePid();
|
|
87
|
+
|
|
88
|
+
// ── 3. Check if already running ───────────────────────────────────────────
|
|
89
|
+
if (!wasStale && fs.existsSync(PID_FILE)) {
|
|
90
|
+
// PID file exists and process is alive → already running
|
|
91
|
+
if (await isDaemonRunning()) {
|
|
92
|
+
console.log(`✅ The Invisible Billion daemon is already running as ${identity.identity}`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// PID alive but IPC not responding — force clean and restart
|
|
96
|
+
console.log('⚠️ Daemon PID exists but IPC is unresponsive. Force-restarting...');
|
|
97
|
+
fs.unlinkSync(PID_FILE);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── 4. Spawn daemon ───────────────────────────────────────────────────────
|
|
101
|
+
const child = spawn(process.execPath, [DAEMON_ENTRY], {
|
|
102
|
+
detached: true,
|
|
103
|
+
stdio: 'ignore',
|
|
104
|
+
windowsHide: true, // Critical: no console window on Windows
|
|
105
|
+
});
|
|
106
|
+
child.unref(); // Allow CLI to exit immediately
|
|
107
|
+
|
|
108
|
+
console.log(`🌍 Starting The Invisible Billion daemon... (PID will be ${child.pid})`);
|
|
109
|
+
|
|
110
|
+
// ── 5. Wait for IPC ready ─────────────────────────────────────────────────
|
|
111
|
+
const ready = await waitForDaemon(4000);
|
|
112
|
+
|
|
113
|
+
if (ready) {
|
|
114
|
+
console.log(`\n✅ The Invisible Billion daemon is running!`);
|
|
115
|
+
console.log(` Your identity: ${identity.identity}`);
|
|
116
|
+
console.log(` Log: ~/.ib/daemon.log`);
|
|
117
|
+
console.log(` Stop: ib stop\n`);
|
|
118
|
+
} else {
|
|
119
|
+
console.error(
|
|
120
|
+
'\n⚠️ Daemon spawned but IPC is not responding yet.\n' +
|
|
121
|
+
' It may still be starting up — check ~/.ib/daemon.log for details.'
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = { startCommand };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { runCommand } = require('../ipc');
|
|
4
|
+
|
|
5
|
+
async function statusCommand(messageId) {
|
|
6
|
+
if (!messageId) {
|
|
7
|
+
console.error('Usage: ib status <messageId>');
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
await runCommand({ type: 'status', messageId }, (res) => {
|
|
12
|
+
if (!res.ok) {
|
|
13
|
+
console.error(`❌ ${res.error}`);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const icons = {
|
|
18
|
+
undelivered: '🕐',
|
|
19
|
+
intransit: '🚀',
|
|
20
|
+
delivered: '✅',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const labels = {
|
|
24
|
+
undelivered: 'Undelivered (waiting for a carrier to pass through)',
|
|
25
|
+
intransit: 'In Transit (virus is spreading through the network)',
|
|
26
|
+
delivered: 'Delivered (recipient received and decrypted)',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const s = res.status;
|
|
30
|
+
const created = new Date(res.created_at).toLocaleString();
|
|
31
|
+
const expires = new Date(res.ttl).toLocaleString();
|
|
32
|
+
|
|
33
|
+
console.log(`\n${icons[s] || '❓'} Message Status`);
|
|
34
|
+
console.log(` ID: ${res.messageId}`);
|
|
35
|
+
console.log(` To: ${res.destination}`);
|
|
36
|
+
console.log(` Status: ${labels[s] || s}`);
|
|
37
|
+
console.log(` Hops: ${res.hop_count}`);
|
|
38
|
+
console.log(` Created: ${created}`);
|
|
39
|
+
console.log(` Expires: ${expires}\n`);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { statusCommand };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { runCommand } = require('../ipc');
|
|
4
|
+
|
|
5
|
+
async function stopCommand() {
|
|
6
|
+
await runCommand({ type: 'stop' }, (res) => {
|
|
7
|
+
if (res.ok) {
|
|
8
|
+
console.log('⛔ The Invisible Billion daemon stopped.');
|
|
9
|
+
} else {
|
|
10
|
+
console.error('❌ Stop failed:', res.error);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = { stopCommand };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { runCommand } = require('../ipc');
|
|
4
|
+
|
|
5
|
+
async function syncCommand() {
|
|
6
|
+
console.log('🔄 Triggering sync with all known peers...');
|
|
7
|
+
await runCommand({ type: 'sync' }, (res) => {
|
|
8
|
+
if (!res.ok) {
|
|
9
|
+
console.error('❌ Sync failed:', res.error);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
console.log(`\n🔄 Sync complete!\n`);
|
|
14
|
+
console.log(` Pending messages: ${res.pending}`);
|
|
15
|
+
console.log(` Deliverable now: ${res.deliverable} (peers with known IPs)`);
|
|
16
|
+
console.log(` Known peers: ${res.peers}`);
|
|
17
|
+
|
|
18
|
+
if (res.pending > 0 && res.deliverable === 0) {
|
|
19
|
+
console.log(`\n ⏳ Messages are queued but destination peers are not`);
|
|
20
|
+
console.log(` yet on this network. They will deliver automatically`);
|
|
21
|
+
console.log(` when those peers appear. (This is Epidemic Routing)\n`);
|
|
22
|
+
} else if (res.deliverable > 0) {
|
|
23
|
+
console.log(`\n 📤 Delivery in progress — check status with:`);
|
|
24
|
+
console.log(` ib status <messageId>\n`);
|
|
25
|
+
} else {
|
|
26
|
+
console.log(`\n ✅ No pending messages.\n`);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { syncCommand };
|
package/src/cli/ipc.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI → Daemon IPC client.
|
|
5
|
+
*
|
|
6
|
+
* Connects to the daemon's IPC socket (Unix socket on Linux/macOS,
|
|
7
|
+
* named pipe on Windows), sends a newline-delimited JSON command,
|
|
8
|
+
* waits for the JSON response, then resolves.
|
|
9
|
+
*
|
|
10
|
+
* Never hangs — times out after 5 seconds.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const net = require('net');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
|
|
17
|
+
const IB_DIR = path.join(os.homedir(), '.ib');
|
|
18
|
+
|
|
19
|
+
function getIPCPath() {
|
|
20
|
+
if (process.platform === 'win32') {
|
|
21
|
+
return '\\\\.\\pipe\\ib-ipc'; //Windows uses named pipes.
|
|
22
|
+
}
|
|
23
|
+
return path.join(IB_DIR, 'daemon.sock'); //Unix socket for Linux/macOS.
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const IPC_SOCKET = getIPCPath();
|
|
27
|
+
const TIMEOUT_MS = 5000;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Send a command object to the daemon and return the response.
|
|
31
|
+
*
|
|
32
|
+
* @param {object} command - e.g. { type: 'ping' } or { type: 'send', ... }
|
|
33
|
+
* @returns {Promise<object>} - The daemon's JSON response
|
|
34
|
+
*/
|
|
35
|
+
function sendCommand(command) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const socket = new net.Socket();
|
|
38
|
+
let buffer = '';
|
|
39
|
+
let timedOut = false;
|
|
40
|
+
|
|
41
|
+
const timer = setTimeout(() => {
|
|
42
|
+
timedOut = true;
|
|
43
|
+
socket.destroy();
|
|
44
|
+
reject(new Error('TIMEOUT'));
|
|
45
|
+
}, TIMEOUT_MS);
|
|
46
|
+
|
|
47
|
+
socket.connect(IPC_SOCKET, () => {
|
|
48
|
+
socket.write(JSON.stringify(command) + '\n');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
socket.on('data', (chunk) => {
|
|
52
|
+
buffer += chunk.toString();
|
|
53
|
+
const lines = buffer.split('\n');
|
|
54
|
+
buffer = lines.pop();
|
|
55
|
+
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
if (!line.trim()) continue;
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
try {
|
|
60
|
+
const response = JSON.parse(line);
|
|
61
|
+
socket.destroy();
|
|
62
|
+
resolve(response);
|
|
63
|
+
} catch {
|
|
64
|
+
socket.destroy();
|
|
65
|
+
reject(new Error('Invalid response from daemon'));
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
socket.on('error', (err) => {
|
|
72
|
+
if (timedOut) return;
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
socket.destroy();
|
|
75
|
+
if (err.code === 'ENOENT' || err.code === 'ECONNREFUSED') {
|
|
76
|
+
reject(new Error('DAEMON_NOT_RUNNING'));
|
|
77
|
+
} else {
|
|
78
|
+
reject(err);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
socket.on('close', () => {
|
|
83
|
+
if (timedOut) return;
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
// If we got here without resolving, resolve with empty
|
|
86
|
+
resolve({ ok: false, error: 'Connection closed unexpectedly' });
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Convenience wrapper: send a command and print the result.
|
|
93
|
+
* Handles "daemon not running" with a user-friendly message.
|
|
94
|
+
*
|
|
95
|
+
* @param {object} command
|
|
96
|
+
* @param {function} formatter - (response) => void — formats and prints the result
|
|
97
|
+
*/
|
|
98
|
+
async function runCommand(command, formatter) {
|
|
99
|
+
try {
|
|
100
|
+
const response = await sendCommand(command);
|
|
101
|
+
formatter(response);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
if (err.message === 'DAEMON_NOT_RUNNING') {
|
|
104
|
+
console.error('❌ Daemon is not running. Start it with: ib start');
|
|
105
|
+
} else if (err.message === 'TIMEOUT') {
|
|
106
|
+
console.error('❌ Daemon is not responding (timeout). Try: ib stop && ib start');
|
|
107
|
+
} else {
|
|
108
|
+
console.error('❌ IPC error:', err.message);
|
|
109
|
+
}
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check if daemon is alive (ping).
|
|
116
|
+
* Returns true if responding, false otherwise.
|
|
117
|
+
*/
|
|
118
|
+
async function isDaemonRunning() {
|
|
119
|
+
try {
|
|
120
|
+
const res = await sendCommand({ type: 'ping' });
|
|
121
|
+
return res.ok === true;
|
|
122
|
+
} catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = { sendCommand, runCommand, isDaemonRunning, IPC_SOCKET };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hybrid encryption: AES-256-GCM (for arbitrary-length plaintext)
|
|
5
|
+
* + RSA-OAEP (to encrypt the AES key).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* RSA encryption needs something called padding.
|
|
12
|
+
* Padding is extra data added before encryption to make RSA secure.
|
|
13
|
+
*/
|
|
14
|
+
const RSA_PADDING = crypto.constants.RSA_PKCS1_OAEP_PADDING;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* OAEP is a padding scheme for RSA encryption.
|
|
18
|
+
* It uses a hash function (SHA-256) to add randomness and security.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const OAEP_HASH = 'sha256';
|
|
22
|
+
|
|
23
|
+
//The crypto functions expect binary data, not strings.
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Encrypt a plaintext string for a recipient.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} plaintext - The message to encrypt
|
|
29
|
+
* @param {string} recipientPublicKeyPem - Recipient's RSA public key (PEM)
|
|
30
|
+
* @returns {{ encryptedKey: string, iv: string, tag: string, ciphertext: string }}
|
|
31
|
+
*/
|
|
32
|
+
function encrypt(plaintext, recipientPublicKeyPem) {
|
|
33
|
+
|
|
34
|
+
//Generate a fresh random AES-256 key for this message only
|
|
35
|
+
const aesKey = crypto.randomBytes(32); // 256-bit
|
|
36
|
+
const iv = crypto.randomBytes(12); // 96-bit IV for GCM
|
|
37
|
+
|
|
38
|
+
// Encrypt plaintext with AES-256-GCM
|
|
39
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', aesKey, iv); // in decryption this process will happen secondly, then down one(Interms of decryption)
|
|
40
|
+
const ciphertextBuf = Buffer.concat([
|
|
41
|
+
cipher.update(plaintext, 'utf8'),
|
|
42
|
+
cipher.final(),
|
|
43
|
+
]);
|
|
44
|
+
const tag = cipher.getAuthTag(); // while encrypting, we also generate a 16-byte authentication tag
|
|
45
|
+
|
|
46
|
+
// Encrypt the AES key with the recipient's RSA public key
|
|
47
|
+
const encryptedKey = crypto.publicEncrypt(
|
|
48
|
+
{
|
|
49
|
+
key: recipientPublicKeyPem, //hence this should also be binary-data?
|
|
50
|
+
padding: RSA_PADDING,
|
|
51
|
+
oaepHash: OAEP_HASH,
|
|
52
|
+
},
|
|
53
|
+
aesKey
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
encryptedKey: encryptedKey.toString('hex'),
|
|
58
|
+
iv: iv.toString('hex'),//be careful with this syntax, dont cconfuse with the Buffer.from(string, 'hex')
|
|
59
|
+
tag: tag.toString('hex'),
|
|
60
|
+
ciphertext: ciphertextBuf.toString('hex'),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Decrypt a payload produced by encrypt().
|
|
66
|
+
*
|
|
67
|
+
* @param {{ encryptedKey: string, iv: string, tag: string, ciphertext: string }} payload
|
|
68
|
+
* @param {string} privateKeyPem - Recipient's RSA private key (PEM)
|
|
69
|
+
* @returns {string} - Decrypted plaintext
|
|
70
|
+
*/
|
|
71
|
+
function decrypt(payload, privateKeyPem) { //payload is the encrypted message
|
|
72
|
+
const { encryptedKey, iv, tag, ciphertext } = payload;
|
|
73
|
+
|
|
74
|
+
//unlocking rsa stuff
|
|
75
|
+
const aesKey = crypto.privateDecrypt(
|
|
76
|
+
{
|
|
77
|
+
key: privateKeyPem,
|
|
78
|
+
padding: RSA_PADDING,
|
|
79
|
+
oaepHash: OAEP_HASH,
|
|
80
|
+
},
|
|
81
|
+
Buffer.from(encryptedKey, 'hex')
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// creating a decryption engine
|
|
85
|
+
const decipher = crypto.createDecipheriv(
|
|
86
|
+
'aes-256-gcm',
|
|
87
|
+
aesKey,
|
|
88
|
+
Buffer.from(iv, 'hex')
|
|
89
|
+
);
|
|
90
|
+
decipher.setAuthTag(Buffer.from(tag, 'hex'));
|
|
91
|
+
|
|
92
|
+
const plaintext = Buffer.concat([
|
|
93
|
+
decipher.update(Buffer.from(ciphertext, 'hex')),
|
|
94
|
+
decipher.final(),
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
return plaintext.toString('utf8');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = { encrypt, decrypt };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const { IB_DIR } = require('../identity/index');
|
|
7
|
+
|
|
8
|
+
const KEYS_DIR = path.join(IB_DIR, 'keys');
|
|
9
|
+
const PUBLIC_KEY_FILE = path.join(KEYS_DIR, 'public.pem');
|
|
10
|
+
const PRIVATE_KEY_FILE = path.join(KEYS_DIR, 'private.pem');
|
|
11
|
+
|
|
12
|
+
//using RSA algorithm to generate keypair
|
|
13
|
+
function generateKeypair() {
|
|
14
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
|
|
15
|
+
modulusLength: 2048,
|
|
16
|
+
//use spki format for public key and pkcs8 format for private key
|
|
17
|
+
//pem is a text format for keys
|
|
18
|
+
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
19
|
+
/**
|
|
20
|
+
* something like this
|
|
21
|
+
* -----BEGIN PUBLIC KEY-----
|
|
22
|
+
* MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
|
|
23
|
+
* -----END PUBLIC KEY-----
|
|
24
|
+
*/
|
|
25
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
26
|
+
});
|
|
27
|
+
return { publicKey, privateKey };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Load existing keypair from disk, or generate and save a new one.
|
|
32
|
+
* Returns { publicKey, privateKey } as PEM strings.
|
|
33
|
+
*/
|
|
34
|
+
function loadOrCreateKeypair() {
|
|
35
|
+
if (!fs.existsSync(KEYS_DIR)) {
|
|
36
|
+
fs.mkdirSync(KEYS_DIR, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
//if key already exists, then load it
|
|
40
|
+
if (fs.existsSync(PUBLIC_KEY_FILE) && fs.existsSync(PRIVATE_KEY_FILE)) {
|
|
41
|
+
const publicKey = fs.readFileSync(PUBLIC_KEY_FILE, 'utf8');
|
|
42
|
+
const privateKey = fs.readFileSync(PRIVATE_KEY_FILE, 'utf8');
|
|
43
|
+
return { publicKey, privateKey };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
//if not, generate and save it
|
|
47
|
+
const { publicKey, privateKey } = generateKeypair();
|
|
48
|
+
fs.writeFileSync(PUBLIC_KEY_FILE, publicKey, 'utf8');
|
|
49
|
+
// Private key: restrict permissions on unix-like systems
|
|
50
|
+
/*
|
|
51
|
+
The number is in octal format.
|
|
52
|
+
|
|
53
|
+
0o600
|
|
54
|
+
|
|
55
|
+
Break it down:
|
|
56
|
+
|
|
57
|
+
Digit Meaning
|
|
58
|
+
6 owner permissions
|
|
59
|
+
0 group permissions
|
|
60
|
+
0 others permissions
|
|
61
|
+
*/
|
|
62
|
+
fs.writeFileSync(PRIVATE_KEY_FILE, privateKey, { encoding: 'utf8', mode: 0o600 });
|
|
63
|
+
|
|
64
|
+
console.log('🔑 RSA keypair generated and stored in ~/.ib/keys/');
|
|
65
|
+
return { publicKey, privateKey };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Load public key PEM for the local user.
|
|
70
|
+
* Throws if not found (daemon should call loadOrCreateKeypair first).
|
|
71
|
+
*/
|
|
72
|
+
function loadPublicKey() {
|
|
73
|
+
if (!fs.existsSync(PUBLIC_KEY_FILE)) {
|
|
74
|
+
throw new Error('No public key found. Run: ib start');
|
|
75
|
+
}
|
|
76
|
+
return fs.readFileSync(PUBLIC_KEY_FILE, 'utf8');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Load private key PEM for the local user, also used by daemon to sign messages
|
|
81
|
+
*/
|
|
82
|
+
function loadPrivateKey() {
|
|
83
|
+
if (!fs.existsSync(PRIVATE_KEY_FILE)) {
|
|
84
|
+
throw new Error('No private key found. Run: ib start');
|
|
85
|
+
}
|
|
86
|
+
return fs.readFileSync(PRIVATE_KEY_FILE, 'utf8');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = {
|
|
90
|
+
loadOrCreateKeypair,
|
|
91
|
+
loadPublicKey,
|
|
92
|
+
loadPrivateKey,
|
|
93
|
+
PUBLIC_KEY_FILE,
|
|
94
|
+
PRIVATE_KEY_FILE,
|
|
95
|
+
};
|