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,6 +38,27 @@ 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" });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
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