From 4d68e7a9f790037ab13a1833ebf1bb3549b2c9a0 Mon Sep 17 00:00:00 2001 From: Sam Hadow Date: Fri, 21 Feb 2025 19:16:50 +0100 Subject: [PATCH] change representation of public key in db to hex string + create room --- src/authentication.js | 24 +++--------- src/controllers/account.js | 5 ++- src/controllers/chat.js | 19 +++++++++ src/controllers/main.js | 36 +++--------------- src/db.js | 74 +++++++++++++++++++++++++----------- src/public/popups-logged.js | 51 +++++++++++++++++++++++++ src/routes/chat.js | 14 ++++--- src/routes/index.js | 4 +- src/stringutils.js | 43 +++++++++++++++++++++ src/views/index.pug | 11 +++++- tests/authentication.test.js | 48 +++++++++++++++++++---- 11 files changed, 239 insertions(+), 90 deletions(-) create mode 100644 src/public/popups-logged.js diff --git a/src/authentication.js b/src/authentication.js index 1cacf87..81e372e 100644 --- a/src/authentication.js +++ b/src/authentication.js @@ -1,4 +1,5 @@ const { subtle } = require('node:crypto').webcrypto; +const stringutils = require("./stringutils"); const sharedSecret = process.env.SHARED_SECRET; @@ -8,9 +9,9 @@ const authentication = { }, verifySignature : async (msg, sig, publicKeys) => { try { - for (const pemPubKey of publicKeys) { + for (const hexKey of publicKeys) { try { - const pubKey = await authentication.pemToKey(pemPubKey); + const pubKey = await stringutils.hexToKey(hexKey); const verified = await subtle.verify( 'Ed25519', pubKey, @@ -18,31 +19,16 @@ const authentication = { msg ); if (verified) { - return pemPubKey; + return hexKey; } } catch (err) { - console.log('Failed to verify signature with public key:', pemPubKey, err); + console.log('Failed to verify signature with public key:', hexKey, err); } } return null; } catch (err) { console.error('Error verifying signature:', err); } - }, - pemToKey: async (pemKey) => { - const base64 = pemKey.replace(`-----BEGIN PUBLIC KEY-----`, '').replace(`-----END PUBLIC KEY-----`, '').trim(); - const buffer = Buffer.from(base64, 'base64'); - const uint8Array = new Uint8Array(buffer); - const publicKey = await subtle.importKey( - "spki", - uint8Array, - { - name: "Ed25519", - }, - true, - ["verify"], - ); - return publicKey; } }; diff --git a/src/controllers/account.js b/src/controllers/account.js index 60776e6..5d02825 100644 --- a/src/controllers/account.js +++ b/src/controllers/account.js @@ -1,5 +1,6 @@ const crypto = require('crypto'); const database = require("../db"); +const stringutils = require("../stringutils"); const authentication = require("../authentication"); const socket = require('../socket').default; @@ -10,9 +11,9 @@ const accountController = { if (!sharedSecret || !publicKey) { return res.status(400).json({ error: "Missing sharedSecret or publicKey" }); } - console.log('Received data:', { sharedSecret, publicKey }); if (authentication.checkSharedSecret(sharedSecret)) { - database.addUser(publicKey); + let pubkey = stringutils.pemToHex(publicKey) + database.addUser(pubkey); } else { return res.status(400).json({ error: "Wrong sharedSecret" }); } diff --git a/src/controllers/chat.js b/src/controllers/chat.js index 8b13789..a9e4e31 100644 --- a/src/controllers/chat.js +++ b/src/controllers/chat.js @@ -1 +1,20 @@ +const database = require("../db"); +const stringutils = require("../stringutils"); +const chatController = { + add: async (req, res) => { + try { + const { pubkey } = req.body; + if (!pubkey) { + return res.status(400).json({ error: "Missing publicKey" }); + } + await database.createRoom(req.session.publicKey, stringutils.pemToHex(pubkey)); + return res.status(201).json({ message: "Room created successfully." }); + } catch (error) { + console.error("Error creating the room:", error); + return res.status(500).json({ error: "Failed to create the room" }); + } + }, +} + +module.exports = chatController; diff --git a/src/controllers/main.js b/src/controllers/main.js index 3999002..bf3170f 100644 --- a/src/controllers/main.js +++ b/src/controllers/main.js @@ -1,39 +1,13 @@ const path = require('path'); +const stringutils = require("../stringutils"); const mainController = { root: (req, res) => { - let pubKey = req.session.publicKey; - console.log(pubKey); - let isLoggedIn = typeof pubKey !== 'undefined'; + let pubKeyHex = req.session.publicKey; + let isLoggedIn = typeof pubKeyHex !== 'undefined'; + let pubKey = isLoggedIn ? stringutils.hexToPem(pubKeyHex).replaceAll('\n','') : null; res.render('index', {isLoggedIn, pubKey}); - }, - // style: (req, res) => { - // res.sendFile(path.resolve(__dirname + '/../public/style.css')); - // }, - // script: (req, res) => { - // res.sendFile(path.resolve(__dirname + '/../public/script.js')); - // }, - // ecc: (req, res) => { - // res.sendFile(path.resolve(__dirname + '/../public/ecc.js')); - // }, - // ecdh: (req, res) => { - // res.sendFile(path.resolve(__dirname + '/../public/ecdh.js')); - // }, - // popups: (req, res) => { - // res.sendFile(path.resolve(__dirname + '/../public/popups.js')); - // }, - // chat : (req, res) => { - // res.sendFile(path.resolve(__dirname + '/../public/chat.js')); - // }, - // register : (req, res) => { - // res.sendFile(path.resolve(__dirname + '/../public/register.js')); - // }, - // pubkey : (req, res) => { - // res.sendFile(path.resolve(__dirname + '/../public/pubkey.js')); - // }, - // registertext : (req, res) => { - // res.sendFile(path.resolve(__dirname + '/../public/registertext.js')); - // } + } }; module.exports = mainController; diff --git a/src/db.js b/src/db.js index 7bb4c44..6ffceaa 100644 --- a/src/db.js +++ b/src/db.js @@ -35,40 +35,43 @@ const database = { createTables: () => { pool.query(` CREATE TABLE IF NOT EXISTS "users" ( - uuid integer PRIMARY KEY, + uuid SERIAL PRIMARY KEY, pubkey text ); + CREATE TABLE IF NOT EXISTS "room" ( + uuid SERIAL PRIMARY KEY + ); + CREATE TABLE IF NOT EXISTS "room_members" ( + room_uuid INTEGER REFERENCES "room"(uuid), + user_uuid INTEGER REFERENCES "users"(uuid), + PRIMARY KEY (room_uuid, user_uuid) + ); `, (err, _) => { if (err) { - console.error('Error creating users table', err); + console.error('Error creating tables', err); return; - } - pool.query(` - CREATE SEQUENCE IF NOT EXISTS uuid_sequence - INCREMENT BY 1 - START WITH 1; - `, (err, _) => { - if (err) { - console.error('Error creating sequence', err); - } else { - console.log("users table and sequence created successfully."); + } else { + console.log("tables created successfully."); } - }); }); }, checkSchema: () => { return new Promise((resolve, reject) => { pool.query(` - SELECT EXISTS ( - SELECT 1 FROM pg_tables WHERE tablename = 'users' - ) AS table_exists; + SELECT + (EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users')) + AND + (EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'room')) + AND + (EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'room_members')) + AS all_tables_exist; `, (err, res) => { if (err) { console.error('Error executing query', err); reject(err); } else { - const tableExists = res.rows[0].table_exists; - resolve(tableExists); + const all_tables_exist = res.rows[0].all_tables_exist; + resolve(all_tables_exist); } }); }); @@ -90,17 +93,44 @@ const database = { return; } try { - const result = await pool.query('SELECT NEXTVAL(\'uuid_sequence\') AS next_uuid'); - const nextUuid = result.rows[0].next_uuid; await pool.query( - 'INSERT INTO "users" (uuid, pubkey) VALUES ($1, $2)', - [nextUuid, pubkey] + 'INSERT INTO "users" (uuid, pubkey) VALUES (DEFAULT, $1)', + [pubkey,] ); console.log(`Added user with the public key ${pubkey} .`); } catch (err) { console.error('Error adding user:', err); } }, + createRoom: async (pubkey1, pubkey2) => { + try { + const userQuery = 'SELECT uuid FROM users WHERE pubkey = $1'; + const uuidRes1 = await pool.query(userQuery, [pubkey1]); + const uuidRes2 = await pool.query(userQuery, [pubkey2]); + + if (!uuidRes1.rows[0] || !uuidRes2.rows[0]) { + throw new Error('One or both users not found'); + } + + const uuid1 = uuidRes1.rows[0].uuid; + const uuid2 = uuidRes2.rows[0].uuid; + + const roomRes = await pool.query( + 'INSERT INTO room (uuid) VALUES (DEFAULT) RETURNING uuid' + ); + const roomid = roomRes.rows[0].uuid; + + await pool.query( + `INSERT INTO room_members (room_uuid, user_uuid) + VALUES ($1, $2), ($1, $3)`, + [roomid, uuid1, uuid2] + ); + return roomid; + } catch (err) { + console.error('Error creating the room:', err); + throw err; // Re-throw to handle in calling code + } + }, getPublicKeys: async () => { try { const result = await pool.query('SELECT pubkey FROM users'); diff --git a/src/public/popups-logged.js b/src/public/popups-logged.js new file mode 100644 index 0000000..8dacba8 --- /dev/null +++ b/src/public/popups-logged.js @@ -0,0 +1,51 @@ +const currentUrl = window.location.href; + +// handle key presses (close/confirm) +document.addEventListener("keydown", async function(event) { + if (event.isComposing || event.key === 'Escape') { + Array.from(document.getElementsByClassName("popup")).forEach(function(x) { + x.style.display = 'none'; + }); + document.getElementById("publickeyadd").innerText = ""; + } else if (event.key === 'Enter') { + if (addPopup.style.display == 'flex') { + await addConfirm(); + } + } +}); + +// add popup +document.getElementById("add").addEventListener("click", function () { + addPopup.style.display = 'flex'; +}); +// cancel +document.getElementById("addcancel").addEventListener("click", function () { + addPopup.style.display = 'none'; + document.getElementById("publickeyadd").innerText = ""; +}); +//confirm +document.getElementById("addconfirm").addEventListener("click", async () => { + await addConfirm(); +}); + +export async function addConfirm() { + const apiUrl = `${currentUrl}chat/add`; + const inputFieldPublicKey = document.getElementById("publickeyadd"); + + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + pubkey: inputFieldPublicKey.value + }) + }); + + if (!response.ok) { + throw new Error('Failed to add'); + } else { + inputFieldPublicKey.value = ''; + location.reload(); + } +} diff --git a/src/routes/chat.js b/src/routes/chat.js index 2b7a041..74ec200 100644 --- a/src/routes/chat.js +++ b/src/routes/chat.js @@ -1,5 +1,9 @@ -// const express = require("express"); -// const chatController = require("../controllers/chat"); -// const router = express.Router(); -// -// module.exports = router; +const express = require("express"); +const chatController = require("../controllers/chat"); +const router = express.Router(); + +router + .route("/add") + .post(chatController.add); + +module.exports = router; diff --git a/src/routes/index.js b/src/routes/index.js index 90ec39d..34810b1 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -1,11 +1,11 @@ const express = require("express"); const rootRoutes = require('./root'); const accountRoutes = require('./account.js'); -// const chatRoutes = require('./chat.js'); +const chatRoutes = require('./chat.js'); const router = express.Router(); router.use("/", rootRoutes); router.use("/account", accountRoutes); -// router.use("/chat", chatRoutes); +router.use("/chat", chatRoutes); module.exports = router; diff --git a/src/stringutils.js b/src/stringutils.js index 7bf368f..66d9698 100644 --- a/src/stringutils.js +++ b/src/stringutils.js @@ -1,9 +1,52 @@ +const { subtle } = require('node:crypto').webcrypto; + const stringutils = { hexToArray: (hexString) => { return Uint8Array.from(Buffer.from(hexString, 'hex')); }, arrayToHex: (hex) => { return Buffer.from(hex).toString('hex'); + }, + pemToHex: (pemKey) => { + const base64 = pemKey.replace(`-----BEGIN PUBLIC KEY-----`, '').replace(`-----END PUBLIC KEY-----`, '').trim().replaceAll('\n',''); + const buffer = Buffer.from(base64, 'base64'); + const hex = buffer.toString('hex'); + return hex + }, + hexToPem: (hexString) => { + const buffer = Buffer.from(hexString, 'hex'); + const base64 = buffer.toString('base64'); + // split into 64 character long chunks for PEM format + const base64Chunks = base64.match(/.{1,64}/g) || []; + return `-----BEGIN PUBLIC KEY-----\n${base64Chunks.join('\n')}\n-----END PUBLIC KEY-----`; + }, + hexToKey: async (hexString) => { + const uint8Array = stringutils.hexToArray(hexString); + const publicKey = await subtle.importKey( + "spki", + uint8Array, + { + name: "Ed25519", + }, + true, + ["verify"], + ); + return publicKey; + }, + pemToKey: async (pemKey) => { + const base64 = pemKey.replace(`-----BEGIN PUBLIC KEY-----`, '').replace(`-----END PUBLIC KEY-----`, '').trim(); + const buffer = Buffer.from(base64, 'base64'); + const uint8Array = new Uint8Array(buffer); + const publicKey = await subtle.importKey( + "spki", + uint8Array, + { + name: "Ed25519", + }, + true, + ["verify"], + ); + return publicKey; } } module.exports = stringutils; diff --git a/src/views/index.pug b/src/views/index.pug index fa323e5..4385ca7 100644 --- a/src/views/index.pug +++ b/src/views/index.pug @@ -13,6 +13,7 @@ html(lang="en-US") script(src="/pubkey.js", defer) script(src="/noble-curves.js", defer) script(type="module", src="/ecdh.js", defer) + script(type="module", src="/popups-logged.js", defer) else script(type="module", src="/popups.js", defer) script(type="module", src="/register.js", defer) @@ -47,7 +48,8 @@ html(lang="en-US") else .btn-toolbar.btn-group-sm(role="toolbar", aria-label="Toolbar") .btn-group.mr-2(role="group", aria-label="logout") - a#logout.btn.btn-secondary(href="./account/logout") logout + a#logout.btn.btn-secondary(href="/account/logout") logout + button#add.btn.btn-secondary(type="button") add .d-flex.mb-3 @@ -60,5 +62,12 @@ html(lang="en-US") input#input(autocomplete="off") button Send + #addPopup.popup + .popup-content + .btn-group.mr-2.w-100(role="group", aria-label="Add group") + input#publickeyadd.form-control.input-sm.w-50(type="text", placeholder="public key", required) + button#addconfirm.btn.btn-secondary(type="button") confirm + button#addcancel.btn.btn-secondary(type="button") cancel + diff --git a/tests/authentication.test.js b/tests/authentication.test.js index ac2b418..93efaeb 100644 --- a/tests/authentication.test.js +++ b/tests/authentication.test.js @@ -1,7 +1,36 @@ const { subtle } = require('node:crypto').webcrypto; const authentication = require('../src/authentication'); +const stringutils = require("../src/stringutils"); describe('authentication module', () => { + it('conversion between pem and hex', async () => { + let pemKey = '-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEALrj4A3Vftz5TgWXEHi5KG+HD+uQLGB3bGc4TprDi9kE=\n-----END PUBLIC KEY-----'; + let hexKey = stringutils.pemToHex(pemKey); + let pemKeyFromHex = stringutils.hexToPem(hexKey); + expect(pemKeyFromHex).toBe(pemKey); + }); + + it('should convert a pemkey to hex', async () => { + const { publicKey, privateKey } = await crypto.subtle.generateKey( + { + name: "Ed25519", + }, + true, + ["sign", "verify"], + ); + const exportedPubkey = await crypto.subtle.exportKey("spki", publicKey); + const exportedBuffer = Buffer.from(exportedPubkey); + const exportedAsBase64 = exportedBuffer.toString('base64'); + let pemKey = `-----BEGIN PUBLIC KEY-----\n${exportedAsBase64}\n-----END PUBLIC KEY-----`; + let hexKey = stringutils.pemToHex(pemKey); + let importedKey = await stringutils.hexToKey(hexKey); + const exportedPubkeyAgain = await crypto.subtle.exportKey("spki", importedKey); + const exportedBufferAgain = Buffer.from(exportedPubkeyAgain); + const exportedAsBase64Again = exportedBufferAgain.toString('base64'); + let pemKeyAgain = `-----BEGIN PUBLIC KEY-----\n${exportedAsBase64Again}\n-----END PUBLIC KEY-----`; + expect(pemKeyAgain).toBe(pemKey); + }); + it('should return the public key if the signature is verified successfully', async () => { const { publicKey, privateKey } = await crypto.subtle.generateKey( { @@ -11,10 +40,11 @@ describe('authentication module', () => { ["sign", "verify"], ); const exportedPubkey = await crypto.subtle.exportKey("spki", publicKey); - let exportedAsString = String.fromCharCode.apply(null, new Uint8Array(exportedPubkey)); - let exportedAsBase64 = Buffer.from(exportedAsString, 'binary').toString('base64'); + const exportedBuffer = Buffer.from(exportedPubkey); + const exportedAsBase64 = exportedBuffer.toString('base64'); let pemKey = `-----BEGIN PUBLIC KEY-----\n${exportedAsBase64}\n-----END PUBLIC KEY-----`; - const publicKeys = [pemKey,]; + let hexKey = stringutils.pemToHex(pemKey); + const publicKeys = [hexKey,]; const msg = 'test message'; const encoder = new TextEncoder(); const encodedData = encoder.encode(msg); @@ -26,10 +56,11 @@ describe('authentication module', () => { encodedData, ); const result = await authentication.verifySignature(encodedData, signature, publicKeys); - expect(result).toBe(pemKey); + expect(result).toBe(hexKey); let fakeKey = `-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAmrBLT6lyiFh/eUticsIFNY6AkjXuQPqj0Qvb99pCJJk=\n-----END PUBLIC KEY-----` - const result2 = await authentication.verifySignature(encodedData, signature, [fakeKey,]); + let fakeKeyHex = stringutils.pemToHex(fakeKey); + const result2 = await authentication.verifySignature(encodedData, signature, [fakeKeyHex,]); expect(result2).toBe(null); }); @@ -62,10 +93,11 @@ describe('authentication module', () => { const sig = Buffer.from(signatureBase64, 'base64'); - let pemKey2 = '-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEALrj4A3Vftz5TgWXEHi5KG+HD+uQLGB3bGc4TprDi9kE=\n-----END PUBLIC KEY-----' - const publicKeys2 = [pemKey2,]; + let pemKey2 = '-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEALrj4A3Vftz5TgWXEHi5KG+HD+uQLGB3bGc4TprDi9kE=\n-----END PUBLIC KEY-----'; + let hexKey2 = stringutils.pemToHex(pemKey2); + const publicKeys2 = [hexKey2,]; const result3 = await authentication.verifySignature(encodedData2, sig, publicKeys2); - expect(result3).toBe(pemKey2); + expect(result3).toBe(hexKey2); }); });