send and verify login challenge
This commit is contained in:
		| @@ -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" | ||||
|   } | ||||
|   | ||||
							
								
								
									
										27
									
								
								src/app.js
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								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, () => { | ||||
|   | ||||
| @@ -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; | ||||
|     } | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
							
								
								
									
										20
									
								
								src/db.js
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								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'); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -18,6 +18,7 @@ | ||||
|       <div class="btn-toolbar btn-group-sm" role="toolbar" aria-label="Toolbar"> | ||||
|         <div class="btn-group mr-2" role="group" aria-label="register"> | ||||
|           <button id="register" class="btn btn-secondary" type="button">register</button> | ||||
|           <button id="login" class="btn btn-secondary" type="button">login</button> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div id="registerPopup" class="popup"> | ||||
| @@ -31,6 +32,15 @@ | ||||
|           <div id="registerPopupText"></div> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div id="loginPopup" class="popup"> | ||||
|         <div class="popup-content"> | ||||
|           <div class="btn-group mr-2 w-100" role="group" aria-label="Add group"> | ||||
|             <input id="privatekey" type="password" class="form-control input-sm w-50" placeholder="private key" required> | ||||
|             <button id="loginconfirm" class="btn btn-secondary" type="button">login</button> | ||||
|             <button id="logincancel" class="btn btn-secondary" type="button">cancel</button> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|       <a href="./account/cookie" class="btn btn-primary">Get cookie</a> | ||||
|       <ul id="messages"></ul> | ||||
|       <form id="form" action=""> | ||||
|   | ||||
| @@ -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); | ||||
| }); | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user