7f5f4bea39
Removed visible attribute that was left over from testing Removed drag attribute on check boxes, needs better implimentation later. Drag prevented click events
629 lines
17 KiB
JavaScript
629 lines
17 KiB
JavaScript
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.4.3'
|
|
|
|
//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) => {
|
|
|
|
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
|
|
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 5
|
|
`, [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
|
|
|
|
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})
|
|
})
|
|
})
|
|
} |