send and verify login challenge
This commit is contained in:
@ -10,8 +10,10 @@
|
|||||||
"license": "BSD-3-Clause-Attribution",
|
"license": "BSD-3-Clause-Attribution",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
|
"connect-sqlite3": "^0.9.15",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
|
"express-session": "^1.18.1",
|
||||||
"pg": "^8.13.1",
|
"pg": "^8.13.1",
|
||||||
"socket.io": "^4.8.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();
|
const app = express();
|
||||||
var http = require('http').Server(app);
|
var http = require('http').Server(app);
|
||||||
const port = process.env.PORT || 3333;
|
const port = process.env.PORT || 3333;
|
||||||
app.set("port", port);
|
|
||||||
var io = require('socket.io')(http);
|
var io = require('socket.io')(http);
|
||||||
const cookieParser = require('cookie-parser');
|
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
|
//database
|
||||||
const database = require(__dirname + '/db');
|
const database = require(__dirname + '/db');
|
||||||
@ -17,7 +11,28 @@ database.init();
|
|||||||
|
|
||||||
//routes
|
//routes
|
||||||
const routes = require(__dirname + '/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);
|
app.use("/", routes);
|
||||||
|
// bootstrap
|
||||||
|
app.use('/css', express.static(__dirname + '/node_modules/bootstrap/dist/css'));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//start server
|
//start server
|
||||||
var server = http.listen(port, () => {
|
var server = http.listen(port, () => {
|
||||||
|
@ -1,8 +1,49 @@
|
|||||||
|
const { subtle } = require('node:crypto').webcrypto;
|
||||||
|
|
||||||
const sharedSecret = process.env.SHARED_SECRET;
|
const sharedSecret = process.env.SHARED_SECRET;
|
||||||
|
|
||||||
const authentication = {
|
const authentication = {
|
||||||
checkSharedSecret: (providedSecret) => {
|
checkSharedSecret: (providedSecret) => {
|
||||||
return sharedSecret === 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);
|
console.error("Error during registration:", error);
|
||||||
return res.status(500).json({ error: "Server error during registration" });
|
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;
|
module.exports = accountController;
|
||||||
|
20
src/db.js
20
src/db.js
@ -34,13 +34,13 @@ const database = {
|
|||||||
},
|
},
|
||||||
createTables: () => {
|
createTables: () => {
|
||||||
pool.query(`
|
pool.query(`
|
||||||
CREATE TABLE IF NOT EXISTS "user" (
|
CREATE TABLE IF NOT EXISTS "users" (
|
||||||
uuid integer PRIMARY KEY,
|
uuid integer PRIMARY KEY,
|
||||||
pubkey text
|
pubkey text
|
||||||
);
|
);
|
||||||
`, (err, _) => {
|
`, (err, _) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('Error creating user table', err);
|
console.error('Error creating users table', err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pool.query(`
|
pool.query(`
|
||||||
@ -51,7 +51,7 @@ const database = {
|
|||||||
if (err) {
|
if (err) {
|
||||||
console.error('Error creating sequence', err);
|
console.error('Error creating sequence', err);
|
||||||
} else {
|
} 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) => {
|
return new Promise((resolve, reject) => {
|
||||||
pool.query(`
|
pool.query(`
|
||||||
SELECT EXISTS (
|
SELECT EXISTS (
|
||||||
SELECT 1 FROM pg_tables WHERE tablename = 'user'
|
SELECT 1 FROM pg_tables WHERE tablename = 'users'
|
||||||
) AS table_exists;
|
) AS table_exists;
|
||||||
`, (err, res) => {
|
`, (err, res) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
@ -93,13 +93,23 @@ const database = {
|
|||||||
const result = await pool.query('SELECT NEXTVAL(\'uuid_sequence\') AS next_uuid');
|
const result = await pool.query('SELECT NEXTVAL(\'uuid_sequence\') AS next_uuid');
|
||||||
const nextUuid = result.rows[0].next_uuid;
|
const nextUuid = result.rows[0].next_uuid;
|
||||||
await pool.query(
|
await pool.query(
|
||||||
'INSERT INTO "user" (uuid, pubkey) VALUES ($1, $2)',
|
'INSERT INTO "users" (uuid, pubkey) VALUES ($1, $2)',
|
||||||
[nextUuid, pubkey]
|
[nextUuid, pubkey]
|
||||||
);
|
);
|
||||||
console.log(`Added user with the public key ${pubkey} .`);
|
console.log(`Added user with the public key ${pubkey} .`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error adding user:', 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));
|
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) {
|
function exportedKeyToPem(key, type) {
|
||||||
let exportedAsString = ab2str(key);
|
let exportedAsString = ab2str(key);
|
||||||
let exportedAsBase64 = window.btoa(exportedAsString);
|
let exportedAsBase64 = window.btoa(exportedAsString);
|
||||||
return `-----BEGIN ${type.toUpperCase()} KEY-----\n${exportedAsBase64}\n-----END ${type.toUpperCase()} KEY-----`;
|
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() {
|
async function genKey() {
|
||||||
// Generate keys
|
// Generate keys
|
||||||
const { publicKey, privateKey } = await crypto.subtle.generateKey(
|
const { publicKey, privateKey } = await crypto.subtle.generateKey(
|
||||||
@ -30,64 +59,3 @@ async function genKey() {
|
|||||||
}
|
}
|
||||||
return key;
|
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-toolbar btn-group-sm" role="toolbar" aria-label="Toolbar">
|
||||||
<div class="btn-group mr-2" role="group" aria-label="register">
|
<div class="btn-group mr-2" role="group" aria-label="register">
|
||||||
<button id="register" class="btn btn-secondary" type="button">register</button>
|
<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>
|
</div>
|
||||||
<div id="registerPopup" class="popup">
|
<div id="registerPopup" class="popup">
|
||||||
@ -31,6 +32,15 @@
|
|||||||
<div id="registerPopupText"></div>
|
<div id="registerPopupText"></div>
|
||||||
</div>
|
</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>
|
<a href="./account/cookie" class="btn btn-primary">Get cookie</a>
|
||||||
<ul id="messages"></ul>
|
<ul id="messages"></ul>
|
||||||
<form id="form" action="">
|
<form id="form" action="">
|
||||||
|
@ -58,3 +58,57 @@ document.getElementById("registerconfirm").addEventListener("click", async funct
|
|||||||
throw new Error('Error in server response');
|
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")
|
.route("/register")
|
||||||
.post(accountController.register);
|
.post(accountController.register);
|
||||||
|
|
||||||
|
router
|
||||||
|
.route("/login")
|
||||||
|
.get(accountController.loginGetChallenge);
|
||||||
|
|
||||||
|
router
|
||||||
|
.route("/verify-challenge")
|
||||||
|
.post(accountController.loginVerifyChallenge);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
Reference in New Issue
Block a user