@manch1kz/yanac 1.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 +33 -0
- package/app.ts +29 -0
- package/build/app.js +23 -0
- package/build/client/clientInstance.js +36 -0
- package/build/client/handlers/mainInputHandler.js +21 -0
- package/build/client/handlers/processMessage.js +41 -0
- package/build/security/sessionManager.js +74 -0
- package/build/server/handlers/processMessage.js +48 -0
- package/build/server/handlers/setupSocket.js +20 -0
- package/build/server/serverInstance.js +28 -0
- package/build/utils/edUtils.js +33 -0
- package/build/utils/genNameAlias.js +10 -0
- package/build/utils/localChatStore.js +21 -0
- package/client/clientInstance.ts +34 -0
- package/client/handlers/mainInputHandler.ts +21 -0
- package/client/handlers/processMessage.ts +47 -0
- package/package.json +36 -0
- package/security/sessionManager.ts +78 -0
- package/server/handlers/processMessage.ts +57 -0
- package/server/handlers/setupSocket.ts +26 -0
- package/server/serverInstance.ts +25 -0
- package/tsconfig.json +14 -0
- package/utils/edUtils.ts +37 -0
- package/utils/genNameAlias.ts +7 -0
- package/utils/localChatStore.ts +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
## Installation
|
|
2
|
+
```npm install -g @manch1kz/yanac```
|
|
3
|
+
|
|
4
|
+
## Using
|
|
5
|
+
```npm i``` before using if you using this from the source code
|
|
6
|
+
|
|
7
|
+
Starting the server:
|
|
8
|
+
```
|
|
9
|
+
yanac start server [options]
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Starting the сlient:
|
|
13
|
+
```
|
|
14
|
+
yanac start client [options]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Options for server:
|
|
18
|
+
* ```--port <limit>``` - port (default: 3000)
|
|
19
|
+
* ```--limit <limit>``` - chat maximum size (default: 100)
|
|
20
|
+
|
|
21
|
+
Options for client:
|
|
22
|
+
* ```--port <limit>``` - port (default: 3000)
|
|
23
|
+
* ```--host <limit>``` - host (default: default)
|
|
24
|
+
* ```--name <limit>``` - port (default: random_string)
|
|
25
|
+
|
|
26
|
+
## Communication demo
|
|
27
|
+
https://github.com/user-attachments/assets/37b0010c-6ea2-436c-96b0-f66d71bb3a7a
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
package/app.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander"
|
|
4
|
+
import { Server } from "./server/serverInstance"
|
|
5
|
+
import { Client } from "./client/clientInstance"
|
|
6
|
+
|
|
7
|
+
const program = new Command()
|
|
8
|
+
|
|
9
|
+
program.command("start <type>")
|
|
10
|
+
.option("--port <port>", "Server port (default: 3000)")
|
|
11
|
+
.option("--host <host>", "Server host (default: localhost)")
|
|
12
|
+
.option("--name <name>", "Client name (default: random_str)")
|
|
13
|
+
.option("--limit <limit>", "Limit of chat storage (default: 100)")
|
|
14
|
+
.action((str, options) => {
|
|
15
|
+
switch (str) {
|
|
16
|
+
case "server":
|
|
17
|
+
const server = new Server(
|
|
18
|
+
Number(options.limit) ? Number(options.limit) : undefined,
|
|
19
|
+
Number(options.port) ? Number(options.port) : undefined
|
|
20
|
+
)
|
|
21
|
+
break;
|
|
22
|
+
case "client":
|
|
23
|
+
const client = new Client(options.name, options.host, Number(options.port) ? Number(options.port) : undefined)
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
program.parse()
|
|
29
|
+
|
package/build/app.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const serverInstance_1 = require("./server/serverInstance");
|
|
6
|
+
const clientInstance_1 = require("./client/clientInstance");
|
|
7
|
+
const program = new commander_1.Command();
|
|
8
|
+
program.command("start <type>")
|
|
9
|
+
.option("--port <port>", "Server port (default: 3000)")
|
|
10
|
+
.option("--host <host>", "Server host (default: localhost)")
|
|
11
|
+
.option("--name <name>", "Client name (default: random_str)")
|
|
12
|
+
.option("--limit <limit>", "Limit of chat storage (default: 100)")
|
|
13
|
+
.action((str, options) => {
|
|
14
|
+
switch (str) {
|
|
15
|
+
case "server":
|
|
16
|
+
const server = new serverInstance_1.Server(Number(options.limit) ? Number(options.limit) : undefined, Number(options.port) ? Number(options.port) : undefined);
|
|
17
|
+
break;
|
|
18
|
+
case "client":
|
|
19
|
+
const client = new clientInstance_1.Client(options.name, options.host, Number(options.port) ? Number(options.port) : undefined);
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
program.parse();
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Client = void 0;
|
|
7
|
+
const net_1 = __importDefault(require("net"));
|
|
8
|
+
const promises_1 = __importDefault(require("readline/promises"));
|
|
9
|
+
const sessionManager_1 = require("../security/sessionManager");
|
|
10
|
+
const processMessage_1 = require("./handlers/processMessage");
|
|
11
|
+
const mainInputHandler_1 = require("./handlers/mainInputHandler");
|
|
12
|
+
const genNameAlias_1 = require("../utils/genNameAlias");
|
|
13
|
+
class Client {
|
|
14
|
+
manager;
|
|
15
|
+
rl;
|
|
16
|
+
name;
|
|
17
|
+
server;
|
|
18
|
+
key_generated;
|
|
19
|
+
constructor(name = (0, genNameAlias_1.genName)(), host = "localhost", port = 3000) {
|
|
20
|
+
this.name = name;
|
|
21
|
+
this.server = net_1.default.connect({ host: host, port: port });
|
|
22
|
+
this.key_generated = false;
|
|
23
|
+
this.manager = new sessionManager_1.SessionManager();
|
|
24
|
+
this.rl = promises_1.default.createInterface({ input: process.stdin, output: process.stdout, prompt: `${this.name} >>> ` });
|
|
25
|
+
this.server.on("connect", () => {
|
|
26
|
+
this.server.write(JSON.stringify({ type: 'public_key_request', body: this.manager.getClient() }));
|
|
27
|
+
});
|
|
28
|
+
this.server.on("data", (data) => {
|
|
29
|
+
processMessage_1.processMessage.call(this, data);
|
|
30
|
+
});
|
|
31
|
+
this.rl.on("line", mainInputHandler_1.onLine.bind(this));
|
|
32
|
+
this.server.on("close", () => { this.server.end(); process.exit(); });
|
|
33
|
+
this.rl.on("SIGINT", () => { this.server.end(); process.exit(); });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.Client = Client;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.onLine = onLine;
|
|
4
|
+
const edUtils_1 = require("../../utils/edUtils");
|
|
5
|
+
function onLine(line) {
|
|
6
|
+
if (line.length > 0) {
|
|
7
|
+
process.stdout.moveCursor(0, -1);
|
|
8
|
+
process.stdout.write('\r\x1B[K');
|
|
9
|
+
console.log(`${this.name}: ${line}`);
|
|
10
|
+
if (line == "/exit") {
|
|
11
|
+
this.rl.emit("SIGINT");
|
|
12
|
+
}
|
|
13
|
+
this.server.write((0, edUtils_1.encrypt)(JSON.stringify({ type: 'msg', body: [this.name, line] }), this.manager.session_key));
|
|
14
|
+
this.rl.prompt();
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
process.stdout.moveCursor(0, -1);
|
|
18
|
+
process.stdout.write('\r\x1B[K');
|
|
19
|
+
this.rl.prompt();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.processMessage = processMessage;
|
|
4
|
+
const edUtils_1 = require("../../utils/edUtils");
|
|
5
|
+
async function processMessage(data) {
|
|
6
|
+
const cur = this.rl.getCursorPos();
|
|
7
|
+
let processed;
|
|
8
|
+
if (this.key_generated) {
|
|
9
|
+
processed = JSON.parse((0, edUtils_1.decrypt)(data.toString(), this.manager.session_key));
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
processed = JSON.parse(data.toString('utf-8'));
|
|
13
|
+
}
|
|
14
|
+
switch (processed.type) {
|
|
15
|
+
case 'public_key_response':
|
|
16
|
+
this.manager.setServer(processed.body[1]);
|
|
17
|
+
this.server.write(JSON.stringify({ type: 'master_key_response', body: [this.manager.getMaster(processed.body[0]), this.name] }));
|
|
18
|
+
break;
|
|
19
|
+
case 'finished':
|
|
20
|
+
this.manager.generateSession();
|
|
21
|
+
this.key_generated = true;
|
|
22
|
+
this.server.write((0, edUtils_1.encrypt)(JSON.stringify({ type: "finished", body: this.name }), this.manager.session_key));
|
|
23
|
+
this.rl.prompt();
|
|
24
|
+
break;
|
|
25
|
+
case 'history':
|
|
26
|
+
process.stdout.write('\r\x1B[K');
|
|
27
|
+
for (let i = 0; i < processed.body.length; i++) {
|
|
28
|
+
console.log(processed.body[i][0] + ':', processed.body[i][1]);
|
|
29
|
+
}
|
|
30
|
+
console.log('[SERVER]:', this.name, 'connected to server');
|
|
31
|
+
this.rl.prompt(true);
|
|
32
|
+
process.stdout.cursorTo(cur.cols);
|
|
33
|
+
break;
|
|
34
|
+
case 'msg':
|
|
35
|
+
process.stdout.write('\r\x1B[K');
|
|
36
|
+
console.log(processed.data[0] + ':', processed.data[1]);
|
|
37
|
+
this.rl.prompt(true);
|
|
38
|
+
process.stdout.cursorTo(cur.cols);
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SessionManager = void 0;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
class SessionManager {
|
|
9
|
+
client_random;
|
|
10
|
+
server_random;
|
|
11
|
+
master_random;
|
|
12
|
+
session_key;
|
|
13
|
+
privateKey;
|
|
14
|
+
publicKey;
|
|
15
|
+
constructor() {
|
|
16
|
+
this.client_random = null;
|
|
17
|
+
this.server_random = null;
|
|
18
|
+
this.master_random = null;
|
|
19
|
+
this.session_key = null;
|
|
20
|
+
const { publicKey, privateKey } = crypto_1.default.generateKeyPairSync('rsa', {
|
|
21
|
+
modulusLength: 2048,
|
|
22
|
+
publicKeyEncoding: {
|
|
23
|
+
format: 'pem',
|
|
24
|
+
type: 'pkcs1'
|
|
25
|
+
},
|
|
26
|
+
privateKeyEncoding: {
|
|
27
|
+
format: 'pem',
|
|
28
|
+
type: 'pkcs1'
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
this.privateKey = privateKey;
|
|
32
|
+
this.publicKey = publicKey;
|
|
33
|
+
}
|
|
34
|
+
generateSession() {
|
|
35
|
+
if (this.client_random && this.server_random && this.master_random) {
|
|
36
|
+
const sum = this.client_random + this.server_random + Number(this.master_random);
|
|
37
|
+
this.session_key = crypto_1.default.createHash('sha256').update(String(sum)).digest().toString('hex');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
setClient(client_random) {
|
|
41
|
+
this.client_random = client_random;
|
|
42
|
+
}
|
|
43
|
+
getClient() {
|
|
44
|
+
if (this.client_random == null) {
|
|
45
|
+
this.client_random = Math.floor(Math.random() * 5000);
|
|
46
|
+
}
|
|
47
|
+
return this.client_random;
|
|
48
|
+
}
|
|
49
|
+
getServer() {
|
|
50
|
+
if (this.server_random == null) {
|
|
51
|
+
this.server_random = Math.floor(Math.random() * 5000);
|
|
52
|
+
}
|
|
53
|
+
return this.server_random;
|
|
54
|
+
}
|
|
55
|
+
setServer(server_random) {
|
|
56
|
+
this.server_random = server_random;
|
|
57
|
+
}
|
|
58
|
+
getMaster(public_key) {
|
|
59
|
+
if (this.master_random == null) {
|
|
60
|
+
this.master_random = String(Math.floor(Math.random() * 5000));
|
|
61
|
+
}
|
|
62
|
+
return crypto_1.default.publicEncrypt({
|
|
63
|
+
key: public_key,
|
|
64
|
+
padding: crypto_1.default.constants.RSA_PKCS1_OAEP_PADDING,
|
|
65
|
+
}, this.master_random).toString('base64');
|
|
66
|
+
}
|
|
67
|
+
setMaster(master_random, private_key) {
|
|
68
|
+
this.master_random = crypto_1.default.privateDecrypt({
|
|
69
|
+
key: private_key,
|
|
70
|
+
padding: crypto_1.default.constants.RSA_PKCS1_OAEP_PADDING,
|
|
71
|
+
}, Buffer.from(master_random, 'base64')).toString();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.SessionManager = SessionManager;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.processMessage = processMessage;
|
|
4
|
+
const edUtils_1 = require("../../utils/edUtils");
|
|
5
|
+
function processMessage(socket, data) {
|
|
6
|
+
const socket_item = this.connections.get(socket);
|
|
7
|
+
const socket_generator = socket_item.generator;
|
|
8
|
+
let message;
|
|
9
|
+
if (socket_item.key_generated) {
|
|
10
|
+
message = JSON.parse((0, edUtils_1.decrypt)(data.toString(), socket_item.generator.session_key));
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
message = JSON.parse(data.toString());
|
|
14
|
+
}
|
|
15
|
+
switch (message.type) {
|
|
16
|
+
case 'msg':
|
|
17
|
+
this.localChatStore.push(message.body);
|
|
18
|
+
Array.from(this.connections.keys()).forEach((i) => {
|
|
19
|
+
if (i != socket) {
|
|
20
|
+
i.write((0, edUtils_1.encrypt)(JSON.stringify({ type: 'msg', data: message.body }), this.connections.get(i).generator.session_key));
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
break;
|
|
24
|
+
case 'public_key_request':
|
|
25
|
+
const server_random = this.manager.getServer();
|
|
26
|
+
socket_generator.setClient(message.body);
|
|
27
|
+
socket_generator.setServer(server_random);
|
|
28
|
+
socket.write(JSON.stringify({ type: 'public_key_response', body: [this.manager.publicKey, server_random] }));
|
|
29
|
+
break;
|
|
30
|
+
case 'master_key_response':
|
|
31
|
+
socket_generator.setMaster(message.body[0], this.manager.privateKey);
|
|
32
|
+
socket_generator.generateSession();
|
|
33
|
+
socket_item.name = message.body[1];
|
|
34
|
+
socket_item.key_generated = true;
|
|
35
|
+
socket.write(JSON.stringify({ type: 'finished' }));
|
|
36
|
+
break;
|
|
37
|
+
case 'finished':
|
|
38
|
+
socket_item.name = message.body;
|
|
39
|
+
socket.write((0, edUtils_1.encrypt)(JSON.stringify({ type: 'history', body: this.localChatStore.getChat() }), socket_generator.session_key));
|
|
40
|
+
this.localChatStore.push(['[SERVER]', message.body + ' connected to server']);
|
|
41
|
+
Array.from(this.connections.keys()).forEach((i) => {
|
|
42
|
+
if (i != socket) {
|
|
43
|
+
i.write((0, edUtils_1.encrypt)(JSON.stringify({ type: 'msg', data: ['[SERVER]', message.body + ' connected to server'] }), this.connections.get(i).generator.session_key));
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setupSocket = setupSocket;
|
|
4
|
+
const sessionManager_1 = require("../../security/sessionManager");
|
|
5
|
+
const edUtils_1 = require("../../utils/edUtils");
|
|
6
|
+
function setupSocket(socket, cb) {
|
|
7
|
+
this.connections.set(socket, { name: null, generator: new sessionManager_1.SessionManager(), key_generated: false });
|
|
8
|
+
socket.on('close', () => {
|
|
9
|
+
let body = ['[SERVER]', this.connections.get(socket).name + ' disconnected from server'];
|
|
10
|
+
this.localChatStore.push(body);
|
|
11
|
+
Array.from(this.connections.keys()).forEach((i) => {
|
|
12
|
+
i.write((0, edUtils_1.encrypt)(JSON.stringify({ type: 'msg', data: body }), this.connections.get(i).generator.session_key));
|
|
13
|
+
});
|
|
14
|
+
this.connections.delete(socket);
|
|
15
|
+
});
|
|
16
|
+
socket.on("error", () => { });
|
|
17
|
+
socket.on('data', (data) => {
|
|
18
|
+
cb(socket, data);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Server = void 0;
|
|
7
|
+
const net_1 = __importDefault(require("net"));
|
|
8
|
+
const sessionManager_1 = require("../security/sessionManager");
|
|
9
|
+
const localChatStore_1 = require("../utils/localChatStore");
|
|
10
|
+
const processMessage_1 = require("./handlers/processMessage");
|
|
11
|
+
const setupSocket_1 = require("./handlers/setupSocket");
|
|
12
|
+
class Server {
|
|
13
|
+
localChatStore;
|
|
14
|
+
server;
|
|
15
|
+
manager;
|
|
16
|
+
connections;
|
|
17
|
+
constructor(limit = 100, port = 3000) {
|
|
18
|
+
this.localChatStore = new localChatStore_1.LocalChatStore(100);
|
|
19
|
+
this.manager = new sessionManager_1.SessionManager();
|
|
20
|
+
this.connections = new Map();
|
|
21
|
+
this.server = net_1.default.createServer();
|
|
22
|
+
this.server.on("connection", (socket) => {
|
|
23
|
+
setupSocket_1.setupSocket.call(this, socket, processMessage_1.processMessage.bind(this));
|
|
24
|
+
});
|
|
25
|
+
this.server.listen(port);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.Server = Server;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.encrypt = encrypt;
|
|
7
|
+
exports.decrypt = decrypt;
|
|
8
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
9
|
+
function formatKey(key) {
|
|
10
|
+
if (key.length > 32) {
|
|
11
|
+
key = key.slice(0, 32);
|
|
12
|
+
}
|
|
13
|
+
else if (key.length < 32) {
|
|
14
|
+
key += Array.from({ length: 32 - key.length }, () => 0).join('');
|
|
15
|
+
}
|
|
16
|
+
return key;
|
|
17
|
+
}
|
|
18
|
+
function encrypt(value, key) {
|
|
19
|
+
key = formatKey(key);
|
|
20
|
+
const iv = Buffer.alloc(16, 0);
|
|
21
|
+
const cipher = crypto_1.default.createCipheriv("aes-256-cbc", key, iv);
|
|
22
|
+
let encrypted = cipher.update(value, "utf-8", "hex");
|
|
23
|
+
encrypted += cipher.final("hex");
|
|
24
|
+
return encrypted;
|
|
25
|
+
}
|
|
26
|
+
function decrypt(value, key) {
|
|
27
|
+
key = formatKey(key);
|
|
28
|
+
const iv = Buffer.alloc(16, 0);
|
|
29
|
+
const decipher = crypto_1.default.createDecipheriv("aes-256-cbc", key, iv);
|
|
30
|
+
let decrypted = decipher.update(value, "hex", "utf-8");
|
|
31
|
+
decrypted += decipher.final("utf-8");
|
|
32
|
+
return decrypted;
|
|
33
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.genName = genName;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
function genName() {
|
|
9
|
+
return crypto_1.default.randomBytes(8).toString("hex");
|
|
10
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LocalChatStore = void 0;
|
|
4
|
+
class LocalChatStore {
|
|
5
|
+
chat;
|
|
6
|
+
limit;
|
|
7
|
+
constructor(limit) {
|
|
8
|
+
this.chat = [];
|
|
9
|
+
this.limit = limit;
|
|
10
|
+
}
|
|
11
|
+
push(message) {
|
|
12
|
+
if (this.chat.length >= this.limit) {
|
|
13
|
+
this.chat.splice(0, 1);
|
|
14
|
+
}
|
|
15
|
+
this.chat.push(message);
|
|
16
|
+
}
|
|
17
|
+
getChat() {
|
|
18
|
+
return this.chat;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.LocalChatStore = LocalChatStore;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import net from 'net'
|
|
2
|
+
import readline from 'readline/promises'
|
|
3
|
+
import { SessionManager } from '../security/sessionManager'
|
|
4
|
+
import { processMessage } from './handlers/processMessage'
|
|
5
|
+
import { onLine } from "./handlers/mainInputHandler"
|
|
6
|
+
import { genName } from '../utils/genNameAlias'
|
|
7
|
+
|
|
8
|
+
export class Client {
|
|
9
|
+
manager: SessionManager
|
|
10
|
+
rl: readline.Interface
|
|
11
|
+
name: string
|
|
12
|
+
server: net.Socket
|
|
13
|
+
key_generated: boolean
|
|
14
|
+
|
|
15
|
+
constructor(name=genName(), host="localhost", port=3000) {
|
|
16
|
+
this.name = name
|
|
17
|
+
this.server = net.connect({ host: host, port: port })
|
|
18
|
+
this.key_generated = false
|
|
19
|
+
this.manager = new SessionManager()
|
|
20
|
+
this.rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: `${this.name} >>> ` })
|
|
21
|
+
|
|
22
|
+
this.server.on("connect", () => {
|
|
23
|
+
this.server.write(JSON.stringify({type: 'public_key_request', body: this.manager.getClient()}))
|
|
24
|
+
})
|
|
25
|
+
this.server.on("data", (data) => {
|
|
26
|
+
processMessage.call(this, data)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
this.rl.on("line", onLine.bind(this))
|
|
30
|
+
|
|
31
|
+
this.server.on("close", () => {this.server.end(); process.exit()})
|
|
32
|
+
this.rl.on("SIGINT", () => {this.server.end(); process.exit()})
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Client } from "../clientInstance";
|
|
2
|
+
import { encrypt } from "../../utils/edUtils";
|
|
3
|
+
|
|
4
|
+
function onLine(this: Client, line: string) {
|
|
5
|
+
if (line.length > 0) {
|
|
6
|
+
process.stdout.moveCursor(0, -1)
|
|
7
|
+
process.stdout.write('\r\x1B[K');
|
|
8
|
+
console.log(`${this.name}: ${line}`)
|
|
9
|
+
if (line == "/exit") {
|
|
10
|
+
this.rl.emit("SIGINT")
|
|
11
|
+
}
|
|
12
|
+
this.server.write(encrypt(JSON.stringify({type: 'msg', body: [this.name, line]}), this.manager.session_key))
|
|
13
|
+
this.rl.prompt()
|
|
14
|
+
} else {
|
|
15
|
+
process.stdout.moveCursor(0, -1)
|
|
16
|
+
process.stdout.write('\r\x1B[K');
|
|
17
|
+
this.rl.prompt()
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { onLine }
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { decrypt, encrypt } from "../../utils/edUtils"
|
|
2
|
+
import { Client } from "../clientInstance"
|
|
3
|
+
|
|
4
|
+
async function processMessage(this: Client, data: any) {
|
|
5
|
+
const cur = this.rl.getCursorPos()
|
|
6
|
+
let processed
|
|
7
|
+
|
|
8
|
+
if (this.key_generated) {
|
|
9
|
+
processed = JSON.parse(decrypt(data.toString(), this.manager.session_key))
|
|
10
|
+
} else {
|
|
11
|
+
processed = JSON.parse(data.toString('utf-8'))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
switch (processed.type) {
|
|
15
|
+
case 'public_key_response':
|
|
16
|
+
this.manager.setServer(processed.body[1])
|
|
17
|
+
this.server.write(JSON.stringify({type: 'master_key_response', body: [this.manager.getMaster(processed.body[0]), this.name]}))
|
|
18
|
+
break;
|
|
19
|
+
case 'finished':
|
|
20
|
+
this.manager.generateSession()
|
|
21
|
+
this.key_generated = true
|
|
22
|
+
this.server.write(encrypt(JSON.stringify({type: "finished", body: this.name}), this.manager.session_key))
|
|
23
|
+
this.rl.prompt()
|
|
24
|
+
break;
|
|
25
|
+
case 'history':
|
|
26
|
+
process.stdout.write('\r\x1B[K')
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < processed.body.length; i++) {
|
|
29
|
+
console.log(processed.body[i][0] + ':', processed.body[i][1])
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log('[SERVER]:', this.name, 'connected to server')
|
|
33
|
+
|
|
34
|
+
this.rl.prompt(true)
|
|
35
|
+
process.stdout.cursorTo(cur.cols)
|
|
36
|
+
break;
|
|
37
|
+
case 'msg':
|
|
38
|
+
process.stdout.write('\r\x1B[K');
|
|
39
|
+
console.log(processed.data[0] + ':', processed.data[1])
|
|
40
|
+
|
|
41
|
+
this.rl.prompt(true)
|
|
42
|
+
process.stdout.cursorTo(cur.cols)
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { processMessage }
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@manch1kz/yanac",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "(YANAC) Yet another not another chat",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/manch1kz/YANAC"
|
|
8
|
+
},
|
|
9
|
+
"main": "./build/app.js",
|
|
10
|
+
"bin": "./build/app.js",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"prepack": "npm run build",
|
|
13
|
+
"build": "tsc"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"cryptography",
|
|
17
|
+
"handshake",
|
|
18
|
+
"tcp",
|
|
19
|
+
"educational",
|
|
20
|
+
"cli",
|
|
21
|
+
"security"
|
|
22
|
+
],
|
|
23
|
+
"author": "manch1kz",
|
|
24
|
+
"license": "ISC",
|
|
25
|
+
"type": "commonjs",
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"commander": "^14.0.2"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^25.0.8",
|
|
34
|
+
"typescript": "^5.0.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import crypto from "crypto"
|
|
2
|
+
|
|
3
|
+
export class SessionManager {
|
|
4
|
+
client_random: any
|
|
5
|
+
server_random: any
|
|
6
|
+
master_random: any
|
|
7
|
+
session_key: any
|
|
8
|
+
privateKey: any
|
|
9
|
+
publicKey: any
|
|
10
|
+
|
|
11
|
+
constructor() {
|
|
12
|
+
this.client_random = null
|
|
13
|
+
this.server_random = null
|
|
14
|
+
this.master_random = null
|
|
15
|
+
this.session_key = null
|
|
16
|
+
|
|
17
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
|
|
18
|
+
modulusLength: 2048,
|
|
19
|
+
publicKeyEncoding: {
|
|
20
|
+
format: 'pem',
|
|
21
|
+
type: 'pkcs1'
|
|
22
|
+
},
|
|
23
|
+
privateKeyEncoding: {
|
|
24
|
+
format: 'pem',
|
|
25
|
+
type: 'pkcs1'
|
|
26
|
+
}})
|
|
27
|
+
|
|
28
|
+
this.privateKey = privateKey
|
|
29
|
+
this.publicKey = publicKey
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
generateSession() {
|
|
33
|
+
if (this.client_random && this.server_random && this.master_random) {
|
|
34
|
+
const sum = this.client_random + this.server_random + Number(this.master_random)
|
|
35
|
+
this.session_key = crypto.createHash('sha256').update(String(sum)).digest().toString('hex')
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
setClient(client_random: any) {
|
|
40
|
+
this.client_random = client_random
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getClient() {
|
|
44
|
+
if (this.client_random == null) {
|
|
45
|
+
this.client_random = Math.floor(Math.random() * 5000)
|
|
46
|
+
}
|
|
47
|
+
return this.client_random
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getServer() {
|
|
51
|
+
if (this.server_random == null) {
|
|
52
|
+
this.server_random = Math.floor(Math.random() * 5000)
|
|
53
|
+
}
|
|
54
|
+
return this.server_random
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
setServer(server_random: any) {
|
|
58
|
+
this.server_random = server_random
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getMaster(public_key: any) {
|
|
62
|
+
if (this.master_random == null) {
|
|
63
|
+
this.master_random = String(Math.floor(Math.random() * 5000))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return crypto.publicEncrypt({
|
|
67
|
+
key: public_key,
|
|
68
|
+
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
|
|
69
|
+
}, this.master_random).toString('base64')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
setMaster(master_random: any, private_key: any) {
|
|
73
|
+
this.master_random = crypto.privateDecrypt({
|
|
74
|
+
key: private_key,
|
|
75
|
+
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
|
|
76
|
+
}, Buffer.from(master_random, 'base64')).toString()
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { encrypt, decrypt } from "../../utils/edUtils"
|
|
2
|
+
import { Server } from "../serverInstance"
|
|
3
|
+
|
|
4
|
+
function processMessage(this: Server, socket: any, data: any) {
|
|
5
|
+
const socket_item = this.connections.get(socket)
|
|
6
|
+
const socket_generator = socket_item.generator
|
|
7
|
+
let message
|
|
8
|
+
|
|
9
|
+
if (socket_item.key_generated) {
|
|
10
|
+
message = JSON.parse(decrypt(data.toString(), socket_item.generator.session_key))
|
|
11
|
+
} else {
|
|
12
|
+
message = JSON.parse(data.toString())
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
switch (message.type) {
|
|
16
|
+
case 'msg':
|
|
17
|
+
this.localChatStore.push(message.body)
|
|
18
|
+
Array.from(this.connections.keys()).forEach((i: any) => {
|
|
19
|
+
if (i != socket) {
|
|
20
|
+
i.write(encrypt(JSON.stringify({type: 'msg', data: message.body}), this.connections.get(i).generator.session_key))
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
break;
|
|
24
|
+
case 'public_key_request':
|
|
25
|
+
const server_random = this.manager.getServer()
|
|
26
|
+
|
|
27
|
+
socket_generator.setClient(message.body)
|
|
28
|
+
socket_generator.setServer(server_random)
|
|
29
|
+
|
|
30
|
+
socket.write(JSON.stringify({type: 'public_key_response', body: [this.manager.publicKey, server_random]}))
|
|
31
|
+
break;
|
|
32
|
+
case 'master_key_response':
|
|
33
|
+
socket_generator.setMaster(message.body[0], this.manager.privateKey)
|
|
34
|
+
socket_generator.generateSession()
|
|
35
|
+
socket_item.name = message.body[1]
|
|
36
|
+
socket_item.key_generated = true
|
|
37
|
+
|
|
38
|
+
socket.write(JSON.stringify({type: 'finished'}))
|
|
39
|
+
break;
|
|
40
|
+
case 'finished':
|
|
41
|
+
socket_item.name = message.body
|
|
42
|
+
socket.write(encrypt(JSON.stringify({type: 'history', body: this.localChatStore.getChat()}), socket_generator.session_key))
|
|
43
|
+
this.localChatStore.push(['[SERVER]', message.body + ' connected to server'])
|
|
44
|
+
|
|
45
|
+
Array.from(this.connections.keys()).forEach((i: any) => {
|
|
46
|
+
if (i != socket) {
|
|
47
|
+
i.write(
|
|
48
|
+
encrypt(JSON.stringify({type: 'msg', data: ['[SERVER]', message.body + ' connected to server']}),
|
|
49
|
+
this.connections.get(i).generator.session_key)
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { processMessage }
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { SessionManager } from "../../security/sessionManager"
|
|
2
|
+
import { encrypt, decrypt } from "../../utils/edUtils"
|
|
3
|
+
import { Server } from "../serverInstance"
|
|
4
|
+
import net from 'net'
|
|
5
|
+
|
|
6
|
+
function setupSocket(this: Server, socket: net.Socket, cb: Function) {
|
|
7
|
+
this.connections.set(socket, {name: null, generator: new SessionManager(), key_generated: false})
|
|
8
|
+
|
|
9
|
+
socket.on('close', () => {
|
|
10
|
+
let body = ['[SERVER]', this.connections.get(socket).name + ' disconnected from server']
|
|
11
|
+
this.localChatStore.push(body)
|
|
12
|
+
|
|
13
|
+
Array.from(this.connections.keys()).forEach((i: any) => {
|
|
14
|
+
i.write(encrypt(JSON.stringify({type: 'msg', data: body}), this.connections.get(i).generator.session_key))
|
|
15
|
+
})
|
|
16
|
+
this.connections.delete(socket)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
socket.on("error", () => {})
|
|
20
|
+
|
|
21
|
+
socket.on('data', (data: any) => {
|
|
22
|
+
cb(socket, data)
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export { setupSocket }
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import net from 'net'
|
|
2
|
+
import { SessionManager } from '../security/sessionManager'
|
|
3
|
+
import { LocalChatStore } from '../utils/localChatStore'
|
|
4
|
+
import { processMessage } from "./handlers/processMessage"
|
|
5
|
+
import { setupSocket } from "./handlers/setupSocket"
|
|
6
|
+
|
|
7
|
+
export class Server {
|
|
8
|
+
localChatStore: LocalChatStore
|
|
9
|
+
server: net.Server
|
|
10
|
+
manager: SessionManager
|
|
11
|
+
connections: Map<any, any>
|
|
12
|
+
|
|
13
|
+
constructor(limit=100, port=3000) {
|
|
14
|
+
this.localChatStore = new LocalChatStore(100)
|
|
15
|
+
this.manager = new SessionManager()
|
|
16
|
+
this.connections = new Map()
|
|
17
|
+
this.server = net.createServer()
|
|
18
|
+
|
|
19
|
+
this.server.on("connection", (socket) => {
|
|
20
|
+
setupSocket.call(this, socket, processMessage.bind(this))
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
this.server.listen(port)
|
|
24
|
+
}
|
|
25
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2022",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"rootDir": "./",
|
|
6
|
+
"outDir": "./build",
|
|
7
|
+
"sourceMap": false,
|
|
8
|
+
"declaration": false,
|
|
9
|
+
"declarationMap": false,
|
|
10
|
+
"moduleResolution": "node",
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
},
|
|
13
|
+
"exclude": ["node_modules", "build"]
|
|
14
|
+
}
|
package/utils/edUtils.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import crypto from "crypto"
|
|
2
|
+
|
|
3
|
+
function formatKey(key: string) {
|
|
4
|
+
if (key.length > 32) {
|
|
5
|
+
key = key.slice(0, 32)
|
|
6
|
+
} else if (key.length < 32) {
|
|
7
|
+
key += Array.from({length: 32 - key.length}, () => 0).join('')
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return key
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function encrypt(value: string, key: string) {
|
|
14
|
+
key = formatKey(key)
|
|
15
|
+
|
|
16
|
+
const iv = Buffer.alloc(16, 0);
|
|
17
|
+
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv)
|
|
18
|
+
|
|
19
|
+
let encrypted = cipher.update(value, "utf-8", "hex")
|
|
20
|
+
encrypted += cipher.final("hex")
|
|
21
|
+
|
|
22
|
+
return encrypted
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function decrypt(value: string, key: string) {
|
|
26
|
+
key = formatKey(key)
|
|
27
|
+
|
|
28
|
+
const iv = Buffer.alloc(16, 0);
|
|
29
|
+
const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv)
|
|
30
|
+
|
|
31
|
+
let decrypted = decipher.update(value, "hex", "utf-8")
|
|
32
|
+
decrypted += decipher.final("utf-8")
|
|
33
|
+
|
|
34
|
+
return decrypted
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { encrypt, decrypt }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class LocalChatStore {
|
|
2
|
+
chat: any
|
|
3
|
+
limit: number
|
|
4
|
+
|
|
5
|
+
constructor(limit: number) {
|
|
6
|
+
this.chat = []
|
|
7
|
+
this.limit = limit
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
push(message: any) {
|
|
11
|
+
if (this.chat.length >= this.limit) {
|
|
12
|
+
this.chat.splice(0, 1)
|
|
13
|
+
}
|
|
14
|
+
this.chat.push(message)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
getChat() {
|
|
18
|
+
return this.chat
|
|
19
|
+
}
|
|
20
|
+
}
|