diodejs 0.2.2 → 0.4.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/README.md +56 -5
- package/bindPort.js +446 -66
- package/clientManager.js +435 -0
- package/connection.js +186 -72
- package/examples/RPCTest.js +9 -7
- package/examples/nativeBindTest.js +55 -0
- package/examples/nativeForwardTest.js +80 -0
- package/examples/nativeTcpClientTest.js +56 -0
- package/examples/nativeUdpClientTest.js +40 -0
- package/examples/portForwardTest.js +5 -7
- package/examples/publishAndBind.js +6 -10
- package/examples/publishPortTest.js +4 -6
- package/index.js +8 -1
- package/logger.js +10 -5
- package/nativeCrypto.js +321 -0
- package/package.json +6 -8
- package/publishPort.js +575 -94
- package/rpc.js +161 -41
- package/testServers/udpTest.js +1 -2
- package/utils.js +42 -9
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const net = require('net');
|
|
2
|
+
const dgram = require('dgram');
|
|
3
|
+
const { KEYUTIL } = require('jsrsasign');
|
|
4
|
+
const { DiodeClientManager, PublishPort } = require('../index');
|
|
5
|
+
|
|
6
|
+
function printPublicKey(connection) {
|
|
7
|
+
try {
|
|
8
|
+
const pem = KEYUTIL.getPEM(connection.keyPair.pubKeyObj, 'PKCS8PUB');
|
|
9
|
+
console.log('Public key (PEM):');
|
|
10
|
+
console.log(pem);
|
|
11
|
+
} catch (error) {
|
|
12
|
+
console.error('Failed to print public key:', error);
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
console.log('Ethereum address:', connection.getEthereumAddress());
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error('Failed to print address:', error);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function startTcpEcho(port) {
|
|
22
|
+
const server = net.createServer((socket) => {
|
|
23
|
+
socket.setNoDelay(true);
|
|
24
|
+
socket.on('data', (data) => {
|
|
25
|
+
socket.write(data);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
server.listen(port, () => {
|
|
30
|
+
console.log(`TCP echo listening on ${port}`);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return server;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function startUdpEcho(port) {
|
|
37
|
+
const socket = dgram.createSocket('udp4');
|
|
38
|
+
socket.on('message', (msg, rinfo) => {
|
|
39
|
+
socket.send(msg, rinfo.port, rinfo.address);
|
|
40
|
+
});
|
|
41
|
+
socket.bind(port, () => {
|
|
42
|
+
console.log(`UDP echo listening on ${port}`);
|
|
43
|
+
});
|
|
44
|
+
return socket;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function main() {
|
|
48
|
+
const keyLocation = './db/keys.json';
|
|
49
|
+
|
|
50
|
+
const tcpServer = startTcpEcho(8089);
|
|
51
|
+
const udpSocket = startUdpEcho(8090);
|
|
52
|
+
|
|
53
|
+
const client = new DiodeClientManager({keyLocation });
|
|
54
|
+
await client.connect();
|
|
55
|
+
const [connection] = client.getConnections();
|
|
56
|
+
if (!connection) {
|
|
57
|
+
throw new Error('No relay connection available');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
printPublicKey(connection);
|
|
61
|
+
|
|
62
|
+
const publishPort = new PublishPort(client, {
|
|
63
|
+
8089: { mode: 'public' },
|
|
64
|
+
8090: { mode: 'public' }
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
console.log('Native forward ports published:', publishPort.getPublishedPorts());
|
|
68
|
+
|
|
69
|
+
const shutdown = () => {
|
|
70
|
+
try { client.close(); } catch {}
|
|
71
|
+
try { tcpServer.close(); } catch {}
|
|
72
|
+
try { udpSocket.close(); } catch {}
|
|
73
|
+
process.exit(0);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
process.on('SIGINT', shutdown);
|
|
77
|
+
process.on('SIGTERM', shutdown);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const net = require('net');
|
|
2
|
+
|
|
3
|
+
const host = process.argv[2] || '127.0.0.1';
|
|
4
|
+
const port = Number(process.argv[3] || 3005);
|
|
5
|
+
const message = process.argv.slice(4).join(' ') || 'hello-tcp';
|
|
6
|
+
const intervalMs = Number(process.env.INTERVAL_MS || 1000);
|
|
7
|
+
const connectTimeoutMs = Number(process.env.CONNECT_TIMEOUT_MS || 5000);
|
|
8
|
+
|
|
9
|
+
let intervalId = null;
|
|
10
|
+
let connected = false;
|
|
11
|
+
let counter = 0;
|
|
12
|
+
|
|
13
|
+
const client = net.createConnection({ host, port }, () => {
|
|
14
|
+
connected = true;
|
|
15
|
+
client.setNoDelay(true);
|
|
16
|
+
clearTimeout(connectTimer);
|
|
17
|
+
const sendPayload = () => {
|
|
18
|
+
const payload = `${message} #${counter++} ${new Date().toISOString()}`;
|
|
19
|
+
client.write(Buffer.from(payload, 'utf8'));
|
|
20
|
+
};
|
|
21
|
+
sendPayload();
|
|
22
|
+
intervalId = setInterval(sendPayload, intervalMs);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const connectTimer = setTimeout(() => {
|
|
26
|
+
if (connected) return;
|
|
27
|
+
console.error(`Connect timeout after ${connectTimeoutMs}ms`);
|
|
28
|
+
client.destroy();
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
}, connectTimeoutMs);
|
|
31
|
+
|
|
32
|
+
client.on('data', (data) => {
|
|
33
|
+
console.log(data.toString('utf8'));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
client.on('end', () => {
|
|
37
|
+
if (intervalId) clearInterval(intervalId);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
client.on('close', () => {
|
|
41
|
+
if (intervalId) clearInterval(intervalId);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
client.on('error', (err) => {
|
|
45
|
+
if (intervalId) clearInterval(intervalId);
|
|
46
|
+
console.error(`TCP error: ${err.message}`);
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const shutdown = () => {
|
|
51
|
+
if (intervalId) clearInterval(intervalId);
|
|
52
|
+
if (!client.destroyed) client.end();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
process.on('SIGINT', shutdown);
|
|
56
|
+
process.on('SIGTERM', shutdown);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const dgram = require('dgram');
|
|
2
|
+
|
|
3
|
+
const host = process.argv[2] || '127.0.0.1';
|
|
4
|
+
const port = Number(process.argv[3] || 3006);
|
|
5
|
+
const message = process.argv.slice(4).join(' ') || 'hello-udp';
|
|
6
|
+
const intervalMs = Number(process.env.INTERVAL_MS || 1000);
|
|
7
|
+
|
|
8
|
+
const socket = dgram.createSocket('udp4');
|
|
9
|
+
let intervalId = null;
|
|
10
|
+
let counter = 0;
|
|
11
|
+
|
|
12
|
+
const sendPayload = () => {
|
|
13
|
+
const payload = Buffer.from(`${message} #${counter++} ${new Date().toISOString()}`, 'utf8');
|
|
14
|
+
socket.send(payload, port, host);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
socket.on('message', (msg) => {
|
|
18
|
+
if (msg.length === 0 || (msg.length === 1 && msg[0] === 0)) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
console.log(msg.toString('utf8'));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
socket.on('error', (err) => {
|
|
25
|
+
if (intervalId) clearInterval(intervalId);
|
|
26
|
+
console.error(`UDP error: ${err.message}`);
|
|
27
|
+
socket.close();
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
sendPayload();
|
|
32
|
+
intervalId = setInterval(sendPayload, intervalMs);
|
|
33
|
+
|
|
34
|
+
const shutdown = () => {
|
|
35
|
+
if (intervalId) clearInterval(intervalId);
|
|
36
|
+
socket.close();
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
process.on('SIGINT', shutdown);
|
|
40
|
+
process.on('SIGTERM', shutdown);
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { DiodeClientManager, BindPort } = require('../index');
|
|
2
2
|
|
|
3
3
|
async function main() {
|
|
4
|
-
const host = 'us2.prenet.diode.io';
|
|
5
|
-
const port = 41046;
|
|
6
4
|
const keyLocation = './db/keys.json';
|
|
7
5
|
|
|
8
|
-
const
|
|
9
|
-
await
|
|
6
|
+
const client = new DiodeClientManager({ keyLocation });
|
|
7
|
+
await client.connect();
|
|
10
8
|
|
|
11
|
-
const portForward = new BindPort(
|
|
9
|
+
const portForward = new BindPort(client, {
|
|
12
10
|
3003: { targetPort: 8080, deviceIdHex: "0xca1e71d8105a598810578fb6042fa8cbc1e7f039", protocol: "tcp" },
|
|
13
11
|
3004: { targetPort: 8081, deviceIdHex: "5365baf29cb7ab58de588dfc448913cb609283e2", protocol: "tls" }
|
|
14
12
|
});
|
|
@@ -29,4 +27,4 @@ async function main() {
|
|
|
29
27
|
|
|
30
28
|
}
|
|
31
29
|
|
|
32
|
-
main().catch(console.error);
|
|
30
|
+
main().catch(console.error);
|
|
@@ -1,23 +1,19 @@
|
|
|
1
|
-
const
|
|
2
|
-
const PublishPort = require('../publishPort')
|
|
3
|
-
const BindPort = require('../bindPort')
|
|
1
|
+
const { DiodeClientManager, PublishPort, BindPort } = require('../index')
|
|
4
2
|
|
|
5
|
-
const host = 'us2.prenet.diode.io';
|
|
6
|
-
const port = 41046;
|
|
7
3
|
const keyLocation = './db/keys.json';
|
|
8
4
|
|
|
9
|
-
const
|
|
5
|
+
const client = new DiodeClientManager({ keyLocation });
|
|
10
6
|
|
|
11
7
|
async function main() {
|
|
12
|
-
await
|
|
8
|
+
await client.connect();
|
|
13
9
|
const publishedPorts = [8080]; // Ports you want to publish
|
|
14
|
-
const publishPort = new PublishPort(
|
|
10
|
+
const publishPort = new PublishPort(client, publishedPorts);
|
|
15
11
|
|
|
16
|
-
const portForward = new BindPort(
|
|
12
|
+
const portForward = new BindPort(client, 3002, 8080, "5365baf29cb7ab58de588dfc448913cb609283e2");
|
|
17
13
|
portForward.bind();
|
|
18
14
|
|
|
19
15
|
}
|
|
20
16
|
|
|
21
17
|
main();
|
|
22
18
|
|
|
23
|
-
|
|
19
|
+
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
// example.js
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { DiodeClientManager, PublishPort } = require('../index');
|
|
4
4
|
|
|
5
5
|
async function startPublishing() {
|
|
6
|
-
const host = 'us2.prenet.diode.io';
|
|
7
|
-
const port = 41046;
|
|
8
6
|
const keyLocation = './db/keys.json';
|
|
9
7
|
|
|
10
|
-
const
|
|
11
|
-
await
|
|
8
|
+
const client = new DiodeClientManager({ keyLocation });
|
|
9
|
+
await client.connect();
|
|
12
10
|
|
|
13
11
|
// Create a PublishPort instance with initial ports
|
|
14
|
-
const publishPort = new PublishPort(
|
|
12
|
+
const publishPort = new PublishPort(client, {
|
|
15
13
|
3000: { mode: 'public' },
|
|
16
14
|
8080: {
|
|
17
15
|
mode: 'private',
|
package/index.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
// index.js
|
|
2
2
|
const DiodeConnection = require('./connection');
|
|
3
|
+
const DiodeClientManager = require('./clientManager');
|
|
3
4
|
const DiodeRPC = require('./rpc');
|
|
4
5
|
const BindPort = require('./bindPort');
|
|
5
6
|
const PublishPort = require('./publishPort');
|
|
6
7
|
const makeReadable = require('./utils').makeReadable;
|
|
7
8
|
const logger = require('./logger');
|
|
8
|
-
|
|
9
|
+
process.on('unhandledRejection', (reason) => {
|
|
10
|
+
try { logger.warn(() => `Unhandled promise rejection: ${reason}`); } catch {}
|
|
11
|
+
});
|
|
12
|
+
process.on('uncaughtException', (err) => {
|
|
13
|
+
try { logger.error(() => `Uncaught exception: ${err.stack || err.message}`); } catch {}
|
|
14
|
+
});
|
|
15
|
+
module.exports = { DiodeConnection, DiodeClientManager, DiodeRPC, BindPort , PublishPort, makeReadable, logger };
|
package/logger.js
CHANGED
|
@@ -19,10 +19,15 @@ const options = {
|
|
|
19
19
|
|
|
20
20
|
const logger = setupLogger(options);
|
|
21
21
|
|
|
22
|
+
// Evaluate function args lazily to avoid building strings if logs are disabled
|
|
23
|
+
const evalArg = (a) => (typeof a === 'function' ? a() : a);
|
|
24
|
+
const mapArgs = (args) => args.map(evalArg);
|
|
25
|
+
const shouldDebug = isDebug && isLogEnabled;
|
|
26
|
+
|
|
22
27
|
// Wrap logger calls to respect debug mode
|
|
23
28
|
module.exports = {
|
|
24
|
-
debug: (...args) => { if (
|
|
25
|
-
info: (...args) => { if (isLogEnabled) logger.info(...args, 'app'); },
|
|
26
|
-
warn: (...args) => { if (isLogEnabled) logger.warn(...args, 'app'); },
|
|
27
|
-
error: (...args) => { if (isLogEnabled) logger.error(...args, 'app'); },
|
|
28
|
-
};
|
|
29
|
+
debug: (...args) => { if (shouldDebug) logger.debug(...mapArgs(args), 'app'); },
|
|
30
|
+
info: (...args) => { if (isLogEnabled) logger.info(...mapArgs(args), 'app'); },
|
|
31
|
+
warn: (...args) => { if (isLogEnabled) logger.warn(...mapArgs(args), 'app'); },
|
|
32
|
+
error: (...args) => { if (isLogEnabled) logger.error(...mapArgs(args), 'app'); },
|
|
33
|
+
};
|
package/nativeCrypto.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const secp256k1 = require('secp256k1');
|
|
3
|
+
const ethUtil = require('ethereumjs-util');
|
|
4
|
+
|
|
5
|
+
const DOMAIN = Buffer.from('diode-pp2-v1', 'utf8');
|
|
6
|
+
const UDP_MAGIC = Buffer.from('DUD1', 'utf8');
|
|
7
|
+
|
|
8
|
+
function toDeviceIdBuffer(deviceId) {
|
|
9
|
+
if (!deviceId) return Buffer.alloc(0);
|
|
10
|
+
if (Buffer.isBuffer(deviceId)) return deviceId;
|
|
11
|
+
if (deviceId instanceof Uint8Array) return Buffer.from(deviceId);
|
|
12
|
+
if (typeof deviceId === 'string') {
|
|
13
|
+
const hex = deviceId.toLowerCase().startsWith('0x') ? deviceId.slice(2) : deviceId;
|
|
14
|
+
return Buffer.from(hex, 'hex');
|
|
15
|
+
}
|
|
16
|
+
return Buffer.alloc(0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function encodeHandshakeData(role, deviceId, ephPub, nonce, physicalPort) {
|
|
20
|
+
const deviceIdBuf = toDeviceIdBuffer(deviceId);
|
|
21
|
+
const portBuf = Buffer.alloc(4);
|
|
22
|
+
portBuf.writeUInt32BE(physicalPort >>> 0, 0);
|
|
23
|
+
return Buffer.concat([
|
|
24
|
+
DOMAIN,
|
|
25
|
+
Buffer.from(role, 'utf8'),
|
|
26
|
+
deviceIdBuf,
|
|
27
|
+
ephPub,
|
|
28
|
+
nonce,
|
|
29
|
+
portBuf,
|
|
30
|
+
]);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function signHandshake(privateKey, data) {
|
|
34
|
+
const hash = ethUtil.keccak256(data);
|
|
35
|
+
const sig = ethUtil.ecsign(hash, privateKey);
|
|
36
|
+
return Buffer.concat([sig.r, sig.s, Buffer.from([sig.v])]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function verifyHandshakeSignature(data, signature, expectedDeviceId) {
|
|
40
|
+
if (!signature || signature.length !== 65) {
|
|
41
|
+
return { ok: false, reason: 'Invalid signature length' };
|
|
42
|
+
}
|
|
43
|
+
const hash = ethUtil.keccak256(data);
|
|
44
|
+
const r = signature.slice(0, 32);
|
|
45
|
+
const s = signature.slice(32, 64);
|
|
46
|
+
let v = signature[64];
|
|
47
|
+
if (v < 27) v += 27;
|
|
48
|
+
let pubKey;
|
|
49
|
+
try {
|
|
50
|
+
pubKey = ethUtil.ecrecover(hash, v, r, s);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return { ok: false, reason: 'Invalid signature' };
|
|
53
|
+
}
|
|
54
|
+
const addr = `0x${ethUtil.pubToAddress(pubKey).toString('hex')}`.toLowerCase();
|
|
55
|
+
if (expectedDeviceId && addr !== expectedDeviceId.toLowerCase()) {
|
|
56
|
+
return { ok: false, reason: 'Device mismatch', address: addr };
|
|
57
|
+
}
|
|
58
|
+
return { ok: true, address: addr };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function generateEphemeralKeyPair() {
|
|
62
|
+
let privKey;
|
|
63
|
+
do {
|
|
64
|
+
privKey = crypto.randomBytes(32);
|
|
65
|
+
} while (!secp256k1.privateKeyVerify(privKey));
|
|
66
|
+
const pubKey = Buffer.from(secp256k1.publicKeyCreate(privKey, true)); // compressed 33 bytes
|
|
67
|
+
return { privKey, pubKey };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function createHandshakeMessage({ role, deviceId, physicalPort, privateKey }) {
|
|
71
|
+
const { privKey, pubKey } = generateEphemeralKeyPair();
|
|
72
|
+
const nonce = crypto.randomBytes(16);
|
|
73
|
+
const data = encodeHandshakeData(role, deviceId, pubKey, nonce, physicalPort);
|
|
74
|
+
const sig = signHandshake(privateKey, data);
|
|
75
|
+
const message = {
|
|
76
|
+
v: 1,
|
|
77
|
+
role,
|
|
78
|
+
deviceId: deviceId.toLowerCase(),
|
|
79
|
+
physicalPort,
|
|
80
|
+
ephPub: pubKey.toString('hex'),
|
|
81
|
+
nonce: nonce.toString('hex'),
|
|
82
|
+
sig: sig.toString('hex'),
|
|
83
|
+
};
|
|
84
|
+
return { message, privKey, nonce };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function verifyHandshakeMessage(message, { expectedRole, expectedDeviceId, expectedPhysicalPort }) {
|
|
88
|
+
if (!message || message.v !== 1) {
|
|
89
|
+
return { ok: false, reason: 'Invalid version' };
|
|
90
|
+
}
|
|
91
|
+
if (expectedRole && message.role !== expectedRole) {
|
|
92
|
+
return { ok: false, reason: 'Role mismatch' };
|
|
93
|
+
}
|
|
94
|
+
if (expectedPhysicalPort && Number(message.physicalPort) !== Number(expectedPhysicalPort)) {
|
|
95
|
+
return { ok: false, reason: 'Physical port mismatch' };
|
|
96
|
+
}
|
|
97
|
+
if (expectedDeviceId && message.deviceId && message.deviceId.toLowerCase() !== expectedDeviceId.toLowerCase()) {
|
|
98
|
+
return { ok: false, reason: 'Device mismatch' };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const ephPub = Buffer.from(message.ephPub, 'hex');
|
|
102
|
+
const nonce = Buffer.from(message.nonce, 'hex');
|
|
103
|
+
const sig = Buffer.from(message.sig, 'hex');
|
|
104
|
+
const data = encodeHandshakeData(message.role, message.deviceId, ephPub, nonce, message.physicalPort);
|
|
105
|
+
const verification = verifyHandshakeSignature(data, sig, message.deviceId);
|
|
106
|
+
if (!verification.ok) {
|
|
107
|
+
return { ok: false, reason: verification.reason };
|
|
108
|
+
}
|
|
109
|
+
return { ok: true, deviceId: message.deviceId, ephPub, nonce };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function deriveSessionKeys({
|
|
113
|
+
role,
|
|
114
|
+
localDeviceId,
|
|
115
|
+
remoteDeviceId,
|
|
116
|
+
localEphPriv,
|
|
117
|
+
remoteEphPub,
|
|
118
|
+
localNonce,
|
|
119
|
+
remoteNonce,
|
|
120
|
+
physicalPort,
|
|
121
|
+
}) {
|
|
122
|
+
const shared = Buffer.from(secp256k1.ecdh(remoteEphPub, localEphPriv));
|
|
123
|
+
const bindFirst = role === 'bind';
|
|
124
|
+
const nonceBind = bindFirst ? localNonce : remoteNonce;
|
|
125
|
+
const noncePublish = bindFirst ? remoteNonce : localNonce;
|
|
126
|
+
const idBind = toDeviceIdBuffer(bindFirst ? localDeviceId : remoteDeviceId);
|
|
127
|
+
const idPublish = toDeviceIdBuffer(bindFirst ? remoteDeviceId : localDeviceId);
|
|
128
|
+
const portBuf = Buffer.alloc(4);
|
|
129
|
+
portBuf.writeUInt32BE(physicalPort >>> 0, 0);
|
|
130
|
+
const salt = crypto.createHash('sha256')
|
|
131
|
+
.update(Buffer.concat([nonceBind, noncePublish, idBind, idPublish, portBuf]))
|
|
132
|
+
.digest();
|
|
133
|
+
const info = Buffer.from('diode-pp2-v1', 'utf8');
|
|
134
|
+
const keyMaterial = Buffer.from(crypto.hkdfSync('sha256', shared, salt, info, 72));
|
|
135
|
+
|
|
136
|
+
let txKey = keyMaterial.slice(0, 32);
|
|
137
|
+
let rxKey = keyMaterial.slice(32, 64);
|
|
138
|
+
let txSalt = keyMaterial.slice(64, 68);
|
|
139
|
+
let rxSalt = keyMaterial.slice(68, 72);
|
|
140
|
+
|
|
141
|
+
if (role === 'publish') {
|
|
142
|
+
[txKey, rxKey] = [rxKey, txKey];
|
|
143
|
+
[txSalt, rxSalt] = [rxSalt, txSalt];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
txKey,
|
|
148
|
+
rxKey,
|
|
149
|
+
txSalt,
|
|
150
|
+
rxSalt,
|
|
151
|
+
txCounter: 0n,
|
|
152
|
+
rxCounter: 0n,
|
|
153
|
+
rxBuffer: Buffer.alloc(0),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildNonce(salt, counter) {
|
|
158
|
+
const nonce = Buffer.alloc(12);
|
|
159
|
+
salt.copy(nonce, 0, 0, 4);
|
|
160
|
+
nonce.writeBigUInt64BE(counter, 4);
|
|
161
|
+
return nonce;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function encryptAead(key, nonce, plaintext, aad) {
|
|
165
|
+
const cipher = crypto.createCipheriv('chacha20-poly1305', key, nonce, { authTagLength: 16 });
|
|
166
|
+
if (aad) cipher.setAAD(aad);
|
|
167
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
168
|
+
const tag = cipher.getAuthTag();
|
|
169
|
+
return { ciphertext, tag };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function decryptAead(key, nonce, ciphertext, tag, aad) {
|
|
173
|
+
const decipher = crypto.createDecipheriv('chacha20-poly1305', key, nonce, { authTagLength: 16 });
|
|
174
|
+
if (aad) decipher.setAAD(aad);
|
|
175
|
+
decipher.setAuthTag(tag);
|
|
176
|
+
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
177
|
+
return plaintext;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function createTcpFrame(session, plaintext) {
|
|
181
|
+
const counter = session.txCounter++;
|
|
182
|
+
const nonce = buildNonce(session.txSalt, counter);
|
|
183
|
+
const lenBuf = Buffer.alloc(4);
|
|
184
|
+
lenBuf.writeUInt32BE(plaintext.length, 0);
|
|
185
|
+
const counterBuf = Buffer.alloc(8);
|
|
186
|
+
counterBuf.writeBigUInt64BE(counter, 0);
|
|
187
|
+
const aad = Buffer.concat([lenBuf, counterBuf]);
|
|
188
|
+
const { ciphertext, tag } = encryptAead(session.txKey, nonce, plaintext, aad);
|
|
189
|
+
return Buffer.concat([lenBuf, counterBuf, ciphertext, tag]);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function consumeTcpFrames(session, data) {
|
|
193
|
+
const messages = [];
|
|
194
|
+
let buffer = Buffer.concat([session.rxBuffer, data]);
|
|
195
|
+
while (buffer.length >= 12) {
|
|
196
|
+
const len = buffer.readUInt32BE(0);
|
|
197
|
+
const counter = buffer.readBigUInt64BE(4);
|
|
198
|
+
const frameLength = 12 + len + 16;
|
|
199
|
+
if (buffer.length < frameLength) break;
|
|
200
|
+
if (counter < session.rxCounter) {
|
|
201
|
+
buffer = buffer.slice(frameLength);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const nonce = buildNonce(session.rxSalt, counter);
|
|
205
|
+
const aad = buffer.slice(0, 12);
|
|
206
|
+
const ciphertext = buffer.slice(12, 12 + len);
|
|
207
|
+
const tag = buffer.slice(12 + len, frameLength);
|
|
208
|
+
try {
|
|
209
|
+
const plaintext = decryptAead(session.rxKey, nonce, ciphertext, tag, aad);
|
|
210
|
+
messages.push(plaintext);
|
|
211
|
+
session.rxCounter = counter + 1n;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
throw new Error('TCP decrypt failed');
|
|
214
|
+
}
|
|
215
|
+
buffer = buffer.slice(frameLength);
|
|
216
|
+
}
|
|
217
|
+
session.rxBuffer = buffer;
|
|
218
|
+
return messages;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function createUdpPacket(session, plaintext) {
|
|
222
|
+
const counter = session.txCounter++;
|
|
223
|
+
const nonce = buildNonce(session.txSalt, counter);
|
|
224
|
+
const counterBuf = Buffer.alloc(8);
|
|
225
|
+
counterBuf.writeBigUInt64BE(counter, 0);
|
|
226
|
+
const aad = Buffer.concat([UDP_MAGIC, counterBuf]);
|
|
227
|
+
const { ciphertext, tag } = encryptAead(session.txKey, nonce, plaintext, aad);
|
|
228
|
+
return Buffer.concat([UDP_MAGIC, counterBuf, ciphertext, tag]);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function parseUdpPacket(session, packet) {
|
|
232
|
+
if (packet.length < UDP_MAGIC.length + 8 + 16) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
if (!packet.slice(0, 4).equals(UDP_MAGIC)) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
const counter = packet.readBigUInt64BE(4);
|
|
239
|
+
if (counter < session.rxCounter) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const nonce = buildNonce(session.rxSalt, counter);
|
|
243
|
+
const aad = packet.slice(0, 12);
|
|
244
|
+
const ciphertext = packet.slice(12, packet.length - 16);
|
|
245
|
+
const tag = packet.slice(packet.length - 16);
|
|
246
|
+
try {
|
|
247
|
+
const plaintext = decryptAead(session.rxKey, nonce, ciphertext, tag, aad);
|
|
248
|
+
session.rxCounter = counter + 1n;
|
|
249
|
+
return plaintext;
|
|
250
|
+
} catch (error) {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function writeHandshakeMessage(socket, message) {
|
|
256
|
+
const payload = Buffer.from(JSON.stringify(message), 'utf8');
|
|
257
|
+
const lenBuf = Buffer.alloc(4);
|
|
258
|
+
lenBuf.writeUInt32BE(payload.length, 0);
|
|
259
|
+
socket.write(Buffer.concat([lenBuf, payload]));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function readHandshakeMessage(socket, timeoutMs = 10000) {
|
|
263
|
+
return new Promise((resolve, reject) => {
|
|
264
|
+
let buffer = Buffer.alloc(0);
|
|
265
|
+
let settled = false;
|
|
266
|
+
const timer = setTimeout(() => {
|
|
267
|
+
cleanup();
|
|
268
|
+
reject(new Error('Handshake timeout'));
|
|
269
|
+
}, timeoutMs);
|
|
270
|
+
|
|
271
|
+
const cleanup = () => {
|
|
272
|
+
if (settled) return;
|
|
273
|
+
settled = true;
|
|
274
|
+
clearTimeout(timer);
|
|
275
|
+
socket.off('data', onData);
|
|
276
|
+
socket.off('error', onError);
|
|
277
|
+
socket.off('close', onClose);
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const onError = (err) => {
|
|
281
|
+
cleanup();
|
|
282
|
+
reject(err);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const onClose = () => {
|
|
286
|
+
cleanup();
|
|
287
|
+
reject(new Error('Handshake socket closed'));
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const onData = (chunk) => {
|
|
291
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
292
|
+
if (buffer.length < 4) return;
|
|
293
|
+
const len = buffer.readUInt32BE(0);
|
|
294
|
+
if (buffer.length < 4 + len) return;
|
|
295
|
+
const payload = buffer.slice(4, 4 + len);
|
|
296
|
+
cleanup();
|
|
297
|
+
try {
|
|
298
|
+
const message = JSON.parse(payload.toString('utf8'));
|
|
299
|
+
resolve(message);
|
|
300
|
+
} catch (error) {
|
|
301
|
+
reject(error);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
socket.on('data', onData);
|
|
306
|
+
socket.on('error', onError);
|
|
307
|
+
socket.on('close', onClose);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
module.exports = {
|
|
312
|
+
createHandshakeMessage,
|
|
313
|
+
verifyHandshakeMessage,
|
|
314
|
+
deriveSessionKeys,
|
|
315
|
+
writeHandshakeMessage,
|
|
316
|
+
readHandshakeMessage,
|
|
317
|
+
createTcpFrame,
|
|
318
|
+
consumeTcpFrames,
|
|
319
|
+
createUdpPacket,
|
|
320
|
+
parseUdpPacket,
|
|
321
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "diodejs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "A JavaScript client for interacting with the Diode network. It provides functionalities to bind and publish ports, send RPC commands, and handle responses.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -11,20 +11,18 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@ethereumjs/rlp": "^5.0.2",
|
|
13
13
|
"asn1.js": "^5.4.1",
|
|
14
|
-
"buffer": "^6.0.3",
|
|
15
|
-
"crypto": "^1.0.1",
|
|
16
14
|
"dera-logger": "^2.0.0",
|
|
17
|
-
"dgram": "^1.0.1",
|
|
18
15
|
"dotenv": "^16.4.7",
|
|
19
16
|
"ethereumjs-abi": "^0.6.8",
|
|
20
17
|
"ethereumjs-util": "^7.1.5",
|
|
21
18
|
"ethers": "^6.13.2",
|
|
22
|
-
"fs": "^0.0.1-security",
|
|
23
19
|
"jsrsasign": "^11.1.0",
|
|
24
|
-
"
|
|
20
|
+
"keccak": "^3.0.4",
|
|
25
21
|
"node-fetch": "^2.7.0",
|
|
26
22
|
"rlp": "^3.0.0",
|
|
27
|
-
"secp256k1": "^5.0.1"
|
|
28
|
-
|
|
23
|
+
"secp256k1": "^5.0.1"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
29
27
|
}
|
|
30
28
|
}
|