From 4deb25962e59c8398ef7cb91927f4e92f3453048 Mon Sep 17 00:00:00 2001 From: Sam Hadow Date: Tue, 11 Feb 2025 19:07:13 +0100 Subject: [PATCH] send and verify login challenge --- package.json | 2 + src/app.js | 27 +++++++++--- src/authentication.js | 41 +++++++++++++++++ src/controllers/account.js | 23 +++++++++- src/db.js | 20 ++++++--- src/public/ecc.js | 90 ++++++++++++-------------------------- src/public/index.html | 10 +++++ src/public/popups.js | 54 +++++++++++++++++++++++ src/routes/account.js | 8 ++++ 9 files changed, 202 insertions(+), 73 deletions(-) diff --git a/package.json b/package.json index 39d1684..7bb3735 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,10 @@ "license": "BSD-3-Clause-Attribution", "dependencies": { "bootstrap": "^5.3.3", + "connect-sqlite3": "^0.9.15", "cookie-parser": "^1.4.7", "express": "^4.21.2", + "express-session": "^1.18.1", "pg": "^8.13.1", "socket.io": "^4.8.1" } diff --git a/src/app.js b/src/app.js index 36762e5..f0895f0 100644 --- a/src/app.js +++ b/src/app.js @@ -2,14 +2,8 @@ const express = require('express'); const app = express(); var http = require('http').Server(app); const port = process.env.PORT || 3333; -app.set("port", port); var io = require('socket.io')(http); const cookieParser = require('cookie-parser'); -app.use(cookieParser()); -app.use(express.json()); - -// bootstrap -app.use('/css', express.static(__dirname + '/node_modules/bootstrap/dist/css')); //database const database = require(__dirname + '/db'); @@ -17,7 +11,28 @@ database.init(); //routes const routes = require(__dirname + '/routes'); + +// session (used for login challenge) +const session = require('express-session'); +const SQLiteStore = require('connect-sqlite3')(session); + +// configure app +app.set("port", port); +app.use(cookieParser()); +app.use(express.json()); +app.use(session({ + store: new SQLiteStore, + secret: process.env.COOKIE_SECRET || "toto", + resave: false, + saveUninitialized: true, + cookie: { maxAge: 7 * 24 * 60 * 60 * 1000 } // 1 week +})); app.use("/", routes); +// bootstrap +app.use('/css', express.static(__dirname + '/node_modules/bootstrap/dist/css')); + + + //start server var server = http.listen(port, () => { diff --git a/src/authentication.js b/src/authentication.js index 5368ae0..0461144 100644 --- a/src/authentication.js +++ b/src/authentication.js @@ -1,8 +1,49 @@ +const { subtle } = require('node:crypto').webcrypto; + const sharedSecret = process.env.SHARED_SECRET; const authentication = { checkSharedSecret: (providedSecret) => { return sharedSecret === providedSecret; + }, + verifySignature : async (msg, sig, publicKeys) => { + try { + for (const pemPubKey of publicKeys) { + try { + const pubKey = await authentication.pemToKey(pemPubKey); + const verified = await subtle.verify( + 'Ed25519', + pubKey, + sig, + msg + ); + if (verified) { + console.log('Signature verified successfully with public key:', pemPubKey); + return pemPubKey; + } + } catch (err) { + console.log('Failed to verify signature with public key:', pemPubKey, 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 96b7ff3..0f2f08c 100644 --- a/src/controllers/account.js +++ b/src/controllers/account.js @@ -38,7 +38,28 @@ const accountController = { console.error("Error during registration:", error); return res.status(500).json({ error: "Server error during registration" }); } - } + }, + loginGetChallenge: async (req, res) => { + let randomBuffer = crypto.randomBytes(16); + let randomNumber = randomBuffer.toString('hex'); + req.session.randomNumber = randomNumber; + return res.status(200).json({ + message: "Challenge generated successfully", + challenge: randomNumber + }); + }, + loginVerifyChallenge: async (req, res) => { + const { signature } = req.body; + const publicKeys = await database.getPublicKeys(); + const msg = new TextEncoder().encode(req.session.randomNumber); + const sig = new TextEncoder().encode(signature); + let validKey = authentication.verifySignature(msg, sig, publicKeys); + if (validKey !== null) { + return res.status(200).json({ message: "Challenge solved successfully" }); + } else { + return res.status(400).json({ error: "Challenge failed" }); + } + } }; module.exports = accountController; diff --git a/src/db.js b/src/db.js index 3932d02..7bb4c44 100644 --- a/src/db.js +++ b/src/db.js @@ -34,13 +34,13 @@ const database = { }, createTables: () => { pool.query(` - CREATE TABLE IF NOT EXISTS "user" ( + CREATE TABLE IF NOT EXISTS "users" ( uuid integer PRIMARY KEY, pubkey text ); `, (err, _) => { if (err) { - console.error('Error creating user table', err); + console.error('Error creating users table', err); return; } pool.query(` @@ -51,7 +51,7 @@ const database = { if (err) { console.error('Error creating sequence', err); } else { - console.log("user table and sequence created successfully."); + console.log("users table and sequence created successfully."); } }); }); @@ -60,7 +60,7 @@ const database = { return new Promise((resolve, reject) => { pool.query(` SELECT EXISTS ( - SELECT 1 FROM pg_tables WHERE tablename = 'user' + SELECT 1 FROM pg_tables WHERE tablename = 'users' ) AS table_exists; `, (err, res) => { if (err) { @@ -93,13 +93,23 @@ const database = { const result = await pool.query('SELECT NEXTVAL(\'uuid_sequence\') AS next_uuid'); const nextUuid = result.rows[0].next_uuid; await pool.query( - 'INSERT INTO "user" (uuid, pubkey) VALUES ($1, $2)', + 'INSERT INTO "users" (uuid, pubkey) VALUES ($1, $2)', [nextUuid, pubkey] ); console.log(`Added user with the public key ${pubkey} .`); } catch (err) { console.error('Error adding user:', err); } + }, + getPublicKeys: async () => { + try { + const result = await pool.query('SELECT pubkey FROM users'); + const publicKeys = result.rows.map(row => row.pubkey); + return publicKeys; + } catch (err) { + console.error('Error retrieving public keys:', err); + throw new Error('Error retrieving public keys'); + } } }; diff --git a/src/public/ecc.js b/src/public/ecc.js index 2e18557..0016c70 100644 --- a/src/public/ecc.js +++ b/src/public/ecc.js @@ -6,12 +6,41 @@ function ab2str(buf) { return String.fromCharCode.apply(null, new Uint8Array(buf)); } +/* +Convert a string into an ArrayBuffer +from https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String +*/ +function str2ab(str) { + const buf = new ArrayBuffer(str.length); + const bufView = new Uint8Array(buf); + for (let i = 0, strLen = str.length; i < strLen; i++) { + bufView[i] = str.charCodeAt(i); + } + return buf; +} + function exportedKeyToPem(key, type) { let exportedAsString = ab2str(key); let exportedAsBase64 = window.btoa(exportedAsString); return `-----BEGIN ${type.toUpperCase()} KEY-----\n${exportedAsBase64}\n-----END ${type.toUpperCase()} KEY-----`; } +function pemToKey(pemKey, type) { + const base64 = pemKey.replace(`-----BEGIN ${type.toUpperCase()} KEY-----`, '').replace(`-----END ${type.toUpperCase()} KEY-----`, '').trim(); + const binaryDerString = window.atob(base64); + const binaryDer = str2ab(binaryDerString); + return window.crypto.subtle.importKey( + "pkcs8", + binaryDer, + { + name: "Ed25519", + }, + true, + ["sign"], + ); +} + + async function genKey() { // Generate keys const { publicKey, privateKey } = await crypto.subtle.generateKey( @@ -30,64 +59,3 @@ async function genKey() { } return key; } - -async function test(data) { - console.log(`Message: ${data}`); - try { - // Generate keys - const { publicKey, privateKey } = await crypto.subtle.generateKey( - { - name: "Ed25519", - }, - true, - ["sign", "verify"], - ); - - console.log(`publicKey: ${publicKey}, type: ${publicKey.type}`); - console.log(`privateKey: ${privateKey}, type: ${privateKey.type}`); - - // Encode data prior to signing - const encoder = new TextEncoder(); - encodedData = encoder.encode(data); - - // Log the first part of the encoded data - const shorterEncodedBuffer = new Uint8Array(encodedData.buffer, 0, 14); - console.log( - `encodedData: ${shorterEncodedBuffer}...[${encodedData.byteLength} bytes total]`, - ); - //console.log(`encodedData: ${encodedData}`); - - // Sign the data using the private key. - const signature = await crypto.subtle.sign( - { - name: "Ed25519", - }, - privateKey, - encodedData, - ); - - // Log the first part of the signature data - const signatureBuffer = new Uint8Array(signature, 0, 14); - console.log( - `signature: ${signatureBuffer}...[${signature.byteLength} bytes total]`, - ); - - // Verify the signature using the public key - const verifyResult = await crypto.subtle.verify( - { - name: "Ed25519", - }, - publicKey, - signature, - encodedData, - ); - - // Log result - true if the text was signed with the corresponding public key. - console.log(`signature verified?: ${verifyResult}`); - } catch (error) { - console.log(error); - } -} - -test('ceci est un test'); -genKey() diff --git a/src/public/index.html b/src/public/index.html index 8537d04..a038878 100644 --- a/src/public/index.html +++ b/src/public/index.html @@ -18,6 +18,7 @@ + Get cookie
diff --git a/src/public/popups.js b/src/public/popups.js index 1f321d8..4b3860e 100644 --- a/src/public/popups.js +++ b/src/public/popups.js @@ -58,3 +58,57 @@ document.getElementById("registerconfirm").addEventListener("click", async funct throw new Error('Error in server response'); } }); + +//login popup +document.getElementById("login").addEventListener("click", function () { + loginPopup.style.display = 'flex'; +}); +// cancel +document.getElementById("logincancel").addEventListener("click", function () { + loginPopup.style.display = 'none'; +}); +// confirm +document.getElementById("loginconfirm").addEventListener("click", async function () { + const apiUrl = `${currentUrl}account/login`; + const inputFieldPrivateKey = document.getElementById("privatekey"); + const response = await fetch(apiUrl, { method: 'GET' }); + if (!response.ok) { + throw new Error('Failed to fetch challenge'); + } + const { challenge } = await response.json(); + console.log("Received challenge:", challenge); + + privKey = await pemToKey(inputFieldPrivateKey.value, "private"); + + const encoder = new TextEncoder(); + encodedData = encoder.encode(challenge); + + // Sign the data using the private key. + const signature = await crypto.subtle.sign( + { + name: "Ed25519", + }, + privKey, + encodedData, + ); + let signatureString = ab2str(signature); + let signatureBase64 = window.btoa(signatureString); + + const verifyApiUrl = `${currentUrl}account/verify-challenge`; + const verifyResponse = await fetch(verifyApiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + signature: signatureBase64 + }) + }); + + if (!verifyResponse.ok) { + throw new Error('Failed to verify the challenge'); + } + + const verifyResult = await verifyResponse.json(); + console.log("Verification result:", verifyResult); +}); diff --git a/src/routes/account.js b/src/routes/account.js index b7792d1..9e121c0 100644 --- a/src/routes/account.js +++ b/src/routes/account.js @@ -10,4 +10,12 @@ router .route("/register") .post(accountController.register); +router + .route("/login") + .get(accountController.loginGetChallenge); + +router + .route("/verify-challenge") + .post(accountController.loginVerifyChallenge); + module.exports = router;