diff --git a/package.json b/package.json index 08b32eb..832470b 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "node-couchdb": "^1.1.0", "node-forge": "^0.6.46", "redis": "^2.6.3", + "secure-compare": "^3.0.1", "speakeasy": "^2.0.0" }, "devDependencies": { diff --git a/src/config.json b/src/config.json index cc6ec57..53cafdc 100644 --- a/src/config.json +++ b/src/config.json @@ -2,7 +2,7 @@ "port": 3000, "redisConnection": "redis://127.0.0.1:6379", "couchDBName": "secretin", - "couchDBAuth": true, + "couchDBAuth": false, "couchDBConnection": { "host": "127.0.0.1", "protocol": "http", diff --git a/src/index.js b/src/index.js index bd22bee..6399805 100644 --- a/src/index.js +++ b/src/index.js @@ -25,6 +25,7 @@ import unshare from './routes/Unshare'; import share from './routes/Share'; import testTotp from './routes/TestTotp'; import reset from './routes/Reset'; +import getRescueCodes from './routes/GetRescueCodes'; const app = express(); app.server = http.createServer(app); @@ -46,6 +47,7 @@ initializeDb(config, (couchdb, redis) => { app.use('/user', updateUser({ couchdb, redis })); app.use('/protectKey', getProtectKey({ couchdb, redis })); app.use('/activateTotp', activateTotp({ couchdb, redis })); + app.use('/rescueCodes', getRescueCodes({ couchdb, redis })); app.use('/deactivateTotp', deactivateTotp({ couchdb, redis })); app.use('/activateShortLogin', activateShortLogin({ couchdb, redis })); app.use('/secret', getSecret({ couchdb, redis })); diff --git a/src/routes/ActivateTotp.js b/src/routes/ActivateTotp.js index 1525e96..21c73de 100644 --- a/src/routes/ActivateTotp.js +++ b/src/routes/ActivateTotp.js @@ -23,6 +23,7 @@ export default ({ couchdb }) => { }; 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/GetProtectKey.js b/src/routes/GetProtectKey.js index a2007a6..3d7be3f 100644 --- a/src/routes/GetProtectKey.js +++ b/src/routes/GetProtectKey.js @@ -1,5 +1,6 @@ import { Router } from 'express'; import forge from 'node-forge'; +import compare from 'secure-compare'; import Console from '../console'; import Utils from '../utils'; @@ -25,6 +26,9 @@ export default ({ redis, couchdb }) => { }) .then((rIsBruteforce) => { isBruteforce = rIsBruteforce; + if (isBruteforce) { + return Promise.resolve(); + } const key = `protectKey_${req.params.name}_${req.params.deviceId}`; return redis.hgetallAsync(key); }) @@ -35,7 +39,8 @@ export default ({ redis, couchdb }) => { const md = forge.md.sha256.create(); md.update(req.params.hash); - if (!content || isBruteforce || md.digest().toHex() !== content.hash) { + const validHash = compare(md.digest().toHex(), content.hash); + if (!content || isBruteforce || !validHash) { if (!content) { content = { salt: forge.util.bytesToHex(forge.random.getBytesSync(32)), diff --git a/src/routes/GetRescueCodes.js b/src/routes/GetRescueCodes.js new file mode 100644 index 0000000..046c09c --- /dev/null +++ b/src/routes/GetRescueCodes.js @@ -0,0 +1,50 @@ +import { Router } from 'express'; +import url from 'url'; + +import Console from '../console'; +import Utils from '../utils'; + +export default ({ couchdb }) => { + const route = Router(); + route.get('/:name', (req, res) => { + let rescueCodes; + Utils.checkSignature({ + couchdb, + name: req.params.name, + sig: req.query.sig, + data: `${req.baseUrl}${url.parse(req.url).pathname}`, + }) + .then((rawUser) => { + const user = rawUser.data; + + if (user.pass.totp) { + /* Retrocompatibility */ + if (typeof user.rescueCodes === 'undefined') { + const doc = { + _id: rawUser.id, + _rev: rawUser.rev, + user: { + [req.params.name]: rawUser.data, + }, + }; + rescueCodes = Utils.generateRescueCodes(); + doc.user[req.params.name].rescueCodes = rescueCodes; + return couchdb.update(couchdb.databaseName, doc); + } + /* End retrocompatibility */ + rescueCodes = user.rescueCodes; + } else { + rescueCodes = []; + } + return Promise.resolve(); + }) + .then(() => { + res.json(rescueCodes); + }) + .catch((error) => { + Console.error(res, error); + }); + }); + + return route; +}; diff --git a/src/routes/GetUser.js b/src/routes/GetUser.js index c88d5b6..6f9b4c7 100644 --- a/src/routes/GetUser.js +++ b/src/routes/GetUser.js @@ -2,6 +2,7 @@ import { Router } from 'express'; import forge from 'node-forge'; import speakeasy from 'speakeasy'; import url from 'url'; +import compare from 'secure-compare'; import Console from '../console'; import Utils from '../utils'; @@ -26,6 +27,8 @@ export default ({ redis, couchdb }) => { route.get('/:name/:hash', (req, res) => { let rawUser; let submitUser; + let totpValid; + let isBruteforce; Utils.userExists({ couchdb, name: req.params.name }) .then((user) => { rawUser = user; @@ -40,10 +43,14 @@ export default ({ redis, couchdb }) => { } return Utils.checkBruteforce({ redis, ip }); }) - .then((isBruteforce) => { + .then((rIsBruteforce) => { submitUser = rawUser.data; + isBruteforce = rIsBruteforce; + if (isBruteforce) { + return Promise.resolve(); + } - let totpValid = true; + totpValid = true; if (submitUser.pass.totp && req.params.hash !== 'undefined') { totpValid = false; const protectedSeed = Utils.hexStringToUint8Array(submitUser.seed); @@ -54,13 +61,34 @@ export default ({ redis, couchdb }) => { encoding: 'hex', token: req.query.otp, }); - } + if (!totpValid && typeof submitUser.rescueCodes !== 'undefined' && submitUser.rescueCodes.shift() === parseInt(req.query.otp, 10)) { + totpValid = true; + const doc = { + _id: rawUser.id, + _rev: rawUser.rev, + user: { + [req.params.name]: rawUser.data, + }, + }; + 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; + } + return couchdb.update(couchdb.databaseName, doc); + } + } + return Promise.resolve(); + }).then(() => { const md = forge.md.sha256.create(); md.update(req.params.hash); - // if something goes wrong, send fake private key - if (!totpValid || isBruteforce || md.digest().toHex() !== submitUser.pass.hash) { + const validHash = compare(md.digest().toHex(), submitUser.pass.hash); + if (!totpValid || isBruteforce || !validHash) { submitUser.privateKey = { privateKey: forge.util.bytesToHex(forge.random.getBytesSync(3232)), iv: forge.util.bytesToHex(forge.random.getBytesSync(16)), @@ -75,6 +103,7 @@ export default ({ redis, couchdb }) => { .then((allMetadatas) => { submitUser.metadatas = allMetadatas; delete submitUser.seed; + delete submitUser.rescueCodes; delete submitUser.pass.hash; res.json(submitUser); }) @@ -99,6 +128,7 @@ export default ({ redis, couchdb }) => { const user = rawUser.data; user.metadatas = allMetadatas; delete user.seed; + delete user.rescueCodes; delete user.pass.hash; res.json(user); }) diff --git a/src/utils.js b/src/utils.js index 6dcbbfb..5d72d8d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -169,6 +169,18 @@ function checkSignature({ couchdb, name, sig, data }) { }); } +function generateRescueCodes() { + const rescueCodes = []; + const randomBytes = forge.random.getBytesSync(6 * 2); + let rescueCode = 0; + for (let i = 0; i < randomBytes.length; i += 2) { + // eslint-disable-next-line + rescueCode = randomBytes[i].charCodeAt(0) + (randomBytes[i + 1].charCodeAt(0) << 8); + rescueCodes.push(rescueCode); + } + return rescueCodes; +} + const Utils = { userExists, reason, @@ -178,6 +190,7 @@ const Utils = { xorSeed, checkSignature, secretExists, + generateRescueCodes, }; export default Utils;