send and verify login challenge

This commit is contained in:
2025-02-11 19:07:13 +01:00
parent e5ffbac3ea
commit 4deb25962e
9 changed files with 202 additions and 73 deletions

View File

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

View File

@ -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, () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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