Skip to content

Commit c28138a

Browse files
committed
1 parent b70b080 commit c28138a

File tree

3 files changed

+187
-90
lines changed

3 files changed

+187
-90
lines changed

src/crypto.ts

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { webcrypto } from "crypto";
2+
type CK = webcrypto.CryptoKey;
23

34
// #############
45
// ### Utils ###
@@ -104,10 +105,13 @@ export async function rsaEncrypt(
104105
encodedData
105106
);
106107

107-
return arrayBufferToBase64(encryptedData);
108+
const result = arrayBufferToBase64(encryptedData);
109+
console.log("Encrypted Data:", result); // Debugging output
110+
return result;
108111
}
109112

110113

114+
111115
// Decrypts a message using an RSA private key
112116
export async function rsaDecrypt(
113117
encryptedData: string,
@@ -162,37 +166,65 @@ export async function importSymKey(strKey: string): Promise<webcrypto.CryptoKey>
162166

163167
// Encrypt a message using a symmetric key
164168
export async function symEncrypt(
165-
key: webcrypto.CryptoKey,
169+
key: string | CK,
166170
data: string
167171
): Promise<string> {
168-
const iv = webcrypto.getRandomValues(new Uint8Array(16)); // Generate a random IV
169-
const encodedData = new TextEncoder().encode(data);
172+
let cryptoKey: CK;
173+
if (typeof key === 'string') {
174+
cryptoKey = await importSymKey(key);
175+
} else {
176+
cryptoKey = key;
177+
}
170178

171-
const encryptedData = await webcrypto.subtle.encrypt(
172-
{ name: "AES-CBC", iv },
173-
key,
174-
encodedData
179+
const iv = webcrypto.getRandomValues(new Uint8Array(16));
180+
const encoded = new TextEncoder().encode(data);
181+
182+
const encrypted = await webcrypto.subtle.encrypt(
183+
{
184+
name: "AES-CBC",
185+
iv
186+
},
187+
cryptoKey,
188+
encoded
175189
);
176190

177-
return arrayBufferToBase64(iv) + "." + arrayBufferToBase64(encryptedData);
191+
const combined = new Uint8Array(iv.length + encrypted.byteLength);
192+
combined.set(iv);
193+
combined.set(new Uint8Array(encrypted), iv.length);
194+
195+
return Buffer.from(combined).toString('base64');
178196
}
179197

180198

199+
181200
// Decrypt a message using a symmetric key
182201
export async function symDecrypt(
183-
strKey: string,
202+
key: string | CK,
184203
encryptedData: string
185204
): Promise<string> {
186-
const key = await importSymKey(strKey);
187-
const [ivStr, encryptedStr] = encryptedData.split(".");
188-
const iv = base64ToArrayBuffer(ivStr);
189-
const encryptedBuffer = base64ToArrayBuffer(encryptedStr);
205+
let cryptoKey: CK;
206+
if (typeof key === 'string') {
207+
cryptoKey = await importSymKey(key);
208+
} else {
209+
cryptoKey = key;
210+
}
190211

191-
const decryptedData = await webcrypto.subtle.decrypt(
192-
{ name: "AES-CBC", iv },
193-
key,
194-
encryptedBuffer
212+
// Decode the concatenated IV + encrypted data
213+
const combined = Buffer.from(encryptedData, 'base64');
214+
215+
// Extract IV and encrypted data correctly
216+
const iv = combined.slice(0, 16);
217+
const data = combined.slice(16);
218+
219+
const decrypted = await webcrypto.subtle.decrypt(
220+
{
221+
name: "AES-CBC",
222+
iv
223+
},
224+
cryptoKey,
225+
data
195226
);
196227

197-
return new TextDecoder().decode(decryptedData);
228+
return new TextDecoder().decode(decrypted);
198229
}
230+
Lines changed: 72 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
import bodyParser from "body-parser";
2-
import express, { Request, Response } from "express";
3-
import { BASE_ONION_ROUTER_PORT, REGISTRY_PORT } from "../config";
4-
import { generateRsaKeyPair, exportPubKey, exportPrvKey } from "../crypto";
2+
import express from "express";
3+
import { BASE_ONION_ROUTER_PORT, REGISTRY_PORT, BASE_USER_PORT } from "../config";
4+
import {
5+
generateRsaKeyPair,
6+
rsaDecrypt,
7+
symDecrypt,
8+
importSymKey,
9+
exportPubKey,
10+
exportPrvKey,
11+
exportSymKey,
12+
} from "../crypto";
513
import { webcrypto } from "node:crypto";
614

715
type CK = webcrypto.CryptoKey;
@@ -15,8 +23,7 @@ export async function simpleOnionRouter(nodeId: number) {
1523
let publicKey: CK;
1624
let privateKeyStr: string;
1725
let publicKeyStr: string;
18-
19-
// Store the last received messages and destination (default: null)
26+
let lastCircuit: number[] = [];
2027
let lastReceivedEncryptedMessage: string | null = null;
2128
let lastReceivedDecryptedMessage: string | null = null;
2229
let lastMessageDestination: number | null = null;
@@ -25,56 +32,80 @@ export async function simpleOnionRouter(nodeId: number) {
2532
const keyPair = await generateRsaKeyPair();
2633
privateKey = keyPair.privateKey;
2734
publicKey = keyPair.publicKey;
28-
29-
// Convert keys to base64
3035
publicKeyStr = await exportPubKey(publicKey);
3136
privateKeyStr = await exportPrvKey(privateKey);
3237

33-
// Register with the registry
34-
const response = await fetch(`http://localhost:${REGISTRY_PORT}/registerNode`, {
38+
const registryResponse = await fetch(`http://localhost:${REGISTRY_PORT}/registerNode`, {
3539
method: "POST",
3640
headers: { "Content-Type": "application/json" },
3741
body: JSON.stringify({ nodeId, pubKey: publicKeyStr }),
3842
});
3943

40-
if (!response.ok) {
41-
throw new Error(`Failed to register node ${nodeId}`);
42-
}
43-
44+
if (!registryResponse.ok) throw new Error(`Failed to register node: ${registryResponse.statusText}`);
4445
console.log(`Node ${nodeId} registered successfully`);
4546
} catch (error) {
4647
console.error("Failed to initialize node:", error);
4748
throw error;
4849
}
4950

50-
// Status route
51-
onionRouter.get("/status", (req: Request, res: Response) => {
52-
res.send("live");
53-
});
54-
55-
// GET last received encrypted message
56-
onionRouter.get("/getLastReceivedEncryptedMessage", (req: Request, res: Response) => {
57-
res.json({ result: lastReceivedEncryptedMessage });
58-
});
59-
60-
// GET last received decrypted message
61-
onionRouter.get("/getLastReceivedDecryptedMessage", (req: Request, res: Response) => {
62-
res.json({ result: lastReceivedDecryptedMessage });
63-
});
64-
65-
// GET last message destination
66-
onionRouter.get("/getLastMessageDestination", (req: Request, res: Response) => {
67-
res.json({ result: lastMessageDestination });
68-
});
69-
70-
// Provide private key for testing purposes
71-
onionRouter.get("/getPrivateKey", (req: Request, res: Response) => {
72-
res.json({ result: privateKeyStr });
73-
});
74-
75-
const server = onionRouter.listen(BASE_ONION_ROUTER_PORT + nodeId, () => {
76-
console.log(`Onion router ${nodeId} is listening on port ${BASE_ONION_ROUTER_PORT + nodeId}`);
51+
onionRouter.get("/status", (req, res) => res.send("live"));
52+
onionRouter.get("/getLastReceivedEncryptedMessage", (req, res) => res.json({ result: lastReceivedEncryptedMessage }));
53+
onionRouter.get("/getLastReceivedDecryptedMessage", (req, res) => res.json({ result: lastReceivedDecryptedMessage }));
54+
onionRouter.get("/getLastMessageDestination", (req, res) => res.json({ result: lastMessageDestination }));
55+
onionRouter.get("/getPrivateKey", async (req, res) => res.json({ result: privateKeyStr }));
56+
onionRouter.get("/getLastCircuit", (req, res) => res.json({ result: lastCircuit }));
57+
58+
onionRouter.post("/message", async (req, res) => {
59+
try {
60+
const { message, circuit = [] } = req.body;
61+
console.log(`Node ${nodeId} received message:`, message);
62+
63+
if (!message) return res.status(400).json({ error: "Message required" });
64+
65+
lastReceivedEncryptedMessage = message;
66+
console.log(`Last received encrypted message on Node ${nodeId}:`, lastReceivedEncryptedMessage);
67+
68+
const encryptedSymKey = message.slice(0, 344);
69+
const encryptedPayload = message.slice(344);
70+
71+
let symKey;
72+
try {
73+
const symKeyStr = await rsaDecrypt(encryptedSymKey, privateKey);
74+
symKey = await importSymKey(symKeyStr);
75+
} catch (error) {
76+
console.error(`Node ${nodeId} failed to decrypt symmetric key:`, error);
77+
return res.status(500).json({ error: "Symmetric key decryption failed" });
78+
}
79+
80+
let decryptedPayload;
81+
try {
82+
decryptedPayload = await symDecrypt(await exportSymKey(symKey), encryptedPayload);
83+
} catch (error) {
84+
console.error(`Node ${nodeId} failed to decrypt payload:`, error);
85+
return res.status(500).json({ error: "Payload decryption failed" });
86+
}
87+
88+
const destination = parseInt(decryptedPayload.slice(0, 10), 10);
89+
const remainingMessage = decryptedPayload.slice(10);
90+
91+
lastReceivedDecryptedMessage = remainingMessage;
92+
lastMessageDestination = destination;
93+
lastCircuit = [...circuit, nodeId];
94+
95+
await fetch(`http://localhost:${destination}/message`, {
96+
method: "POST",
97+
headers: { "Content-Type": "application/json" },
98+
body: JSON.stringify({ message: remainingMessage, circuit: lastCircuit }),
99+
});
100+
101+
return res.json({ success: true });
102+
} catch (error) {
103+
console.error("Error processing message:", error);
104+
return res.status(500).json({ error: "Failed to process message" });
105+
}
77106
});
78107

79-
return server;
108+
return onionRouter.listen(BASE_ONION_ROUTER_PORT + nodeId, () =>
109+
console.log(`Onion router ${nodeId} is listening on port ${BASE_ONION_ROUTER_PORT + nodeId}`)
110+
);
80111
}

src/users/user.ts

Lines changed: 64 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,84 @@
11
import bodyParser from "body-parser";
2-
import express, { Request, Response } from "express";
3-
import { BASE_USER_PORT } from "../config";
2+
import express from "express";
3+
import { BASE_USER_PORT, REGISTRY_PORT, BASE_ONION_ROUTER_PORT } from "../config";
4+
import { symEncrypt, rsaEncrypt, exportSymKey, importPubKey, createRandomSymmetricKey, exportPubKey } from "../crypto";
5+
interface RegistryResponse {
6+
nodes: Array<{ nodeId: number; pubKey: string }>;
7+
}
48

5-
export type SendMessageBody = {
6-
message: string;
7-
destinationUserId: number;
8-
};
9+
let lastReceivedMessage: string | null = null;
10+
let lastSentMessage: string | null = null;
911

1012
export async function user(userId: number) {
13+
let lastCircuit: number[] = [];
1114
const _user = express();
1215
_user.use(express.json());
1316
_user.use(bodyParser.json());
1417

15-
// Store last received and last sent messages (default: null)
16-
let lastReceivedMessage: string | null = null;
17-
let lastSentMessage: string | null = null;
18+
_user.get("/status", (req, res) => res.send("live"));
19+
_user.get("/getLastReceivedMessage", (req, res) => res.json({ result: lastReceivedMessage }));
20+
_user.get("/getLastSentMessage", (req, res) => res.json({ result: lastSentMessage }));
21+
_user.get("/getLastCircuit", (req, res) => res.json({ result: lastCircuit }));
1822

19-
// Status route
20-
_user.get("/status", (req: Request, res: Response) => {
21-
res.send("live");
22-
});
23+
_user.post("/message", (req, res) => {
24+
const { message, circuit } = req.body;
25+
if (!message) return res.status(400).json({ error: "Message required." });
2326

24-
// GET last received message
25-
_user.get("/getLastReceivedMessage", (req: Request, res: Response) => {
26-
res.json({ result: lastReceivedMessage });
27-
});
27+
lastReceivedMessage = message;
28+
if (circuit) lastCircuit = circuit;
2829

29-
// GET last sent message
30-
_user.get("/getLastSentMessage", (req: Request, res: Response) => {
31-
res.json({ result: lastSentMessage });
30+
return res.send("success");
3231
});
3332

34-
_user.post("/message", (req: Request, res: Response) => {
35-
const { message } = req.body as SendMessageBody;
33+
_user.post("/sendMessage", async (req, res) => {
34+
const { message, destinationUserId } = req.body;
35+
try {
36+
const registryResponse = await fetch(`http://localhost:${REGISTRY_PORT}/getNodeRegistry`);
37+
const { nodes } = await registryResponse.json() as RegistryResponse;
3638

37-
if (!message || typeof message !== "string") {
38-
return res.status(400).json({ error: "Invalid message" });
39-
}
39+
if (nodes.length < 3) return res.status(500).json({ error: "Not enough nodes available" });
4040

41-
lastReceivedMessage = message;
42-
return res.send("success"); // ✅ Explicitly returning ensures all paths return a value
43-
});
41+
const selectedNodes = selectRandomNodes(nodes, 3);
42+
const circuit = selectedNodes.map(node => node.nodeId);
43+
let finalDestination = `${BASE_USER_PORT + destinationUserId}`.padStart(10, "0");
44+
45+
let encryptedMessage = message;
46+
for (let i = circuit.length - 1; i >= 0; i--) {
47+
const symKey = await createRandomSymmetricKey();
48+
const symKeyStr = await exportSymKey(symKey);
49+
const nodePublicKey = await importPubKey(selectedNodes[i].pubKey);
4450

45-
const server = _user.listen(BASE_USER_PORT + userId, () => {
46-
console.log(`User ${userId} is listening on port ${BASE_USER_PORT + userId}`);
51+
const encryptedSymKey = await rsaEncrypt(symKeyStr, await exportPubKey(nodePublicKey));
52+
const payload = finalDestination + encryptedMessage;
53+
const encryptedPayload = await symEncrypt(symKey, payload);
54+
55+
encryptedMessage = encryptedSymKey + encryptedPayload;
56+
finalDestination = `${BASE_ONION_ROUTER_PORT + circuit[i]}`.padStart(10, "0");
57+
}
58+
59+
await fetch(`http://localhost:${BASE_ONION_ROUTER_PORT + circuit[0]}/message`, {
60+
method: "POST",
61+
headers: { "Content-Type": "application/json" },
62+
body: JSON.stringify({ message: encryptedMessage, circuit: [] }),
63+
}).then(response => response.text()).then(data => console.log("Message sent response:", data));
64+
65+
66+
lastSentMessage = message;
67+
lastCircuit = circuit;
68+
return res.json({ success: true });
69+
} catch (error) {
70+
console.error("Error sending message:", error);
71+
return res.status(500).json({ error: "Failed to send message" });
72+
}
4773
});
4874

75+
const server = _user.listen(BASE_USER_PORT + userId, () =>
76+
console.log(`User ${userId} is listening on port ${BASE_USER_PORT + userId}`)
77+
);
78+
4979
return server;
5080
}
81+
82+
function selectRandomNodes(nodes: Array<{ nodeId: number; pubKey: string }>, count: number) {
83+
return nodes.sort(() => Math.random() - 0.5).slice(0, count);
84+
}

0 commit comments

Comments
 (0)