SolidScribe/server/models/User.js

629 lines
17 KiB
JavaScript
Raw Normal View History

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.6.2'
//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) => {
return new Promise((resolve, reject) => {
2022-02-24 18:33:49 -08:00
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) 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
2020-03-13 16:34:32 -07:00
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
2022-02-24 18:33:49 -08:00
//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
2022-02-24 18:33:49 -08:00
`, [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
})
2022-02-24 18:33:49 -08:00
//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
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) => {
//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)
}
User.keyPairTest = (testUserName = 'genMan', password = '1', printResults) => {
return new Promise((resolve, reject) => {
let masterKey = null
let testUserId = null
const randomUsername = Math.random().toString(36).substring(2, 15);
const randomPassword = '1'
const secondPassword = '2'
User.register(testUserName, password)
.then( ({ token, userId }) => {
testUserId = userId
if(printResults) console.log('Test: Register User '+testUserName+' - Pass')
return User.getMasterKey(testUserId, password)
})
.then(newMasterKey => {
masterKey = newMasterKey
if(printResults) console.log('Test: Generate/Decrypt Master Key - Pass')
return User.generateKeypair(testUserId, masterKey)
})
.then(({publicKey, privateKey}) => {
const publicKeyMessage = 'Test: Public key decrypt - Pass'
const privateKeyMessage = 'Test: Private key decrypt - Pass'
//Encrypt Message with private Key
const privateKeyEncrypted = crypto.privateEncrypt(privateKey, Buffer.from(privateKeyMessage, 'utf8')).toString('base64')
const decryptedPrivate = crypto.publicDecrypt(publicKey, Buffer.from(privateKeyEncrypted, 'base64'))
//Conver back to a string
if(printResults) console.log(decryptedPrivate.toString('utf8'))
//Encrypt with public key
const pubEncrMsc = crypto.publicEncrypt(publicKey, Buffer.from(publicKeyMessage, 'utf8')).toString('base64')
const publicDeccryptMessage = crypto.privateDecrypt(privateKey, Buffer.from(pubEncrMsc, 'base64') )
//Convert it back to string
if(printResults) console.log(publicDeccryptMessage.toString('utf8'))
return User.login(testUserName, password)
})
.then( ({token, userId}) => {
if(printResults) console.log('Test: Login New User - Pass')
return User.changePassword(testUserId, randomPassword, secondPassword)
})
.then(passwordChangeResults => {
if(printResults) console.log('Test: Password Change - ', passwordChangeResults?'Pass':'Fail')
return User.login(testUserName, secondPassword)
})
.then(reLogin => {
if(printResults) console.log('Test: Login With new Password - Pass')
return User.getMasterKey(testUserId, secondPassword)
})
.then(newMasterKey => {
masterKey = newMasterKey
resolve({testUserId, masterKey})
})
})
}