diff --git a/core/servers/content/web.js b/core/servers/content/web.js index f26ecc53..463c0767 100644 --- a/core/servers/content/web.js +++ b/core/servers/content/web.js @@ -49,18 +49,18 @@ class Route { isValid() { return ( (this.pathRegExp instanceof RegExp && - -1 !== - [ - 'GET', - 'HEAD', - 'POST', - 'PUT', - 'DELETE', - 'CONNECT', - 'OPTIONS', - 'TRACE', - ].indexOf(this.method)) || - !_.isFunction(this.handler) + -1 !== + [ + 'GET', + 'HEAD', + 'POST', + 'PUT', + 'DELETE', + 'CONNECT', + 'OPTIONS', + 'TRACE', + ].indexOf(this.method)) || + !_.isFunction(this.handler) ); } @@ -96,13 +96,13 @@ exports.getModule = class WebServerModule extends ServerModule { } buildUrl(pathAndQuery) { - // - // Create a URL such as - // https://l33t.codes:44512/ + |pathAndQuery| - // - // Prefer HTTPS over HTTP. Be explicit about the port - // only if non-standard. Allow users to override full prefix in config. - // + // + // Create a URL such as + // https://l33t.codes:44512/ + |pathAndQuery| + // + // Prefer HTTPS over HTTP. Be explicit about the port + // only if non-standard. Allow users to override full prefix in config. + // const config = Config(); if (_.isString(config.contentServers.web.overrideUrlPrefix)) { return `${config.contentServers.web.overrideUrlPrefix}${pathAndQuery}`; @@ -113,15 +113,15 @@ exports.getModule = class WebServerModule extends ServerModule { if (config.contentServers.web.https.enabled) { schema = 'https://'; port = - 443 === config.contentServers.web.https.port - ? '' - : `:${config.contentServers.web.https.port}`; + 443 === config.contentServers.web.https.port + ? '' + : `:${config.contentServers.web.https.port}`; } else { schema = 'http://'; port = - 80 === config.contentServers.web.http.port - ? '' - : `:${config.contentServers.web.http.port}`; + 80 === config.contentServers.web.http.port + ? '' + : `:${config.contentServers.web.http.port}`; } return `${schema}${config.contentServers.web.domain}${port}${pathAndQuery}`; @@ -168,11 +168,17 @@ exports.getModule = class WebServerModule extends ServerModule { try { const normalizedName = _.camelCase(module.moduleInfo.name); if (!WebHandlerModule.isEnabled(normalizedName)) { - Log.info({ moduleName: normalizedName }, 'Skipping web handler module - not enabled.'); + Log.info( + { moduleName: normalizedName }, + 'Skipping web handler module - not enabled.' + ); return nextModule(null); } - Log.info({ moduleName: normalizedName }, 'Initializing web handler module.'); + Log.info( + { moduleName: normalizedName }, + 'Initializing web handler module.' + ); moduleInst.init(err => { return nextModule(err); @@ -329,8 +335,8 @@ exports.getModule = class WebServerModule extends ServerModule { const headers = { 'Content-Type': - mimeTypes.contentType(paths.basename(filePath)) || - mimeTypes.contentType('.bin'), + mimeTypes.contentType(paths.basename(filePath)) || + mimeTypes.contentType('.bin'), 'Content-Length': stats.size, }; @@ -362,8 +368,8 @@ exports.getModule = class WebServerModule extends ServerModule { const headers = { 'Content-Type': - mimeTypes.contentType(paths.basename(filePath)) || - mimeTypes.contentType('.bin'), + mimeTypes.contentType(paths.basename(filePath)) || + mimeTypes.contentType('.bin'), 'Content-Length': stats.size, }; diff --git a/core/servers/content/web_handlers/activitypub.js b/core/servers/content/web_handlers/activitypub.js index 126f4421..7c3ffa0d 100644 --- a/core/servers/content/web_handlers/activitypub.js +++ b/core/servers/content/web_handlers/activitypub.js @@ -58,7 +58,10 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule { userFromAccount(accountName, (err, user) => { if (err) { - Log.info({ accountName: accountName }, 'Unable to find user from account retrieving self url.'); + Log.info( + { accountName: accountName }, + 'Unable to find user from account retrieving self url.' + ); return this._notFound(resp); } @@ -71,23 +74,21 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule { if (sendActor) { return this._selfAsActorHandler(user, req, resp); - } - else { + } else { return this._standardSelfHandler(user, req, resp); } }); } _selfAsActorHandler(user, req, resp) { - - const sUrl = selfUrl(this.webServer, user); + const selfUrl = selfUrl(this.webServer, user); const bodyJson = { '@context': [ 'https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1', ], - id: sUrl, + id: selfUrl, type: 'Person', preferredUsername: user.username, name: user.getSanitizedName('real'), @@ -105,11 +106,14 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule { }; const publicKeyPem = user.getProperty(UserProps.PublicKeyMain); - if (!(_.isEmpty(publicKeyPem))) { - bodyJson['publicKey'] = { id: sUrl + '#main-key', owner: sUrl, publicKeyPem: user.getProperty(UserProps.PublicKeyMain) }; - } - else { - Log.debug({ User: user.username }, 'User does not have a publickey.'); + if (!_.isEmpty(publicKeyPem)) { + bodyJson['publicKey'] = { + id: selfUrl + '#main-key', + owner: sUrl, + publicKeyPem: user.getProperty(UserProps.PublicKeyMain), + }; + } else { + Log.debug({ username: user.username }, 'User does not have a publickey.'); } const body = JSON.stringify(bodyJson); diff --git a/core/user.js b/core/user.js index 3ea4c5d9..8d6b1653 100644 --- a/core/user.js +++ b/core/user.js @@ -21,1005 +21,1009 @@ const sanatizeFilename = require('sanitize-filename'); const ssh2 = require('ssh2'); module.exports = class User { - constructor() { - this.userId = 0; - this.username = ''; - this.properties = {}; // name:value - this.groups = []; // group membership(s) - this.authFactor = User.AuthFactors.None; - this.statusFlags = User.StatusFlags.None; - } - - // static property accessors - static get RootUserID() { - return 1; - } - - static get AuthFactors() { - return { - None: 0, // Not yet authenticated in any way - Factor1: 1, // username + password/pubkey/etc. checked out - Factor2: 2, // validated with 2FA of some sort such as OTP - }; - } - - static get PBKDF2() { - return { - iterations: 1000, - keyLen: 128, - saltLen: 32, - }; - } - - static get StandardPropertyGroups() { - return { - auth: [ - UserProps.PassPbkdf2Salt, - UserProps.PassPbkdf2Dk, - UserProps.AuthPubKey, - ], - }; - } - - static get AccountStatus() { - return { - disabled: 0, // +op disabled - inactive: 1, // inactive, aka requires +op approval/activation - active: 2, // standard, active - locked: 3, // locked out (too many bad login attempts, etc.) - }; - } - - static get StatusFlags() { - return { - None: 0x00000000, - NotAvailable: 0x00000001, // Not currently available for chat, message, page, etc. - NotVisible: 0x00000002, // Invisible -- does not show online, last callers, etc. - }; - } - - isAuthenticated() { - return true === this.authenticated; - } - - isValid() { - if (this.userId <= 0 || this.username.length < Config().users.usernameMin) { - return false; + constructor() { + this.userId = 0; + this.username = ''; + this.properties = {}; // name:value + this.groups = []; // group membership(s) + this.authFactor = User.AuthFactors.None; + this.statusFlags = User.StatusFlags.None; } - return this.hasValidPasswordProperties(); - } - - hasValidPasswordProperties() { - const salt = this.getProperty(UserProps.PassPbkdf2Salt); - const dk = this.getProperty(UserProps.PassPbkdf2Dk); - - if ( - !salt || - !dk || - salt.length !== User.PBKDF2.saltLen * 2 || - dk.length !== User.PBKDF2.keyLen * 2 - ) { - return false; + // static property accessors + static get RootUserID() { + return 1; } - return true; - } - - isRoot() { - return User.isRootUserId(this.userId); - } - - isSysOp() { - // alias to isRoot() - return this.isRoot(); - } - - isGroupMember(groupNames) { - if (_.isString(groupNames)) { - groupNames = [groupNames]; + static get AuthFactors() { + return { + None: 0, // Not yet authenticated in any way + Factor1: 1, // username + password/pubkey/etc. checked out + Factor2: 2, // validated with 2FA of some sort such as OTP + }; } - const isMember = groupNames.some(gn => -1 !== this.groups.indexOf(gn)); - return isMember; - } - - getSanitizedName(type = 'username') { - const name = - 'real' === type ? this.getProperty(UserProps.RealName) : this.username; - return sanatizeFilename(name) || `user${this.userId.toString()}`; - } - - isAvailable() { - return (this.statusFlags & User.StatusFlags.NotAvailable) == 0; - } - - isVisible() { - return (this.statusFlags & User.StatusFlags.NotVisible) == 0; - } - - setAvailability(available) { - if (available) { - this.statusFlags &= ~User.StatusFlags.NotAvailable; - } else { - this.statusFlags |= User.StatusFlags.NotAvailable; - } - } - - setVisibility(visible) { - if (visible) { - this.statusFlags &= ~User.StatusFlags.NotVisible; - } else { - this.statusFlags |= User.StatusFlags.NotVisible; - } - } - - getLegacySecurityLevel() { - if (this.isRoot() || this.isGroupMember('sysops')) { - return 100; + static get PBKDF2() { + return { + iterations: 1000, + keyLen: 128, + saltLen: 32, + }; } - if (this.isGroupMember('users')) { - return 30; + static get StandardPropertyGroups() { + return { + auth: [ + UserProps.PassPbkdf2Salt, + UserProps.PassPbkdf2Dk, + UserProps.AuthPubKey, + ], + }; } - return 10; // :TODO: Is this what we want? - } + static get AccountStatus() { + return { + disabled: 0, // +op disabled + inactive: 1, // inactive, aka requires +op approval/activation + active: 2, // standard, active + locked: 3, // locked out (too many bad login attempts, etc.) + }; + } - processFailedLogin(userId, cb) { - async.waterfall( - [ - callback => { - return User.getUser(userId, callback); - }, - (tempUser, callback) => { - return StatLog.incrementUserStat( - tempUser, - UserProps.FailedLoginAttempts, - 1, - (err, failedAttempts) => { - return callback(null, tempUser, failedAttempts); + static get StatusFlags() { + return { + None: 0x00000000, + NotAvailable: 0x00000001, // Not currently available for chat, message, page, etc. + NotVisible: 0x00000002, // Invisible -- does not show online, last callers, etc. + }; + } + + isAuthenticated() { + return true === this.authenticated; + } + + isValid() { + if (this.userId <= 0 || this.username.length < Config().users.usernameMin) { + return false; + } + + return this.hasValidPasswordProperties(); + } + + hasValidPasswordProperties() { + const salt = this.getProperty(UserProps.PassPbkdf2Salt); + const dk = this.getProperty(UserProps.PassPbkdf2Dk); + + if ( + !salt || + !dk || + salt.length !== User.PBKDF2.saltLen * 2 || + dk.length !== User.PBKDF2.keyLen * 2 + ) { + return false; + } + + return true; + } + + isRoot() { + return User.isRootUserId(this.userId); + } + + isSysOp() { + // alias to isRoot() + return this.isRoot(); + } + + isGroupMember(groupNames) { + if (_.isString(groupNames)) { + groupNames = [groupNames]; + } + + const isMember = groupNames.some(gn => -1 !== this.groups.indexOf(gn)); + return isMember; + } + + getSanitizedName(type = 'username') { + const name = + 'real' === type ? this.getProperty(UserProps.RealName) : this.username; + return sanatizeFilename(name) || `user${this.userId.toString()}`; + } + + isAvailable() { + return (this.statusFlags & User.StatusFlags.NotAvailable) == 0; + } + + isVisible() { + return (this.statusFlags & User.StatusFlags.NotVisible) == 0; + } + + setAvailability(available) { + if (available) { + this.statusFlags &= ~User.StatusFlags.NotAvailable; + } else { + this.statusFlags |= User.StatusFlags.NotAvailable; + } + } + + setVisibility(visible) { + if (visible) { + this.statusFlags &= ~User.StatusFlags.NotVisible; + } else { + this.statusFlags |= User.StatusFlags.NotVisible; + } + } + + getLegacySecurityLevel() { + if (this.isRoot() || this.isGroupMember('sysops')) { + return 100; + } + + if (this.isGroupMember('users')) { + return 30; + } + + return 10; // :TODO: Is this what we want? + } + + processFailedLogin(userId, cb) { + async.waterfall( + [ + callback => { + return User.getUser(userId, callback); + }, + (tempUser, callback) => { + return StatLog.incrementUserStat( + tempUser, + UserProps.FailedLoginAttempts, + 1, + (err, failedAttempts) => { + return callback(null, tempUser, failedAttempts); + } + ); + }, + (tempUser, failedAttempts, callback) => { + const lockAccount = _.get(Config(), 'users.failedLogin.lockAccount'); + if (lockAccount > 0 && failedAttempts >= lockAccount) { + const props = { + [UserProps.AccountStatus]: User.AccountStatus.locked, + [UserProps.AccountLockedTs]: StatLog.now, + }; + if ( + !_.has(tempUser.properties, UserProps.AccountLockedPrevStatus) + ) { + props[UserProps.AccountLockedPrevStatus] = + tempUser.getProperty(UserProps.AccountStatus); + } + Log.info( + { userId, failedAttempts }, + '(Re)setting account to locked due to failed logins' + ); + return tempUser.persistProperties(props, callback); + } + + return cb(null); + }, + ], + err => { + return cb(err); } - ); - }, - (tempUser, failedAttempts, callback) => { - const lockAccount = _.get(Config(), 'users.failedLogin.lockAccount'); - if (lockAccount > 0 && failedAttempts >= lockAccount) { - const props = { - [UserProps.AccountStatus]: User.AccountStatus.locked, - [UserProps.AccountLockedTs]: StatLog.now, - }; + ); + } + + unlockAccount(cb) { + const prevStatus = this.getProperty(UserProps.AccountLockedPrevStatus); + if (!prevStatus) { + return cb(null); // nothing to do + } + + this.persistProperty(UserProps.AccountStatus, prevStatus, err => { + if (err) { + return cb(err); + } + + return this.removeProperties( + [UserProps.AccountLockedPrevStatus, UserProps.AccountLockedTs], + cb + ); + }); + } + + static get AuthFactor1Types() { + return { + SSHPubKey: 'sshPubKey', + Password: 'password', + TLSClient: 'tlsClientAuth', + }; + } + + authenticateFactor1(authInfo, cb) { + const username = authInfo.username; + const self = this; + const tempAuthInfo = {}; + + const validatePassword = (props, callback) => { + User.generatePasswordDerivedKey( + authInfo.password, + props[UserProps.PassPbkdf2Salt], + (err, dk) => { + if (err) { + return callback(err); + } + + // + // Use constant time comparison here for security feel-goods + // + const passDkBuf = Buffer.from(dk, 'hex'); + const propsDkBuf = Buffer.from(props[UserProps.PassPbkdf2Dk], 'hex'); + + return callback( + crypto.timingSafeEqual(passDkBuf, propsDkBuf) + ? null + : Errors.AccessDenied('Invalid password') + ); + } + ); + }; + + const validatePubKey = (props, callback) => { + const pubKeyActual = ssh2.utils.parseKey(props[UserProps.AuthPubKey]); + if (!pubKeyActual) { + return callback(Errors.AccessDenied('Invalid public key')); + } + if ( - !_.has(tempUser.properties, UserProps.AccountLockedPrevStatus) + authInfo.pubKey.key.algo != pubKeyActual.type || + !crypto.timingSafeEqual( + authInfo.pubKey.key.data, + pubKeyActual.getPublicSSH() + ) ) { - props[UserProps.AccountLockedPrevStatus] = - tempUser.getProperty(UserProps.AccountStatus); + return callback(Errors.AccessDenied('Invalid public key')); } - Log.info( - { userId, failedAttempts }, - '(Re)setting account to locked due to failed logins' - ); - return tempUser.persistProperties(props, callback); - } - return cb(null); - }, - ], - err => { - return cb(err); - } - ); - } + return callback(null); + }; - unlockAccount(cb) { - const prevStatus = this.getProperty(UserProps.AccountLockedPrevStatus); - if (!prevStatus) { - return cb(null); // nothing to do + async.waterfall( + [ + function fetchUserId(callback) { + // get user ID + User.getUserIdAndName(username, (err, uid, un) => { + tempAuthInfo.userId = uid; + tempAuthInfo.username = un; + + return callback(err); + }); + }, + function getRequiredAuthProperties(callback) { + // fetch properties required for authentication + User.loadProperties( + tempAuthInfo.userId, + { names: User.StandardPropertyGroups.auth }, + (err, props) => { + return callback(err, props); + } + ); + }, + function validatePassOrPubKey(props, callback) { + if (User.AuthFactor1Types.SSHPubKey === authInfo.type) { + return validatePubKey(props, callback); + } + return validatePassword(props, callback); + }, + function initProps(callback) { + User.loadProperties(tempAuthInfo.userId, (err, allProps) => { + if (!err) { + tempAuthInfo.properties = allProps; + } + + return callback(err); + }); + }, + function checkAccountStatus(callback) { + const accountStatus = parseInt( + tempAuthInfo.properties[UserProps.AccountStatus], + 10 + ); + if (User.AccountStatus.disabled === accountStatus) { + return callback( + Errors.AccessDenied('Account disabled', ErrorReasons.Disabled) + ); + } + if (User.AccountStatus.inactive === accountStatus) { + return callback( + Errors.AccessDenied('Account inactive', ErrorReasons.Inactive) + ); + } + + if (User.AccountStatus.locked === accountStatus) { + const autoUnlockMinutes = _.get( + Config(), + 'users.failedLogin.autoUnlockMinutes' + ); + const lockedTs = moment( + tempAuthInfo.properties[UserProps.AccountLockedTs] + ); + if (autoUnlockMinutes && lockedTs.isValid()) { + const minutesSinceLocked = moment().diff(lockedTs, 'minutes'); + if (minutesSinceLocked >= autoUnlockMinutes) { + // allow the login - we will clear any lock there + Log.info( + { + username, + userId: tempAuthInfo.userId, + lockedAt: lockedTs.format(), + }, + 'Locked account will now be unlocked due to auto-unlock minutes policy' + ); + return callback(null); + } + } + return callback( + Errors.AccessDenied('Account is locked', ErrorReasons.Locked) + ); + } + + // anything else besides active is still not allowed + if (User.AccountStatus.active !== accountStatus) { + return callback(Errors.AccessDenied('Account is not active')); + } + + return callback(null); + }, + function initGroups(callback) { + userGroup.getGroupsForUser(tempAuthInfo.userId, (err, groups) => { + if (!err) { + tempAuthInfo.groups = groups; + } + + return callback(err); + }); + }, + ], + err => { + if (err) { + // + // If we failed login due to something besides an inactive or disabled account, + // we need to update failure status and possibly lock the account. + // + // If locked already, update the lock timestamp -- ie, extend the lockout period. + // + if ( + ![ErrorReasons.Disabled, ErrorReasons.Inactive].includes( + err.reasonCode + ) && + tempAuthInfo.userId + ) { + self.processFailedLogin(tempAuthInfo.userId, persistErr => { + if (persistErr) { + Log.warn( + { error: persistErr.message }, + 'Failed to persist failed login information' + ); + } + return cb(err); // pass along original error + }); + } else { + return cb(err); + } + } else { + // everything checks out - load up info + self.userId = tempAuthInfo.userId; + self.username = tempAuthInfo.username; + self.properties = tempAuthInfo.properties; + self.groups = tempAuthInfo.groups; + self.authFactor = User.AuthFactors.Factor1; + + // + // If 2FA/OTP is required, this user is not quite authenticated yet. + // + self.authenticated = !(self.getProperty(UserProps.AuthFactor2OTP) + ? true + : false); + + self.removeProperty(UserProps.FailedLoginAttempts); + + // + // We need to *revert* any locked status back to + // the user's previous status & clean up props. + // + self.unlockAccount(unlockErr => { + if (unlockErr) { + Log.warn( + { error: unlockErr.message }, + 'Failed to unlock account' + ); + } + return cb(null); + }); + } + } + ); } - this.persistProperty(UserProps.AccountStatus, prevStatus, err => { - if (err) { - return cb(err); - } + create(createUserInfo, cb) { + assert(0 === this.userId); + const config = Config(); - return this.removeProperties( - [UserProps.AccountLockedPrevStatus, UserProps.AccountLockedTs], - cb - ); - }); - } - - static get AuthFactor1Types() { - return { - SSHPubKey: 'sshPubKey', - Password: 'password', - TLSClient: 'tlsClientAuth', - }; - } - - authenticateFactor1(authInfo, cb) { - const username = authInfo.username; - const self = this; - const tempAuthInfo = {}; - - const validatePassword = (props, callback) => { - User.generatePasswordDerivedKey( - authInfo.password, - props[UserProps.PassPbkdf2Salt], - (err, dk) => { - if (err) { - return callback(err); - } - - // - // Use constant time comparison here for security feel-goods - // - const passDkBuf = Buffer.from(dk, 'hex'); - const propsDkBuf = Buffer.from(props[UserProps.PassPbkdf2Dk], 'hex'); - - return callback( - crypto.timingSafeEqual(passDkBuf, propsDkBuf) - ? null - : Errors.AccessDenied('Invalid password') - ); + if ( + this.username.length < config.users.usernameMin || + this.username.length > config.users.usernameMax + ) { + return cb(Errors.Invalid('Invalid username length')); } - ); - }; - const validatePubKey = (props, callback) => { - const pubKeyActual = ssh2.utils.parseKey(props[UserProps.AuthPubKey]); - if (!pubKeyActual) { - return callback(Errors.AccessDenied('Invalid public key')); - } + const self = this; - if ( - authInfo.pubKey.key.algo != pubKeyActual.type || - !crypto.timingSafeEqual( - authInfo.pubKey.key.data, - pubKeyActual.getPublicSSH() - ) - ) { - return callback(Errors.AccessDenied('Invalid public key')); - } + // :TODO: set various defaults, e.g. default activation status, etc. + self.properties[UserProps.AccountStatus] = config.users.requireActivation + ? User.AccountStatus.inactive + : User.AccountStatus.active; - return callback(null); - }; + async.waterfall( + [ + function beginTransaction(callback) { + return userDb.beginTransaction(callback); + }, + function createUserRec(trans, callback) { + trans.run( + `INSERT INTO user (user_name) + VALUES (?);`, + [self.username], + function inserted(err) { + // use classic function for |this| + if (err) { + return callback(err); + } - async.waterfall( - [ - function fetchUserId(callback) { - // get user ID - User.getUserIdAndName(username, (err, uid, un) => { - tempAuthInfo.userId = uid; - tempAuthInfo.username = un; + self.userId = this.lastID; - return callback(err); - }); - }, - function getRequiredAuthProperties(callback) { - // fetch properties required for authentication - User.loadProperties( - tempAuthInfo.userId, - { names: User.StandardPropertyGroups.auth }, - (err, props) => { - return callback(err, props); + // Do not require activation for userId 1 (root/admin) + if (User.RootUserID === self.userId) { + self.properties[UserProps.AccountStatus] = + User.AccountStatus.active; + } + + return callback(null, trans); + } + ); + }, + function genAuthCredentials(trans, callback) { + User.generatePasswordDerivedKeyAndSalt( + createUserInfo.password, + (err, info) => { + if (err) { + return callback(err); + } + + self.properties[UserProps.PassPbkdf2Salt] = info.salt; + self.properties[UserProps.PassPbkdf2Dk] = info.dk; + return callback(null, trans); + } + ); + }, + function setInitialGroupMembership(trans, callback) { + // Assign initial groups. Must perform a clone: #235 - All users are sysops (and I can't un-sysop them) + self.groups = [...config.users.defaultGroups]; + + if (User.RootUserID === self.userId) { + // root/SysOp? + self.groups.push('sysops'); + } + + return callback(null, trans); + }, + function saveAll(trans, callback) { + self.persistWithTransaction(trans, err => { + return callback(err, trans); + }); + }, + function sendEvent(trans, callback) { + Events.emit(Events.getSystemEvents().NewUser, { + user: Object.assign({}, self, { + sessionId: createUserInfo.sessionId, + }), + }); + return callback(null, trans); + }, + ], + (err, trans) => { + if (trans) { + trans[err ? 'rollback' : 'commit'](transErr => { + return cb(err ? err : transErr); + }); + } else { + return cb(err); + } } - ); - }, - function validatePassOrPubKey(props, callback) { - if (User.AuthFactor1Types.SSHPubKey === authInfo.type) { - return validatePubKey(props, callback); - } - return validatePassword(props, callback); - }, - function initProps(callback) { - User.loadProperties(tempAuthInfo.userId, (err, allProps) => { - if (!err) { - tempAuthInfo.properties = allProps; + ); + } + + persistWithTransaction(trans, cb) { + assert(this.userId > 0); + + const self = this; + + async.series( + [ + function setKeyPair(callback) { + self.generateMainKeyPair(err => { + return callback(err); + }); + }, + function saveProps(callback) { + self.persistProperties(self.properties, trans, err => { + return callback(err); + }); + }, + function saveGroups(callback) { + userGroup.addUserToGroups(self.userId, self.groups, trans, err => { + return callback(err); + }); + }, + ], + err => { + return cb(err); + } + ); + } + + static persistPropertyByUserId(userId, propName, propValue, cb) { + userDb.run( + `REPLACE INTO user_property (user_id, prop_name, prop_value) + VALUES (?, ?, ?);`, + [userId, propName, propValue], + err => { + if (cb) { + return cb(err, propValue); + } + } + ); + } + + setProperty(propName, propValue) { + this.properties[propName] = propValue; + } + + incrementProperty(propName, incrementBy) { + incrementBy = incrementBy || 1; + let newValue = parseInt(this.getProperty(propName)); + if (newValue) { + newValue += incrementBy; + } else { + newValue = incrementBy; + } + this.setProperty(propName, newValue); + return newValue; + } + + getProperty(propName) { + return this.properties[propName]; + } + + getPropertyAsNumber(propName) { + return parseInt(this.getProperty(propName), 10); + } + + persistProperty(propName, propValue, cb) { + // update live props + this.properties[propName] = propValue; + + return User.persistPropertyByUserId(this.userId, propName, propValue, cb); + } + + removeProperty(propName, cb) { + // update live + delete this.properties[propName]; + + userDb.run( + `DELETE FROM user_property + WHERE user_id = ? AND prop_name = ?;`, + [this.userId, propName], + err => { + if (cb) { + return cb(err); + } + } + ); + } + + removeProperties(propNames, cb) { + async.each( + propNames, + (name, next) => { + return this.removeProperty(name, next); + }, + err => { + if (cb) { + return cb(err); + } + } + ); + } + + generateMainKeyPair(cb) { + crypto.generateKeyPair( + 'rsa', + { + modulusLength: 4096, + publicKeyEncoding: { + type: 'spki', + format: 'pem', + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem', + }, + }, + (err, publicKey, privateKey) => { + if (!err) { + this.setProperty(UserProps.PrivateKeyMain, privateKey); + this.setProperty(UserProps.PublicKeyMain, publicKey); + } + return cb(err); + } + ); + } + + persistProperties(properties, transOrDb, cb) { + if (!_.isFunction(cb) && _.isFunction(transOrDb)) { + cb = transOrDb; + transOrDb = userDb; + } + + const self = this; + + // update live props + _.merge(this.properties, properties); + + const stmt = transOrDb.prepare( + `REPLACE INTO user_property (user_id, prop_name, prop_value) + VALUES (?, ?, ?);` + ); + + async.each( + Object.keys(properties), + (propName, nextProp) => { + stmt.run(self.userId, propName, properties[propName], err => { + return nextProp(err); + }); + }, + err => { + if (err) { + return cb(err); + } + + stmt.finalize(() => { + return cb(null); + }); + } + ); + } + + setNewAuthCredentials(password, cb) { + User.generatePasswordDerivedKeyAndSalt(password, (err, info) => { + if (err) { + return cb(err); } - return callback(err); - }); - }, - function checkAccountStatus(callback) { - const accountStatus = parseInt( - tempAuthInfo.properties[UserProps.AccountStatus], - 10 - ); - if (User.AccountStatus.disabled === accountStatus) { - return callback( - Errors.AccessDenied('Account disabled', ErrorReasons.Disabled) - ); - } - if (User.AccountStatus.inactive === accountStatus) { - return callback( - Errors.AccessDenied('Account inactive', ErrorReasons.Inactive) - ); - } + const newProperties = { + [UserProps.PassPbkdf2Salt]: info.salt, + [UserProps.PassPbkdf2Dk]: info.dk, + }; - if (User.AccountStatus.locked === accountStatus) { - const autoUnlockMinutes = _.get( - Config(), - 'users.failedLogin.autoUnlockMinutes' - ); - const lockedTs = moment( - tempAuthInfo.properties[UserProps.AccountLockedTs] - ); - if (autoUnlockMinutes && lockedTs.isValid()) { - const minutesSinceLocked = moment().diff(lockedTs, 'minutes'); - if (minutesSinceLocked >= autoUnlockMinutes) { - // allow the login - we will clear any lock there - Log.info( - { - username, - userId: tempAuthInfo.userId, - lockedAt: lockedTs.format(), - }, - 'Locked account will now be unlocked due to auto-unlock minutes policy' - ); - return callback(null); - } - } - return callback( - Errors.AccessDenied('Account is locked', ErrorReasons.Locked) - ); - } - - // anything else besides active is still not allowed - if (User.AccountStatus.active !== accountStatus) { - return callback(Errors.AccessDenied('Account is not active')); - } - - return callback(null); - }, - function initGroups(callback) { - userGroup.getGroupsForUser(tempAuthInfo.userId, (err, groups) => { - if (!err) { - tempAuthInfo.groups = groups; - } - - return callback(err); - }); - }, - ], - err => { - if (err) { - // - // If we failed login due to something besides an inactive or disabled account, - // we need to update failure status and possibly lock the account. - // - // If locked already, update the lock timestamp -- ie, extend the lockout period. - // - if ( - ![ErrorReasons.Disabled, ErrorReasons.Inactive].includes( - err.reasonCode - ) && - tempAuthInfo.userId - ) { - self.processFailedLogin(tempAuthInfo.userId, persistErr => { - if (persistErr) { - Log.warn( - { error: persistErr.message }, - 'Failed to persist failed login information' - ); - } - return cb(err); // pass along original error + this.persistProperties(newProperties, err => { + return cb(err); }); - } else { - return cb(err); - } - } else { - // everything checks out - load up info - self.userId = tempAuthInfo.userId; - self.username = tempAuthInfo.username; - self.properties = tempAuthInfo.properties; - self.groups = tempAuthInfo.groups; - self.authFactor = User.AuthFactors.Factor1; - - // - // If 2FA/OTP is required, this user is not quite authenticated yet. - // - self.authenticated = !(self.getProperty(UserProps.AuthFactor2OTP) - ? true - : false); - - self.removeProperty(UserProps.FailedLoginAttempts); - - // - // We need to *revert* any locked status back to - // the user's previous status & clean up props. - // - self.unlockAccount(unlockErr => { - if (unlockErr) { - Log.warn( - { error: unlockErr.message }, - 'Failed to unlock account' - ); - } - return cb(null); - }); - } - } - ); - } - - create(createUserInfo, cb) { - assert(0 === this.userId); - const config = Config(); - - if ( - this.username.length < config.users.usernameMin || - this.username.length > config.users.usernameMax - ) { - return cb(Errors.Invalid('Invalid username length')); - } - - const self = this; - - // :TODO: set various defaults, e.g. default activation status, etc. - self.properties[UserProps.AccountStatus] = config.users.requireActivation - ? User.AccountStatus.inactive - : User.AccountStatus.active; - - async.waterfall( - [ - function beginTransaction(callback) { - return userDb.beginTransaction(callback); - }, - function createUserRec(trans, callback) { - trans.run( - `INSERT INTO user (user_name) - VALUES (?);`, - [self.username], - function inserted(err) { - // use classic function for |this| - if (err) { - return callback(err); - } - - self.userId = this.lastID; - - // Do not require activation for userId 1 (root/admin) - if (User.RootUserID === self.userId) { - self.properties[UserProps.AccountStatus] = - User.AccountStatus.active; - } - - return callback(null, trans); - } - ); - }, - function genAuthCredentials(trans, callback) { - User.generatePasswordDerivedKeyAndSalt( - createUserInfo.password, - (err, info) => { - if (err) { - return callback(err); - } - - self.properties[UserProps.PassPbkdf2Salt] = info.salt; - self.properties[UserProps.PassPbkdf2Dk] = info.dk; - return callback(null, trans); - } - ); - }, - function setInitialGroupMembership(trans, callback) { - // Assign initial groups. Must perform a clone: #235 - All users are sysops (and I can't un-sysop them) - self.groups = [...config.users.defaultGroups]; - - if (User.RootUserID === self.userId) { - // root/SysOp? - self.groups.push('sysops'); - } - - return callback(null, trans); - }, - function saveAll(trans, callback) { - self.persistWithTransaction(trans, err => { - return callback(err, trans); - }); - }, - function sendEvent(trans, callback) { - Events.emit(Events.getSystemEvents().NewUser, { - user: Object.assign({}, self, { - sessionId: createUserInfo.sessionId, - }), - }); - return callback(null, trans); - }, - ], - (err, trans) => { - if (trans) { - trans[err ? 'rollback' : 'commit'](transErr => { - return cb(err ? err : transErr); - }); - } else { - return cb(err); - } - } - ); - } - - persistWithTransaction(trans, cb) { - assert(this.userId > 0); - - const self = this; - - async.series( - [ - function setKeyPair(callback) { - self.generateMainKeyPair(err => { - return callback(err); - }); - }, - function saveProps(callback) { - self.persistProperties(self.properties, trans, err => { - return callback(err); - }); - }, - function saveGroups(callback) { - userGroup.addUserToGroups(self.userId, self.groups, trans, err => { - return callback(err); - }); - }, - ], - err => { - return cb(err); - } - ); - } - - static persistPropertyByUserId(userId, propName, propValue, cb) { - userDb.run( - `REPLACE INTO user_property (user_id, prop_name, prop_value) - VALUES (?, ?, ?);`, - [userId, propName, propValue], - err => { - if (cb) { - return cb(err, propValue); - } - } - ); - } - - setProperty(propName, propValue) { - this.properties[propName] = propValue; - } - - incrementProperty(propName, incrementBy) { - incrementBy = incrementBy || 1; - let newValue = parseInt(this.getProperty(propName)); - if (newValue) { - newValue += incrementBy; - } else { - newValue = incrementBy; - } - this.setProperty(propName, newValue); - return newValue; - } - - getProperty(propName) { - return this.properties[propName]; - } - - getPropertyAsNumber(propName) { - return parseInt(this.getProperty(propName), 10); - } - - persistProperty(propName, propValue, cb) { - // update live props - this.properties[propName] = propValue; - - return User.persistPropertyByUserId(this.userId, propName, propValue, cb); - } - - removeProperty(propName, cb) { - // update live - delete this.properties[propName]; - - userDb.run( - `DELETE FROM user_property - WHERE user_id = ? AND prop_name = ?;`, - [this.userId, propName], - err => { - if (cb) { - return cb(err); - } - } - ); - } - - removeProperties(propNames, cb) { - async.each( - propNames, - (name, next) => { - return this.removeProperty(name, next); - }, - err => { - if (cb) { - return cb(err); - } - } - ); - } - - generateMainKeyPair(cb) { - crypto.generateKeyPair('rsa', { - modulusLength: 4096, - publicKeyEncoding: { - type: 'spki', - format: 'pem' - }, - privateKeyEncoding: { - type: 'pkcs8', - format: 'pem' - } - }, (err, publicKey, privateKey) => { - if (!err) { - this.setProperty(UserProps.PrivateKeyMain, privateKey); - this.setProperty(UserProps.PublicKeyMain, publicKey); - } - return cb(err); - }); - } - - persistProperties(properties, transOrDb, cb) { - if (!_.isFunction(cb) && _.isFunction(transOrDb)) { - cb = transOrDb; - transOrDb = userDb; - } - - const self = this; - - // update live props - _.merge(this.properties, properties); - - const stmt = transOrDb.prepare( - `REPLACE INTO user_property (user_id, prop_name, prop_value) - VALUES (?, ?, ?);` - ); - - async.each( - Object.keys(properties), - (propName, nextProp) => { - stmt.run(self.userId, propName, properties[propName], err => { - return nextProp(err); }); - }, - err => { - if (err) { - return cb(err); + } + + getAge() { + const birthdate = this.getProperty(UserProps.Birthdate); + if (birthdate) { + return moment().diff(birthdate, 'years'); + } + } + + static getUser(userId, cb) { + async.waterfall( + [ + function fetchUserId(callback) { + User.getUserName(userId, (err, userName) => { + return callback(null, userName); + }); + }, + function initProps(userName, callback) { + User.loadProperties(userId, (err, properties) => { + return callback(err, userName, properties); + }); + }, + function initGroups(userName, properties, callback) { + userGroup.getGroupsForUser(userId, (err, groups) => { + return callback(null, userName, properties, groups); + }); + }, + ], + (err, userName, properties, groups) => { + const user = new User(); + user.userId = userId; + user.username = userName; + user.properties = properties; + user.groups = groups; + + // explicitly NOT an authenticated user! + user.authenticated = false; + user.authFactor = User.AuthFactors.None; + + return cb(err, user); + } + ); + } + + static getUserInfo(userId, propsList, cb) { + if (!cb && _.isFunction(propsList)) { + cb = propsList; + propsList = [ + UserProps.RealName, + UserProps.Sex, + UserProps.EmailAddress, + UserProps.Location, + UserProps.Affiliations, + ]; } - stmt.finalize(() => { - return cb(null); + async.waterfall( + [ + callback => { + return User.getUserName(userId, callback); + }, + (userName, callback) => { + User.loadProperties(userId, { names: propsList }, (err, props) => { + return callback( + err, + Object.assign({}, props, { user_name: userName }) + ); + }); + }, + ], + (err, userProps) => { + if (err) { + return cb(err); + } + + const userInfo = {}; + Object.keys(userProps).forEach(key => { + userInfo[_.camelCase(key)] = userProps[key] || 'N/A'; + }); + + return cb(null, userInfo); + } + ); + } + + static isRootUserId(userId) { + return User.RootUserID === userId; + } + + static getUserIdAndName(username, cb) { + userDb.get( + `SELECT id, user_name + FROM user + WHERE user_name LIKE ?;`, + [username], + (err, row) => { + if (err) { + return cb(err); + } + + if (row) { + return cb(null, row.id, row.user_name); + } + + return cb(Errors.DoesNotExist('No matching username')); + } + ); + } + + static getUserIdAndNameByRealName(realName, cb) { + userDb.get( + `SELECT id, user_name + FROM user + WHERE id = ( + SELECT user_id + FROM user_property + WHERE prop_name='${UserProps.RealName}' AND prop_value LIKE ? + );`, + [realName], + (err, row) => { + if (err) { + return cb(err); + } + + if (row) { + return cb(null, row.id, row.user_name); + } + + return cb(Errors.DoesNotExist('No matching real name')); + } + ); + } + + static getUserIdAndNameByLookup(lookup, cb) { + User.getUserIdAndName(lookup, (err, userId, userName) => { + if (err) { + User.getUserIdAndNameByRealName(lookup, (err, userId, userName) => { + return cb(err, userId, userName); + }); + } else { + return cb(null, userId, userName); + } }); - } - ); - } - - setNewAuthCredentials(password, cb) { - User.generatePasswordDerivedKeyAndSalt(password, (err, info) => { - if (err) { - return cb(err); - } - - const newProperties = { - [UserProps.PassPbkdf2Salt]: info.salt, - [UserProps.PassPbkdf2Dk]: info.dk, - }; - - this.persistProperties(newProperties, err => { - return cb(err); - }); - }); - } - - getAge() { - const birthdate = this.getProperty(UserProps.Birthdate); - if (birthdate) { - return moment().diff(birthdate, 'years'); - } - } - - static getUser(userId, cb) { - async.waterfall( - [ - function fetchUserId(callback) { - User.getUserName(userId, (err, userName) => { - return callback(null, userName); - }); - }, - function initProps(userName, callback) { - User.loadProperties(userId, (err, properties) => { - return callback(err, userName, properties); - }); - }, - function initGroups(userName, properties, callback) { - userGroup.getGroupsForUser(userId, (err, groups) => { - return callback(null, userName, properties, groups); - }); - }, - ], - (err, userName, properties, groups) => { - const user = new User(); - user.userId = userId; - user.username = userName; - user.properties = properties; - user.groups = groups; - - // explicitly NOT an authenticated user! - user.authenticated = false; - user.authFactor = User.AuthFactors.None; - - return cb(err, user); - } - ); - } - - static getUserInfo(userId, propsList, cb) { - if (!cb && _.isFunction(propsList)) { - cb = propsList; - propsList = [ - UserProps.RealName, - UserProps.Sex, - UserProps.EmailAddress, - UserProps.Location, - UserProps.Affiliations, - ]; } - async.waterfall( - [ - callback => { - return User.getUserName(userId, callback); - }, - (userName, callback) => { - User.loadProperties(userId, { names: propsList }, (err, props) => { - return callback( - err, - Object.assign({}, props, { user_name: userName }) + static getUserName(userId, cb) { + userDb.get( + `SELECT user_name + FROM user + WHERE id = ?;`, + [userId], + (err, row) => { + if (err) { + return cb(err); + } + + if (row) { + return cb(null, row.user_name); + } + + return cb(Errors.DoesNotExist('No matching user ID')); + } + ); + } + + static loadProperties(userId, options, cb) { + if (!cb && _.isFunction(options)) { + cb = options; + options = {}; + } + + let sql = `SELECT prop_name, prop_value + FROM user_property + WHERE user_id = ?`; + + if (options.names) { + sql += ` AND prop_name IN("${options.names.join('","')}");`; + } else { + sql += ';'; + } + + let properties = {}; + userDb.each( + sql, + [userId], + (err, row) => { + if (err) { + return cb(err); + } + properties[row.prop_name] = row.prop_value; + }, + err => { + return cb(err, err ? null : properties); + } + ); + } + + // :TODO: make this much more flexible - propValue should allow for case-insensitive compare, etc. + static getUserIdsWithProperty(propName, propValue, cb) { + let userIds = []; + + userDb.each( + `SELECT user_id + FROM user_property + WHERE prop_name = ? AND prop_value = ?;`, + [propName, propValue], + (err, row) => { + if (row) { + userIds.push(row.user_id); + } + }, + () => { + return cb(null, userIds); + } + ); + } + + static getUserCount(cb) { + userDb.get( + `SELECT count() AS user_count + FROM user;`, + (err, row) => { + if (err) { + return cb(err); + } + return cb(null, row.user_count); + } + ); + } + + static getUserList(options, cb) { + const userList = []; + + options.properties = options.properties || [UserProps.RealName]; + + const asList = []; + const joinList = []; + for (let i = 0; i < options.properties.length; ++i) { + const dbProp = options.properties[i]; + const propName = options.propsCamelCase ? _.camelCase(dbProp) : dbProp; + asList.push(`p${i}.prop_value AS ${propName}`); + joinList.push( + `LEFT OUTER JOIN user_property p${i} ON p${i}.user_id = u.id AND p${i}.prop_name = '${dbProp}'` ); - }); - }, - ], - (err, userProps) => { - if (err) { - return cb(err); } - const userInfo = {}; - Object.keys(userProps).forEach(key => { - userInfo[_.camelCase(key)] = userProps[key] || 'N/A'; + userDb.each( + `SELECT u.id as userId, u.user_name as userName, ${asList.join(', ')} + FROM user u ${joinList.join(' ')} + ORDER BY u.user_name;`, + (err, row) => { + if (err) { + return cb(err); + } + userList.push(row); + }, + err => { + return cb(err, userList); + } + ); + } + + static generatePasswordDerivedKeyAndSalt(password, cb) { + async.waterfall( + [ + function getSalt(callback) { + User.generatePasswordDerivedKeySalt((err, salt) => { + return callback(err, salt); + }); + }, + function getDk(salt, callback) { + User.generatePasswordDerivedKey(password, salt, (err, dk) => { + return callback(err, salt, dk); + }); + }, + ], + (err, salt, dk) => { + return cb(err, { salt: salt, dk: dk }); + } + ); + } + + static generatePasswordDerivedKeySalt(cb) { + crypto.randomBytes(User.PBKDF2.saltLen, (err, salt) => { + if (err) { + return cb(err); + } + return cb(null, salt.toString('hex')); }); - - return cb(null, userInfo); - } - ); - } - - static isRootUserId(userId) { - return User.RootUserID === userId; - } - - static getUserIdAndName(username, cb) { - userDb.get( - `SELECT id, user_name - FROM user - WHERE user_name LIKE ?;`, - [username], - (err, row) => { - if (err) { - return cb(err); - } - - if (row) { - return cb(null, row.id, row.user_name); - } - - return cb(Errors.DoesNotExist('No matching username')); - } - ); - } - - static getUserIdAndNameByRealName(realName, cb) { - userDb.get( - `SELECT id, user_name - FROM user - WHERE id = ( - SELECT user_id - FROM user_property - WHERE prop_name='${UserProps.RealName}' AND prop_value LIKE ? - );`, - [realName], - (err, row) => { - if (err) { - return cb(err); - } - - if (row) { - return cb(null, row.id, row.user_name); - } - - return cb(Errors.DoesNotExist('No matching real name')); - } - ); - } - - static getUserIdAndNameByLookup(lookup, cb) { - User.getUserIdAndName(lookup, (err, userId, userName) => { - if (err) { - User.getUserIdAndNameByRealName(lookup, (err, userId, userName) => { - return cb(err, userId, userName); - }); - } else { - return cb(null, userId, userName); - } - }); - } - - static getUserName(userId, cb) { - userDb.get( - `SELECT user_name - FROM user - WHERE id = ?;`, - [userId], - (err, row) => { - if (err) { - return cb(err); - } - - if (row) { - return cb(null, row.user_name); - } - - return cb(Errors.DoesNotExist('No matching user ID')); - } - ); - } - - static loadProperties(userId, options, cb) { - if (!cb && _.isFunction(options)) { - cb = options; - options = {}; } - let sql = `SELECT prop_name, prop_value - FROM user_property - WHERE user_id = ?`; + static generatePasswordDerivedKey(password, salt, cb) { + password = Buffer.from(password).toString('hex'); - if (options.names) { - sql += ` AND prop_name IN("${options.names.join('","')}");`; - } else { - sql += ';'; + crypto.pbkdf2( + password, + salt, + User.PBKDF2.iterations, + User.PBKDF2.keyLen, + 'sha1', + (err, dk) => { + if (err) { + return cb(err); + } + + return cb(null, dk.toString('hex')); + } + ); } - - let properties = {}; - userDb.each( - sql, - [userId], - (err, row) => { - if (err) { - return cb(err); - } - properties[row.prop_name] = row.prop_value; - }, - err => { - return cb(err, err ? null : properties); - } - ); - } - - // :TODO: make this much more flexible - propValue should allow for case-insensitive compare, etc. - static getUserIdsWithProperty(propName, propValue, cb) { - let userIds = []; - - userDb.each( - `SELECT user_id - FROM user_property - WHERE prop_name = ? AND prop_value = ?;`, - [propName, propValue], - (err, row) => { - if (row) { - userIds.push(row.user_id); - } - }, - () => { - return cb(null, userIds); - } - ); - } - - static getUserCount(cb) { - userDb.get( - `SELECT count() AS user_count - FROM user;`, - (err, row) => { - if (err) { - return cb(err); - } - return cb(null, row.user_count); - } - ); - } - - static getUserList(options, cb) { - const userList = []; - - options.properties = options.properties || [UserProps.RealName]; - - const asList = []; - const joinList = []; - for (let i = 0; i < options.properties.length; ++i) { - const dbProp = options.properties[i]; - const propName = options.propsCamelCase ? _.camelCase(dbProp) : dbProp; - asList.push(`p${i}.prop_value AS ${propName}`); - joinList.push( - `LEFT OUTER JOIN user_property p${i} ON p${i}.user_id = u.id AND p${i}.prop_name = '${dbProp}'` - ); - } - - userDb.each( - `SELECT u.id as userId, u.user_name as userName, ${asList.join(', ')} - FROM user u ${joinList.join(' ')} - ORDER BY u.user_name;`, - (err, row) => { - if (err) { - return cb(err); - } - userList.push(row); - }, - err => { - return cb(err, userList); - } - ); - } - - static generatePasswordDerivedKeyAndSalt(password, cb) { - async.waterfall( - [ - function getSalt(callback) { - User.generatePasswordDerivedKeySalt((err, salt) => { - return callback(err, salt); - }); - }, - function getDk(salt, callback) { - User.generatePasswordDerivedKey(password, salt, (err, dk) => { - return callback(err, salt, dk); - }); - }, - ], - (err, salt, dk) => { - return cb(err, { salt: salt, dk: dk }); - } - ); - } - - static generatePasswordDerivedKeySalt(cb) { - crypto.randomBytes(User.PBKDF2.saltLen, (err, salt) => { - if (err) { - return cb(err); - } - return cb(null, salt.toString('hex')); - }); - } - - static generatePasswordDerivedKey(password, salt, cb) { - password = Buffer.from(password).toString('hex'); - - crypto.pbkdf2( - password, - salt, - User.PBKDF2.iterations, - User.PBKDF2.keyLen, - 'sha1', - (err, dk) => { - if (err) { - return cb(err); - } - - return cb(null, dk.toString('hex')); - } - ); - } };