From 6cd313678a19079389c82a1b3c3f0372ebbb665a Mon Sep 17 00:00:00 2001 From: Florian Date: Sat, 24 Dec 2022 05:05:06 +0100 Subject: [PATCH] Changing password require oldHash --- src/routes/ActivateTotp.js | 1 - src/routes/GetRescueCodes.js | 34 +++++++++++++++++++ src/routes/GetUser.js | 50 +++++++++++++++++----------- src/routes/UpdateUser.js | 64 +++++++++++++++++++++++++++--------- src/utils.js | 22 +++++++++++-- 5 files changed, 132 insertions(+), 39 deletions(-) diff --git a/src/routes/ActivateTotp.js b/src/routes/ActivateTotp.js index 7ab8bf8..71c9056 100644 --- a/src/routes/ActivateTotp.js +++ b/src/routes/ActivateTotp.js @@ -24,7 +24,6 @@ export default ({ couchdb, redis }) => { }; doc.user[req.params.name].pass.totp = true; doc.user[req.params.name].seed = jsonBody.seed; - doc.user[req.params.name].rescueCodes = Utils.generateRescueCodes(); return couchdb.update(couchdb.databaseName, doc); }) .then(() => { diff --git a/src/routes/GetRescueCodes.js b/src/routes/GetRescueCodes.js index 84f9b83..1c41d7b 100644 --- a/src/routes/GetRescueCodes.js +++ b/src/routes/GetRescueCodes.js @@ -41,5 +41,39 @@ export default ({ couchdb, redis }) => { }); }); + route.put('/:name', (req, res) => { + Utils.checkSignature({ + couchdb, + redis, + name: req.params.name, + sig: req.body.sig, + data: `${req.body.json}|${req.body.sigTime}`, + }) + .then(rawUser => { + const jsonBody = JSON.parse(req.body.json); + const user = rawUser.data; + + if (user.pass.totp) { + const doc = { + _id: rawUser.id, + _rev: rawUser.rev, + user: { + [req.params.name]: rawUser.data, + }, + }; + + doc.user[req.params.name].rescueCodes = jsonBody.rescueCodes; + return couchdb.update(couchdb.databaseName, doc); + } + return Promise.resolve(); + }) + .then(() => { + Utils.reason(res, 200, 'RescueCodes saved'); + }) + .catch(error => { + Console.error(res, error); + }); + }); + return route; }; diff --git a/src/routes/GetUser.js b/src/routes/GetUser.js index 25e7d0a..e7c71bd 100644 --- a/src/routes/GetUser.js +++ b/src/routes/GetUser.js @@ -55,9 +55,7 @@ export default ({ redis, couchdb }) => { totpValid = false; const protectedSeed = Utils.hexStringToUint8Array(submitUser.seed); const hash = Utils.hexStringToUint8Array(req.params.hash); - const seed = Utils.bytesToHexString( - Utils.xorSeed(hash, protectedSeed) - ); + const seed = Utils.xorSeed(protectedSeed, hash) totpValid = speakeasy.totp.verify({ secret: seed, encoding: 'hex', @@ -65,27 +63,39 @@ export default ({ redis, couchdb }) => { }); if ( !totpValid && - typeof submitUser.rescueCodes !== 'undefined' && - submitUser.rescueCodes.shift() === parseInt(req.query.otp, 10) + typeof submitUser.rescueCodes !== 'undefined' ) { - totpValid = true; - const doc = { - _id: rawUser.id, - _rev: rawUser.rev, - user: { - [req.params.name]: rawUser.data, - }, - }; + const protectedNextRescueCode = submitUser.rescueCodes.shift() + // New rescue code are more robust + if(protectedNextRescueCode.length === 8) { + const nextRescueCode = Utils.xorRescueCode(Utils.hexStringToUint8Array(protectedNextRescueCode), hash) + if(compare(nextRescueCode, req.query.otp)) { + totpValid = true + } + // Legacy check with weak rescue code + } else if (protectedNextRescueCode === parseInt(req.query.otp, 10)){ + totpValid = true; + } + + if(totpValid) { + const doc = { + _id: rawUser.id, + _rev: rawUser.rev, + user: { + [req.params.name]: rawUser.data, + }, + }; - doc.user[req.params.name].rescueCodes = submitUser.rescueCodes; + doc.user[req.params.name].rescueCodes = submitUser.rescueCodes; - if (submitUser.rescueCodes.length === 0) { - submitUser.pass.totp = false; - doc.user[req.params.name].pass.totp = false; - delete doc.user[req.params.name].seed; - delete doc.user[req.params.name].rescueCodes; + if (submitUser.rescueCodes.length === 0) { + submitUser.pass.totp = false; + doc.user[req.params.name].pass.totp = false; + delete doc.user[req.params.name].seed; + delete doc.user[req.params.name].rescueCodes; + } + return couchdb.update(couchdb.databaseName, doc); } - return couchdb.update(couchdb.databaseName, doc); } } return Promise.resolve(); diff --git a/src/routes/UpdateUser.js b/src/routes/UpdateUser.js index f00f58b..1411d8d 100644 --- a/src/routes/UpdateUser.js +++ b/src/routes/UpdateUser.js @@ -1,20 +1,26 @@ -import { Router } from 'express'; +import { + Router +} from 'express'; import forge from 'node-forge'; +import compare from 'secure-compare'; import Console from '../console'; import Utils from '../utils'; -export default ({ couchdb, redis }) => { +export default ({ + couchdb, + redis +}) => { const route = Router(); route.put('/:name', (req, res) => { let jsonBody; Utils.checkSignature({ - couchdb, - redis, - name: req.params.name, - sig: req.body.sig, - data: `${req.body.json}|${req.body.sigTime}`, - }) + couchdb, + redis, + name: req.params.name, + sig: req.body.sig, + data: `${req.body.json}|${req.body.sigTime}`, + }) .then(rawUser => { jsonBody = JSON.parse(req.body.json); const doc = { @@ -33,16 +39,44 @@ export default ({ couchdb, redis }) => { } else { doc.user[req.params.name].options = jsonBody; } - } else { - const md = forge.md.sha256.create(); - md.update(jsonBody.pass.hash); - jsonBody.pass.hash = md.digest().toHex(); + return Promise.resolve(doc) + } - doc.user[req.params.name].privateKey = jsonBody.privateKey; - doc.user[req.params.name].pass = jsonBody.pass; + let ip; + if ( + process.env.BEHIND_REVERSE_PROXY && + process.env.BEHIND_REVERSE_PROXY === '1' + ) { + ip = req.headers['x-forwarded-for'] || req.ip; + } else { + ip = req.ip; } - return couchdb.update(couchdb.databaseName, doc); + + return Utils.checkBruteforce({ + redis, + ip + }) + .then((isBruteforce) => { + if (!isBruteforce) { + // Changing password + const mdOldHash = forge.md.sha256.create(); + mdOldHash.update(jsonBody.oldHash); + const validHash = compare(mdOldHash.digest().toHex(), doc.user[req.params.name].pass.hash); + if (!validHash) { + throw new Error('Invalid old password') + } + + const md = forge.md.sha256.create(); + md.update(jsonBody.pass.hash); + jsonBody.pass.hash = md.digest().toHex(); + + doc.user[req.params.name].privateKey = jsonBody.privateKey; + doc.user[req.params.name].pass = jsonBody.pass; + } + return doc + }) }) + .then((doc) => couchdb.update(couchdb.databaseName, doc)) .then(() => { Utils.reason(res, 200, 'User updated'); }) diff --git a/src/utils.js b/src/utils.js index 4d2a570..ed03b87 100644 --- a/src/utils.js +++ b/src/utils.js @@ -128,9 +128,24 @@ function xorSeed(byteArray1, byteArray2) { for (i = 0; i < 32; i += 1) { buf[i] = byteArray1[i] ^ byteArray2[i]; } - return buf; + return bytesToHexString(buf); } - throw 'xorSeed wait for 32 bytes arrays'; + throw new Error('xorSeed wait for 32 bytes arrays'); +} + +function xorRescueCode(rescueCode, hash) { + if ( + hash.length === 32 && + rescueCode.length === 4 + ) { + const buf = new Uint8Array(rescueCode.length); + let i; + for (i = 0; i < rescueCode.length; i += 1) { + buf[i] = rescueCode[i] ^ hash[i]; + } + return bytesToHexString(buf); + } + throw new Error('xorRescueCode wrong bytes arrays'); } function checkSignature({ couchdb, redis, name, sig, data }) { @@ -214,13 +229,14 @@ function generateRescueCodes() { const Utils = { userExists, reason, + generateRescueCodes, checkBruteforce, hexStringToUint8Array, bytesToHexString, xorSeed, checkSignature, secretExists, - generateRescueCodes, + xorRescueCode, }; export default Utils;