change representation of public key in db to hex string + create room

This commit is contained in:
Sam Hadow 2025-02-21 19:16:50 +01:00
parent da4f74b60c
commit 4d68e7a9f7
11 changed files with 239 additions and 90 deletions

View File

@ -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;
}
};

View File

@ -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" });
}

View File

@ -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;

View File

@ -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;

View File

@ -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');

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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);
});
});