const crypto = require('crypto') const Note = require('@models/Note') const db = require('@config/database') const Auth = require('@helpers/Auth') const cs = require('@helpers/CryptoString') const speakeasy = require('speakeasy') let User = module.exports = {} const version = '3.8.0' // 3.7.3 - diff/patch update //Login a user, if that user does not exist create them //Issues login token User.login = (username, password, authToken = null) => { return new Promise((resolve, reject) => { const lowerName = username.toLowerCase() let statusObject = { success: false, token: null, userId: null, verificationRequired: false, message: 'Incorrect Username or Password' } db.promise() .query('SELECT * FROM user WHERE username = ? LIMIT 1', [lowerName]) .then((rows, fields) => { // // Login User // if(rows[0].length == 1){ //Pull out user data from database results const lookedUpUser = rows[0][0] //Verify Token if set const tokenValidates = speakeasy.totp.verify({ 'secret': lookedUpUser['two_fa_secret'], 'encoding': 'base32', 'token': authToken, 'window': 2 }) if(lookedUpUser.two_fa_enabled == 1 && !authToken){ statusObject['verificationRequired'] = true statusObject['message'] = '2FA authentication required.' return resolve(statusObject) } if(lookedUpUser.two_fa_enabled == 1 && !tokenValidates){ statusObject['verificationRequired'] = true statusObject['message'] = 'Invalid Authorization Token.' return resolve(statusObject) } if(lookedUpUser.two_fa_enabled == 0 || (lookedUpUser.two_fa_enabled == 1 && tokenValidates) ){ //hash the password and check for a match const salt = Buffer.from(lookedUpUser.salt, 'binary') crypto.pbkdf2(password, salt, lookedUpUser.iterations, 512, 'sha512', function(err, delivered_key){ if(delivered_key.toString('hex') === lookedUpUser.password){ User.generateMasterKey(lookedUpUser.id, password) .then( result => User.getMasterKey(lookedUpUser.id, password)) .then(masterKey => { User.generateKeypair(lookedUpUser.id, masterKey) .then(({publicKey, privateKey}) => { //Passback a json web token Auth.createToken(lookedUpUser.id, masterKey) .then(token => { statusObject['token'] = token statusObject['userId'] = lookedUpUser.id statusObject['success'] = true return resolve(statusObject) }) }) }) } else { return resolve(statusObject) } }) } } else { //If user is not found, say two factor authentication is required statusObject['verificationRequired'] = true statusObject['message'] = '2FA authentication required.' //Show fake auth token message if(authToken){ statusObject['message'] = 'Invalid Authorization Token.' } return resolve(statusObject) } }) .catch(console.log) }) } //Create user account //Issues login token User.register = (username, password) => { //For some reason, username won't get into the promise. But password will @TODO figure this out const lowerName = username.toLowerCase().trim() return new Promise((resolve, reject) => { db.promise() .query('SELECT * FROM user WHERE username = ? LIMIT 1', [lowerName]) .then((rows, fields) => { if(rows[0].length === 0){ //No users returned, create new one. Start with hashing password //Params for hash function let shasum = crypto.createHash('sha512') //Prepare Hash const ran = parseInt(Date.now()) //Get current time in miliseconds const semiRandomInt = Math.floor(Math.random()*11) //Grab a random number const otherRandomInt = (ran*semiRandomInt+ran)*semiRandomInt-ran //Mix things up a bit shasum.update(''+otherRandomInt) //Update Hasd const saltString = shasum.digest('hex') const salt = Buffer.from(saltString, 'binary') //Generate Salt hash const iterations = 25000 crypto.pbkdf2(password, salt, iterations, 512, 'sha512', function(err, delivered_key) { //Create new user object with freshly salted password var currentDate = new Date().toISOString().slice(0, 19).replace('T', ' '); var new_user = { username: lowerName, password: delivered_key.toString('hex'), salt: salt, iterations: iterations, last_login: currentDate, created: currentDate }; let userId = null let newMasterKey = null db.promise() .query('INSERT INTO user SET ?', new_user) .then((rows, fields) => { userId = rows[0].insertId return User.generateMasterKey(userId, password) }) .then( result => { return User.getMasterKey(userId, password) }) .then(masterKey => { newMasterKey = masterKey return User.generateKeypair(userId, newMasterKey) }) .then(({publicKey, privateKey}) => { return Auth.createToken(userId, newMasterKey) }) .then(token => { return resolve({token, userId}) }) .catch(console.log) }) } else { return reject('Username already in use.') }//END user create }) .catch(console.log) }) } //Counts notes, pinned notes, archived notes, shared notes, unread notes, total files and types User.getCounts = (userId, extendedOptions) => { return new Promise((resolve, reject) => { let countTotals = { tags: {} } // const userHash = cs.hash(String(userId)).toString('base64') db.promise().query( `SELECT SUM(archived = 1 && share_user_id IS NULL && trashed = 0) AS archivedNotes, SUM(trashed = 1) AS trashedNotes, SUM(share_user_id IS NULL && trashed = 0 AND quick_note < 2) AS totalNotes, SUM(share_user_id IS NOT null && opened IS null && trashed = 0) AS youGotMailCount, SUM(share_user_id != ? && trashed = 0) AS sharedToNotes FROM note LEFT JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id) WHERE user_id = ?`, [userId, userId, userId, userId]) .then( (rows, fields) => { Object.assign(countTotals, rows[0][0]) //combine results // // @TODO - Figured out if this is useful // We want, notes shared with user and note user has shared // return db.promise().query( `SELECT count(id) AS sharedFromNotes FROM note WHERE shared = 2 AND user_id = ? AND trashed = 0`, [userId] ) }) .then( (rows, fields) => { Object.assign(countTotals, rows[0][0]) //combine results return db.promise().query( `SELECT SUM(attachment_type = 1) as linkFiles, SUM(attachment_type != 1) as otherFiles, COUNT(id) as totalFiles FROM attachment WHERE visible = 1 AND user_id = ? `, [userId] ) }).then( (rows, fields) => { Object.assign(countTotals, rows[0][0]) //combine results return db.promise().query('SELECT id AS quickNote FROM note WHERE quick_note = 1 AND user_id = ?', [userId]) }).then( (rows, fields) => { Object.assign(countTotals, rows[0][0]) //combine results //Count usages of user tags, sort by most popular return db.promise().query(` SELECT tag.text, COUNT(tag_id) AS uses, tag.id FROM note_tag JOIN tag ON (tag.id = note_tag.tag_id) WHERE user_id = ? GROUP BY tag_id ORDER BY uses DESC LIMIT 16 `, [userId]) }).then( (rows, fields) => { //Convert everything to an int or 0 Object.keys(countTotals).forEach( key => { const count = parseInt(countTotals[key]) countTotals[key] = count ? count : 0 }) //Build out tags object let tagsObject = {} rows[0].forEach(tagRow => { tagsObject[tagRow['text']] = {'id':tagRow.id, 'uses':tagRow.uses} }) //Assign after counts are updated countTotals['tags'] = tagsObject countTotals['currentVersion'] = version // Allow for extended options set on page load if(extendedOptions){ db.promise().query( `SELECT updated FROM note JOIN note_raw_text ON note_raw_text.id = note.note_raw_text_id WHERE note.quick_note = 2 AND user_id = ?`, [userId]) .then( (rows, fields) => { if(rows[0][0] && rows[0][0].updated){ const lastOpened = rows[0][0].updated const timeDiff = Math.round(((+new Date) - (lastOpened))/1000) const hoursInSeconds = (12 * 60 * 60) //12 hours // Show metric tracking button if its been 12 hours since last entry if(lastOpened && timeDiff > hoursInSeconds){ countTotals['showTrackMetricsButton'] = true } } resolve(countTotals) }) } else { resolve(countTotals) } }) }) } //Log out user by deleting login token for that active session User.logout = (sessionId) => { console.log('Terminate Session -> ', sessionId) return db.promise().query('DELETE FROM user_active_session WHERE (session_id = ?)', [sessionId]) } User.generateMasterKey = (userId, password) => { return new Promise((resolve, reject) => { if(!userId || !password){ reject('Need userId and password to generate key') } db.promise() .query('SELECT count(id) as total FROM user_key WHERE user_id = ?', [userId]) .then((rows, fields) => { //Entry already exists, you good. if(rows[0][0]['total'] > 0){ return resolve(true) // throw new Error('User Encryption key already exists') } else { // Generate user key, its big and random const masterPassword = cs.createSmallSalt() //Generate a salt because it wants it const salt = cs.createSmallSalt() // Encrypt master password const encryptedMasterPassword = cs.encrypt(password, salt, masterPassword) const created = Math.round((+new Date)/1000) db.promise() .query( 'INSERT INTO user_key (`user_id`, `salt`, `key`, `created`) VALUES (?, ?, ?, ?);', [userId, salt, encryptedMasterPassword, created] ) .then(results => { return resolve(true) }) } }) .catch(error => { console.log('Create Master Password Error') console.log(error) }) }) } User.getMasterKey = (userId, password) => { return new Promise((resolve, reject) => { if(!userId || !password){ reject('Need userId and password to fetch key') } db.promise().query('SELECT * FROM user_key WHERE user_id = ? LIMIT 1', [userId]) .then((rows, fields) => { const row = rows[0][0] if(!rows[0] || rows[0].length == 0 || rows[0][0] == undefined){ return reject('Row or salt or something not set') } const masterKey = cs.decrypt(password, row['salt'], row['key']) if(masterKey == null){ return reject('Unable to decrypt key') } return resolve(masterKey) }) }) } User.generateKeypair = (userId, masterKey) => { let publicKey = null let privateKey = null return new Promise((resolve, reject) => { db.promise().query('SELECT * FROM user_key WHERE user_id = ?', [userId]) .then((rows, fields) => { const row = rows[0][0] const salt = row['salt'] publicKey = row['public_key'] privateKey = row['private_key_encrypted'] if(row['public_key'] == null){ const keyPair = crypto.generateKeyPairSync('rsa', { modulusLength: 1024, publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem' } }) publicKey = keyPair.publicKey privateKey = keyPair.privateKey const privateKeyEncrypted = cs.encrypt(masterKey, salt, privateKey) db.promise() .query( 'UPDATE user_key SET `public_key` = ?, `private_key_encrypted` = ? WHERE user_id = ?;', [publicKey, privateKeyEncrypted, userId] ) .then((rows, fields)=>{ return resolve({publicKey, privateKey}) }) } else { //Decrypt private key privateKey = cs.decrypt(masterKey, salt, privateKey) return resolve({publicKey, privateKey}) } }) }) } User.getPublicKey = (userId) => { return new Promise((resolve, reject) => { db.promise().query('SELECT public_key FROM user_key WHERE user_id = ?', [userId]) .then((rows, fields) => { const row = rows[0][0] return resolve(row['public_key']) }) }) } User.getPrivateKey = (userId, masterKey) => { return new Promise((resolve, reject) => { db.promise().query('SELECT salt, private_key_encrypted FROM user_key WHERE user_id = ?', [userId]) .then((rows, fields) => { const row = rows[0][0] const salt = row['salt'] privateKey = row['private_key_encrypted'] //Decrypt private key privateKey = cs.decrypt(masterKey, salt, privateKey) return resolve(privateKey) }) }) } User.getByUserName = (username) => { return new Promise((resolve, reject) => { db.promise().query('SELECT * FROM user WHERE username = ? LIMIT 1', [username.toLowerCase()]) .then((rows, fields) => { resolve(rows[0][0]) }) }) } User.changePassword = (userId, oldPass, newPass) => { return new Promise((resolve, reject) => { User.getMasterKey(userId, oldPass) .then(masterKey => { User.getPrivateKey(userId, masterKey) .then(privateKey => { //If success, user has correct password // Generate new master pass, encrypt with new password // const masterPassword = cs.createSmallSalt() const salt = cs.createSmallSalt() const encryptedMasterPassword = cs.encrypt(newPass, salt, masterKey) const encryptedPrivateKey = cs.encrypt(masterKey, salt, privateKey) db.promise() .query( 'UPDATE user_key SET salt = ?, `key` = ?, private_key_encrypted = ? WHERE user_id = ? LIMIT 1', [salt, encryptedMasterPassword, encryptedPrivateKey, userId] ).then((r,f) => { //Create login using password let shasum = crypto.createHash('sha512') //Prepare Hash const saltString = shasum.digest('hex') const passwordSalt = Buffer.from(saltString, 'binary') //Generate Salt hash const iterations = 25000 crypto.pbkdf2(newPass, passwordSalt, iterations, 512, 'sha512', function(err, delivered_key) { const deliveredPass = delivered_key.toString('hex') db.promise().query('UPDATE user SET password = ?, salt = ? WHERE id = ? LIMIT 1', [deliveredPass, passwordSalt, userId]) .then((r,f) => { return resolve(true) }) }) }) }) }) .catch(error => { resolve(false) }) }) } User.revokeActiveSessions = (userId, sessionId) => { return new Promise((resolve, reject) => { const userHash = cs.hash(String(userId)).toString('base64') db.promise().query('DELETE FROM user_active_session WHERE user_hash = ? AND session_id != ?', [userHash, sessionId]) .then((r,f) => { resolve(true) }) }) } User.deleteUser = (userId, password) => { if(!userId || !password){ return new Promise((resolve, reject) => { return resolve('Missing User ID or Password. No Action Taken.') }) } //Verify user is correct by decryptig master key with password let deletePromises = [] //Delete all notes and raw text let noteDelete = db.promise().query(` DELETE note, note_raw_text FROM note JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id) WHERE note.user_id = ? `,[userId]) deletePromises.push(noteDelete) //Delete user entry let userDelete = db.promise().query(` DELETE FROM user WHERE id = ? `,[userId]) deletePromises.push(userDelete) //Delete user_key, encrypted search index let tables = ['user_key', 'user_encrypted_search_index'] tables.forEach(tableName => { const query = `DELETE FROM ${tableName} WHERE user_id = ?` const deleteQuery = db.promise().query(query, [userId]) deletePromises.push(deleteQuery) }) //Remove all note attachments and files return Promise.all(deletePromises) }