diff --git a/package-lock.json b/package-lock.json index 69218075..19848caa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "version": "4.0.2", "license": "GPL-3.0-only", "dependencies": { - "argon2-browser": "^1.15.2", "geoip-country": "^4.0.42" }, "bin": { @@ -25,7 +24,6 @@ "@formatjs/cli": "^2.13.11", "@react-hook/media-query": "^1.1.1", "@react-hook/window-size": "^3.0.7", - "@types/argon2-browser": "^1.12.0", "@types/async": "^3.2.3", "@types/bencode": "^2.0.0", "@types/body-parser": "^1.19.0", @@ -108,6 +106,7 @@ "fs-extra": "^9.0.1", "get-user-locale": "^1.4.0", "glob": "^7.1.6", + "hash-wasm": "^4.4.0", "html-webpack-plugin": "^5.0.0-alpha.10", "http-errors": "^1.8.0", "jest": "^26.6.3", @@ -2365,12 +2364,6 @@ "@sinonjs/commons": "^1.7.0" } }, - "node_modules/@types/argon2-browser": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@types/argon2-browser/-/argon2-browser-1.12.0.tgz", - "integrity": "sha512-ZPpKOoLuyXf+dKUJKcbUAzAajnJ1+ABXqSyHtsjfDaKhdHh19wXNWC3/hpDdzIO8ykiSONnGCWy38juDZVocvg==", - "dev": true - }, "node_modules/@types/async": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/@types/async/-/async-3.2.3.tgz", @@ -4050,11 +4043,6 @@ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "dev": true }, - "node_modules/argon2-browser": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/argon2-browser/-/argon2-browser-1.15.2.tgz", - "integrity": "sha512-kn9rs/+dZk0K4L9Vj8ZDdNw+s9vTYvmHCi8TT7z5OHeoFEwJnElZioICNLWXCve9sHc0d6TfeD0b1GKcscsuwg==" - }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -10999,6 +10987,12 @@ "node": ">=4" } }, + "node_modules/hash-wasm": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/hash-wasm/-/hash-wasm-4.4.0.tgz", + "integrity": "sha512-1E7F1crJ4l2z1UZDrhp/uF0QiiZqSZszccgFE0u0sofi3iEf/yAv1rVw+xss2FChGZWsvDLSTXOuEyDRe3N2Sg==", + "dev": true + }, "node_modules/hash.js": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", @@ -28690,12 +28684,6 @@ "@sinonjs/commons": "^1.7.0" } }, - "@types/argon2-browser": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@types/argon2-browser/-/argon2-browser-1.12.0.tgz", - "integrity": "sha512-ZPpKOoLuyXf+dKUJKcbUAzAajnJ1+ABXqSyHtsjfDaKhdHh19wXNWC3/hpDdzIO8ykiSONnGCWy38juDZVocvg==", - "dev": true - }, "@types/async": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/@types/async/-/async-3.2.3.tgz", @@ -30209,11 +30197,6 @@ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "dev": true }, - "argon2-browser": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/argon2-browser/-/argon2-browser-1.15.2.tgz", - "integrity": "sha512-kn9rs/+dZk0K4L9Vj8ZDdNw+s9vTYvmHCi8TT7z5OHeoFEwJnElZioICNLWXCve9sHc0d6TfeD0b1GKcscsuwg==" - }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -35797,6 +35780,12 @@ "safe-buffer": "^5.2.0" } }, + "hash-wasm": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/hash-wasm/-/hash-wasm-4.4.0.tgz", + "integrity": "sha512-1E7F1crJ4l2z1UZDrhp/uF0QiiZqSZszccgFE0u0sofi3iEf/yAv1rVw+xss2FChGZWsvDLSTXOuEyDRe3N2Sg==", + "dev": true + }, "hash.js": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", diff --git a/package.json b/package.json index f6365137..979fbef6 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "scripts": { "build": "npm run build-assets && npm run build-ts", "build-assets": "node client/scripts/build.js", - "build-ts": "ncc build server/bin/start.ts -m -t -e argon2-browser -e geoip-country", + "build-ts": "ncc build server/bin/start.ts -m -t -e geoip-country", "build-docs": "jsdoc -c ./.jsdoc.json", "build-i18n": "formatjs compile --ast --format simple client/src/javascript/i18n/strings.json --out-file client/src/javascript/i18n/strings.compiled.json && formatjs compile-folder --ast --format simple client/src/javascript/i18n/translations client/src/javascript/i18n/compiled", "deprecated-warning": "node client/scripts/deprecated-warning.js && sleep 10", @@ -51,7 +51,6 @@ "test:client": "FLOOD_OPTION_port=4200 start-server-and-test start 4200 'cypress run'" }, "dependencies": { - "argon2-browser": "^1.15.2", "geoip-country": "^4.0.42" }, "devDependencies": { @@ -65,7 +64,6 @@ "@formatjs/cli": "^2.13.11", "@react-hook/media-query": "^1.1.1", "@react-hook/window-size": "^3.0.7", - "@types/argon2-browser": "^1.12.0", "@types/async": "^3.2.3", "@types/bencode": "^2.0.0", "@types/body-parser": "^1.19.0", @@ -148,6 +146,7 @@ "fs-extra": "^9.0.1", "get-user-locale": "^1.4.0", "glob": "^7.1.6", + "hash-wasm": "^4.4.0", "html-webpack-plugin": "^5.0.0-alpha.10", "http-errors": "^1.8.0", "jest": "^26.6.3", diff --git a/server/bin/migrations/UserInDatabase2.ts b/server/bin/migrations/UserInDatabase2.ts index 7522cf59..3296b9be 100644 --- a/server/bin/migrations/UserInDatabase2.ts +++ b/server/bin/migrations/UserInDatabase2.ts @@ -72,16 +72,13 @@ const migration = () => { return; } - Users.createUser( - userV2, - (_username, errCreation) => { - if (errCreation) { - migrationError(errCreation); - } - + Users.createUser(userV2, false).then( + () => { migratedResolve(); }, - false, + (errCreation) => { + migrationError(errCreation); + }, ); }); }); diff --git a/server/models/Users.ts b/server/models/Users.ts index af0f0299..b76ec6af 100644 --- a/server/models/Users.ts +++ b/server/models/Users.ts @@ -1,4 +1,4 @@ -import argon2 from 'argon2-browser'; +import {argon2id, argon2Verify} from 'hash-wasm'; import crypto from 'crypto'; import Datastore from 'nedb'; import fs from 'fs'; @@ -45,69 +45,99 @@ class Users { }); } - comparePassword( - credentials: Pick, - callback: (isMatch: boolean, level: Credentials['level'] | null, err?: Error) => void, - ) { - this.db.findOne({username: credentials.username}, (err: Error | null, user: UserInDatabase): void => { - if (err) { - return callback(false, null, err); - } + /** + * Validates the provided password against the hashed password in database + * + * @param {Pick} credentials - Username and password + * @return {Promise} - Returns access level of the user if matched or rejects with error. + */ + async comparePassword(credentials: Pick): Promise { + return new Promise((resolve, reject) => { + this.db.findOne({username: credentials.username}, (err: Error | null, user: UserInDatabase): void => { + if (err) { + reject(err); + return; + } - // Wrong data provided - if (credentials?.password == null) { - return callback(false, null, new Error()); - } + // Wrong data provided + if (credentials?.password == null) { + reject(new Error()); + return; + } - // Username not found. - if (user == null) { - return callback(false, null, user); - } + // Username not found. + if (user == null) { + reject(new Error()); + return; + } - argon2 - .verify({pass: credentials.password, encoded: user.password}) - .then(() => callback(true, user.level)) - .catch((e) => callback(false, null, e)); - - return undefined; + argon2Verify({password: credentials.password, hash: user.password}).then( + (isMatch) => { + if (isMatch) { + resolve(user.level); + } else { + reject(new Error()); + } + }, + (verifyErr) => { + reject(verifyErr); + }, + ); + }); }); } - createUser( - credentials: Credentials, - callback: (user: UserInDatabase | null, error?: Error) => void, - shouldHash = true, - ): void { - if (this.db == null) { - return callback(null, new Error('Users database is not ready.')); + /** + * Creates a new user. + * Note that validation function always expects an argon2 hash. + * + * @param {Credentials} credentials - Full credentials of a user. + * @param {boolean} shouldHash - Should the password be hashed or stored as-is. + * @return {Promise} - Returns the created user or rejects with error. + */ + async createUser(credentials: Credentials, shouldHash = true): Promise { + const hashed = shouldHash + ? await argon2id({ + password: credentials.password, + salt: crypto.randomBytes(16), + parallelism: 1, + iterations: 256, + memorySize: 512, + hashLength: 32, + outputType: 'encoded', + }).catch(() => undefined) + : credentials.password; + + if (this.db == null || hashed == null) { + return Promise.reject(new Error()); } - argon2 - .hash({pass: credentials.password, salt: crypto.randomBytes(16).toString('hex')}) - .then((hash) => { - this.db.insert( - { - ...credentials, - password: shouldHash ? hash.encoded : credentials.password, - }, - (error, user) => { - if (error) { - if (error.message.includes('violates the unique constraint')) { - return callback(null, new Error('Username already exists.')); - } - - return callback(null, error); + return new Promise((resolve, reject) => { + this.db.insert( + { + ...credentials, + password: hashed, + }, + (error, user) => { + if (error) { + if (error.message.includes('violates the unique constraint')) { + reject(new Error('Username already exists.')); + return; } - return callback(user as UserInDatabase); - }, - ); - }) - .catch((error) => { - callback(null, error); - }); + reject(new Error()); + return; + } - return undefined; + if (user == null) { + reject(new Error()); + return; + } + + resolve(user as UserInDatabase); + }, + ); + }); } removeUser( diff --git a/server/routes/api/auth.ts b/server/routes/api/auth.ts index f9b75ee7..d2a9033e 100644 --- a/server/routes/api/auth.ts +++ b/server/routes/api/auth.ts @@ -112,20 +112,20 @@ router.post('/authenticate', (req, const credentials = parsedResult.data; - Users.comparePassword(credentials, (isMatch, level, _err) => { - if (isMatch === true && level != null) { + Users.comparePassword(credentials).then( + (level) => { sendAuthenticationResponse(res, { ...credentials, level, }); - return; - } - - // Incorrect username or password. - res.status(401).json({ - message: failedLoginResponse, - }); - }); + }, + () => { + // Incorrect username or password. + res.status(401).json({ + message: failedLoginResponse, + }); + }, + ); }); // Allow unauthenticated registration if no users are currently registered. @@ -180,21 +180,21 @@ router.post('/regis const credentials = parsedResult.data; // Attempt to save the user - Users.createUser(credentials, (user, error) => { - if (error || user == null) { - ajaxUtil.getResponseFn(res)({username: credentials.username}, error); - return; - } + Users.createUser(credentials).then( + (user) => { + services.bootstrapServicesForUser(user); - services.bootstrapServicesForUser(user); + if (req.query.cookie === 'false') { + ajaxUtil.getResponseFn(res)({username: user.username}); + return; + } - if (req.query.cookie === 'false') { - ajaxUtil.getResponseFn(res)({username: user.username}); - return; - } - - sendAuthenticationResponse(res, credentials); - }); + sendAuthenticationResponse(res, credentials); + }, + (err) => { + ajaxUtil.getResponseFn(res)({username: credentials.username}, err); + }, + ); }); // Allow unauthenticated verification if no users are currently registered.