* Added Much better session Management, key updating and deleting
* Force reload of JS if app numbers dont match * Added cool tag display on side of note * Cleaned up a bunch of code and tweaked little things to be better
This commit is contained in:
@@ -6,26 +6,33 @@ let Auth = {}
|
||||
|
||||
const tokenSecretKey = process.env.JSON_KEY
|
||||
|
||||
Auth.createToken = (userId, masterKey, request = null) => {
|
||||
Auth.createToken = (userId, masterKey, pastId = null, pastCreatedDate = null) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
const created = Math.floor(+new Date/1000)
|
||||
const created = pastCreatedDate ? pastCreatedDate : Math.floor(+new Date/1000)
|
||||
const userHash = cs.hash(String(userId)).toString('base64')
|
||||
|
||||
//Encrypt Master Password and save it to the server
|
||||
const sessionId = pastId ? pastId : cs.createSmallSalt().slice(0,9) //Use existing session id
|
||||
const salt = cs.createSmallSalt()
|
||||
const tempPass = cs.createSmallSalt()
|
||||
const encryptedMasterPass = cs.encrypt(tempPass, salt, masterKey)
|
||||
|
||||
|
||||
db.promise().query(
|
||||
|
||||
'INSERT INTO user_active_session (salt, encrypted_master_password, created, uses, user_hash) VALUES (?,?,?,?,?)',
|
||||
[salt, encryptedMasterPass, created, 1, userHash])
|
||||
//Deactivate all other session keys, they delete after 30 seconds
|
||||
db.promise().query('UPDATE user_active_session SET active = 0 WHERE session_id = ?', [sessionId])
|
||||
.then((r,f) => {
|
||||
|
||||
return db.promise().query(
|
||||
'INSERT INTO user_active_session (salt, encrypted_master_password, created, uses, user_hash, session_id) VALUES (?,?,?,?,?,?)',
|
||||
[salt, encryptedMasterPass, created, 40, userHash, sessionId])
|
||||
|
||||
})
|
||||
.then((r,f) => {
|
||||
|
||||
const sessionNum = r[0].insertId
|
||||
|
||||
//Required Data for JWT payload
|
||||
const tokenPayload = {userId, tempPass, salt}
|
||||
const tokenPayload = {userId, tempPass, sessionNum}
|
||||
|
||||
//Return token
|
||||
const token = jwt.sign(tokenPayload, tokenSecretKey)
|
||||
@@ -33,50 +40,85 @@ Auth.createToken = (userId, masterKey, request = null) => {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Auth.decodeToken = (token, request = null) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
let decodedToken = null
|
||||
|
||||
//Delete all tokens older than 20 days before continuing or inacive and older than 1 minute
|
||||
const now = (Math.floor((+new Date)/1000))
|
||||
const twentyDays = (Math.floor((+new Date)/1000)) - (86400 * 20)
|
||||
const thirtySeconds = (Math.floor((+new Date)/1000)) - (30)
|
||||
|
||||
//Decode Json web token
|
||||
jwt.verify(token, tokenSecretKey, function(err, decoded){
|
||||
if(err || decoded.tempPass == undefined || decoded.tempPass.length < 5 || decoded.salt == undefined || decoded.salt.length < 5){
|
||||
return reject('Bad Token')
|
||||
}
|
||||
jwt.verify(token, tokenSecretKey, function(err, decoded){
|
||||
if(err || decoded.tempPass == undefined || decoded.tempPass.length < 5){
|
||||
throw new Error('Bad Token')
|
||||
}
|
||||
|
||||
decodedToken = decoded
|
||||
decodedToken = decoded
|
||||
|
||||
//Lookup session data in database
|
||||
return db.promise().query('SELECT * FROM user_active_session WHERE salt = ? LIMIT 1', [decodedToken.salt])
|
||||
})
|
||||
.then((r,f) => {
|
||||
db.promise().query('DELETE from user_active_session WHERE (created < ?) OR (active = false AND last_used < ?)', [twentyDays, thirtySeconds])
|
||||
.then((r,f) => {
|
||||
|
||||
//Lookup session data in database
|
||||
db.promise().query('SELECT * FROM user_active_session WHERE id = ? LIMIT 1', [decodedToken.sessionNum])
|
||||
.then((r,f) => {
|
||||
|
||||
const row = r[0][0]
|
||||
if(row == undefined || row.length == 0){
|
||||
return reject(false)
|
||||
}
|
||||
if(r == undefined || r[0].length == 0){
|
||||
throw new Error('Active Session not found for token')
|
||||
}
|
||||
|
||||
//Decrypt master key from database
|
||||
const masterKey = cs.decrypt(decodedToken.tempPass, decodedToken.salt, row.encrypted_master_password)
|
||||
if(masterKey == null){
|
||||
return reject (false)
|
||||
}
|
||||
const row = r[0][0]
|
||||
|
||||
const userData = {
|
||||
userId: decodedToken.userId, masterKey, tokenId: row.id
|
||||
}
|
||||
// console.log(decodedToken.sessionNum + ' uses -> ' + row.uses)
|
||||
|
||||
//Async update DB counts
|
||||
db.promise().query('UPDATE user_active_session SET uses = uses + 1 WHERE salt = ? LIMIT 1', [decodedToken.salt])
|
||||
if(row.uses <= 0){
|
||||
throw new Error('Token is used up')
|
||||
}
|
||||
|
||||
return resolve(userData)
|
||||
//Decrypt master key from lookup
|
||||
const masterKey = cs.decrypt(decodedToken.tempPass, row.salt, row.encrypted_master_password)
|
||||
if(masterKey == null){
|
||||
// console.log('Deleting invalid session')
|
||||
Auth.terminateSession(row.session_id)
|
||||
throw new Error ('Unable to decrypt password for session')
|
||||
}
|
||||
|
||||
//Async update DB counts and disable session if needed
|
||||
db.promise().query('UPDATE user_active_session SET uses = uses -1, last_used = ? WHERE id = ? LIMIT 1', [now, decodedToken.sessionNum])
|
||||
.then((r,f) => {
|
||||
|
||||
let userData = {
|
||||
'userId': decodedToken.userId,
|
||||
'masterKey': masterKey,
|
||||
'sessionId': row.session_id,
|
||||
'created': row.created,
|
||||
'remainingUses':(row.uses--),
|
||||
'active': row.active
|
||||
}
|
||||
|
||||
//Return token Data
|
||||
return resolve(userData)
|
||||
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
//Token errors result in having sessions deleted
|
||||
// console.log('-- Auth Token Error --')
|
||||
// console.log(error)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
Auth.reissueToken = () => {
|
||||
//If token has more than 200 uses, renew it
|
||||
|
||||
Auth.terminateSession = (sessionId) => {
|
||||
return db.promise().query('DELETE from user_active_session WHERE session_id = ?', [sessionId])
|
||||
}
|
||||
|
||||
Auth.deletAllLoginKeys = (userId) => {
|
||||
|
||||
const userHash = cs.hash(String(userId)).toString('base64')
|
||||
@@ -86,8 +128,6 @@ Auth.deletAllLoginKeys = (userId) => {
|
||||
|
||||
Auth.test = () => {
|
||||
|
||||
// return Auth.deletAllLoginKeys(testUserId)
|
||||
|
||||
const testUserId = 22
|
||||
const testPass = cs.createSmallSalt()
|
||||
Auth.createToken(testUserId, testPass)
|
||||
@@ -100,7 +140,6 @@ Auth.test = () => {
|
||||
.then(userData => {
|
||||
|
||||
console.log('Test: Decrypted key Match -> ' + (testPass == userData.masterKey))
|
||||
|
||||
return Auth.deletAllLoginKeys(testUserId)
|
||||
})
|
||||
.then(results => {
|
||||
@@ -108,16 +147,6 @@ Auth.test = () => {
|
||||
console.log('Test: Remove user Json Web Tokens - Pass')
|
||||
|
||||
})
|
||||
|
||||
//create token with userId and master key
|
||||
// Auth.createToken()
|
||||
|
||||
//Thirt days ago
|
||||
// const thirtyDays = (Math.floor((+new Date)/1000)) - (86400 * 30)
|
||||
// const created = Math.floor(decoded.date/1000)
|
||||
// if(created < thirtyDays){
|
||||
// return reject('Token Expired')
|
||||
// }
|
||||
}
|
||||
|
||||
module.exports = Auth
|
@@ -31,6 +31,9 @@ CryptoString.encrypt = (password, salt64, rawText) => {
|
||||
//Decrypt base64 string cipher text,
|
||||
CryptoString.decrypt = (password, salt64, cipherTextString) => {
|
||||
|
||||
if(!password || !salt64 || !cipherTextString){ return '' }
|
||||
if(password.length == 0 || salt64.length == 0 || cipherTextString == 0){ return '' }
|
||||
|
||||
let cipherText = Buffer.from(cipherTextString, 'base64')
|
||||
const salt = Buffer.from(salt64, 'base64')
|
||||
|
||||
|
@@ -69,7 +69,7 @@ ProcessText.deduceNoteTitle = (inTitle, inString) => {
|
||||
//Remove inline styles that may be added by editor
|
||||
// inString = inString.replace(/style=".*?"/g,'')
|
||||
|
||||
const tagFreeLength = ProcessText.removeHtml(inString).length
|
||||
// const tagFreeLength = ProcessText.removeHtml(inString).length
|
||||
|
||||
//
|
||||
// Simplified attempt!
|
||||
@@ -80,7 +80,7 @@ ProcessText.deduceNoteTitle = (inTitle, inString) => {
|
||||
// if(tagFreeLength > 200){
|
||||
// sub += '... <i class="green caret down icon"></i>'
|
||||
// }
|
||||
inString += '</end>'
|
||||
// inString += '</end>'
|
||||
|
||||
return {title, sub}
|
||||
|
||||
|
@@ -50,13 +50,37 @@ io.on('connection', function(socket){
|
||||
socket.on('user_connect', token => {
|
||||
Auth.decodeToken(token)
|
||||
.then(userData => {
|
||||
socket.join(userData.id)
|
||||
socket.join(userData.userId)
|
||||
}).catch(error => {
|
||||
//Don't add user to room if they are not logged in
|
||||
// console.log(error)
|
||||
})
|
||||
})
|
||||
|
||||
//Renew Session tokens when users request a new one
|
||||
socket.on('renew_session_token', token => {
|
||||
|
||||
//Decode the token they currently have
|
||||
Auth.decodeToken(token)
|
||||
.then(userData => {
|
||||
|
||||
console.log('Is active -> ', userData.active)
|
||||
|
||||
if(userData.active == 1){
|
||||
//Create a new one using credentials and session keys from current
|
||||
Auth.createToken(userData.userId, userData.masterKey, userData.sessionId, userData.created)
|
||||
.then(newToken => {
|
||||
|
||||
//Emit new token only to user on socket
|
||||
socket.emit('recievend_new_token', newToken)
|
||||
})
|
||||
} else {
|
||||
//Attempting to reactivate disabled session, kills it all
|
||||
Auth.terminateSession(userData.sessionId)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
socket.on('join_room', rawTextId => {
|
||||
// Join user to rawtextid room when they enter
|
||||
socket.join(rawTextId)
|
||||
@@ -78,11 +102,7 @@ io.on('connection', function(socket){
|
||||
//Update users in room count
|
||||
io.to(rawTextId).emit('update_user_count', usersInRoom.length)
|
||||
|
||||
//Debugging text
|
||||
console.log('Note diff object')
|
||||
console.log(noteDiffs)
|
||||
|
||||
|
||||
//Debugging text - prints out notes in limbo
|
||||
let noteDiffKeys = Object.keys(noteDiffs)
|
||||
let totalDiffs = 0
|
||||
noteDiffKeys.forEach(diffSetKey => {
|
||||
@@ -90,9 +110,11 @@ io.on('connection', function(socket){
|
||||
totalDiffs += noteDiffs[diffSetKey].length
|
||||
}
|
||||
})
|
||||
|
||||
console.log('Total notes in limbo -> ', noteDiffKeys.length)
|
||||
console.log('Total Diffs for all notes -> ', totalDiffs)
|
||||
//Debugging Text
|
||||
if(noteDiffKeys.length > 0){
|
||||
console.log('Total notes in limbo -> ', noteDiffKeys.length)
|
||||
console.log('Total Diffs for all notes -> ', totalDiffs)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
@@ -203,18 +225,27 @@ app.use(function(req, res, next){
|
||||
|
||||
//Always null out master key, never allow it set from outside
|
||||
req.headers.masterKey = null
|
||||
req.headers.tokenId = null
|
||||
req.headers.sessionId = null
|
||||
|
||||
//auth token set by axios in headers
|
||||
let token = req.headers.authorizationtoken
|
||||
if(token && token != null && typeof token === 'string'){
|
||||
if(token !== undefined && token.length > 0){
|
||||
Auth.decodeToken(token, req)
|
||||
.then(userData => {
|
||||
req.headers.userId = userData.userId //Update headers for the rest of the application
|
||||
|
||||
//Update headers for the rest of the application
|
||||
req.headers.userId = userData.userId
|
||||
req.headers.masterKey = userData.masterKey
|
||||
req.headers.tokenId = userData.tokenId
|
||||
req.headers.sessionId = userData.sessionId
|
||||
|
||||
//Tell front end remaining uses on current token
|
||||
res.set('remainingUses', userData.remainingUses)
|
||||
|
||||
next()
|
||||
}).catch(error => {
|
||||
})
|
||||
.catch(error => {
|
||||
|
||||
console.log(error)
|
||||
|
||||
res.statusMessage = error //Throw 400 error if token is bad
|
||||
res.status(400).end()
|
||||
@@ -231,7 +262,7 @@ let UserTest = require('@models/User')
|
||||
let NoteTest = require('@models/Note')
|
||||
let AuthTest = require('@helpers/Auth')
|
||||
Auth.test()
|
||||
UserTest.keyPairTest('genMan12', '1', printResults)
|
||||
UserTest.keyPairTest('genMan15', '1', printResults)
|
||||
.then( ({testUserId, masterKey}) => NoteTest.test(testUserId, masterKey, printResults))
|
||||
.then( message => {
|
||||
if(printResults) console.log(message)
|
||||
|
@@ -454,9 +454,12 @@ Note.update = (userId, noteId, noteText, noteTitle, color, pinned, archived, has
|
||||
}
|
||||
|
||||
let encryptedNoteText = ''
|
||||
//Create encrypted snippet
|
||||
const snippet = JSON.stringify([noteTitle, noteText.substring(0, 500)])
|
||||
noteSnippet = cs.encrypt(masterKey, snippetSalt, snippet)
|
||||
//Create encrypted snippet if its a long note
|
||||
let snippet = ''
|
||||
if(noteText.length > 500){
|
||||
snippet = JSON.stringify([noteTitle, noteText.substring(0, 500)])
|
||||
noteSnippet = cs.encrypt(masterKey, snippetSalt, snippet)
|
||||
}
|
||||
|
||||
//Encrypt note text
|
||||
const textObject = JSON.stringify([noteTitle, noteText])
|
||||
@@ -946,9 +949,11 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
|
||||
let searchParams = [userId]
|
||||
let noteSearchQuery = `
|
||||
SELECT note.id,
|
||||
note.snippet as snippet,
|
||||
note.snippet_salt as salt,
|
||||
note_raw_text.updated as updated,
|
||||
note.snippet as snippetText,
|
||||
note.snippet_salt as snippetSalt,
|
||||
note_raw_text.text as noteText,
|
||||
note_raw_text.salt as noteSalt,
|
||||
note_raw_text.updated as updated,
|
||||
opened,
|
||||
color,
|
||||
count(distinct note_tag.id) as tag_count,
|
||||
@@ -1092,26 +1097,39 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
|
||||
}
|
||||
|
||||
|
||||
//Decrypt note text
|
||||
if(note.snippet && note.salt){
|
||||
const decipheredText = cs.decrypt(currentNoteKey, note.salt, note.snippet)
|
||||
const textObject = JSON.parse(decipheredText)
|
||||
if(textObject != null && textObject.length == 2){
|
||||
note.title = textObject[0]
|
||||
note.text = textObject[1]
|
||||
}
|
||||
//Only long notes have snippets, decipher it if present
|
||||
let displayTitle = ''
|
||||
let displayText = ''
|
||||
|
||||
let encryptedText = note.noteText
|
||||
let relatedSalt = note.noteSalt
|
||||
|
||||
//Default to note text, use snippet if set
|
||||
if(note.snippetSalt && note.snippetText && note.snippetSalt.length > 0 && note.snippetText.length > 0){
|
||||
encryptedText = note.snippetText
|
||||
relatedSalt = note.snippetSalt
|
||||
}
|
||||
|
||||
//Deduce note title
|
||||
const textData = ProcessText.deduceNoteTitle(note.title, note.text)
|
||||
|
||||
note.title = textData.title
|
||||
note.subtext = textData.sub
|
||||
try {
|
||||
const decipheredText = cs.decrypt(currentNoteKey, relatedSalt, encryptedText)
|
||||
const textObject = JSON.parse(decipheredText)
|
||||
if(textObject != null && textObject.length == 2){
|
||||
if(textObject[0] && textObject[0] != null && textObject[0].length > 0){
|
||||
displayTitle = textObject[0]
|
||||
}
|
||||
if(textObject[1] && textObject[1] != null && textObject[1].length > 0){
|
||||
displayText = textObject[1]
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
console.log('Error opening note id -> ', note.id)
|
||||
console.log(err)
|
||||
}
|
||||
|
||||
//Remove these variables
|
||||
note.note_highlights = []
|
||||
note.attachment_highlights = []
|
||||
note.tag_highlights = []
|
||||
|
||||
|
||||
note.title = displayTitle
|
||||
note.subtext = ProcessText.stripDoubleBlankLines(displayText)
|
||||
|
||||
//Limit number of attachment thumbs to 4
|
||||
if(note.thumbs){
|
||||
@@ -1123,9 +1141,12 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
|
||||
}
|
||||
|
||||
//Clear out note.text before sending it to front end, its being used in title and subtext
|
||||
delete note.snippet
|
||||
delete note.salt
|
||||
delete note.snippetText
|
||||
delete note.snippetSalt
|
||||
delete note.noteText
|
||||
delete note.noteSalt
|
||||
delete note.encrypted_share_password_key
|
||||
delete note.text //Passed back as title and subtext
|
||||
})
|
||||
|
||||
|
||||
|
@@ -143,6 +143,7 @@ User.getCounts = (userId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
let countTotals = {}
|
||||
const userHash = cs.hash(String(userId)).toString('base64')
|
||||
|
||||
db.promise().query(
|
||||
`SELECT
|
||||
@@ -169,8 +170,6 @@ User.getCounts = (userId) => {
|
||||
.then( (rows, fields) => {
|
||||
|
||||
Object.assign(countTotals, rows[0][0]) //combine results
|
||||
|
||||
const userHash = cs.hash(String(userId)).toString('base64')
|
||||
|
||||
return db.promise().query(
|
||||
`SELECT count(id) as activeSessions FROM user_active_session WHERE user_hash = ?`, [userHash]
|
||||
@@ -199,6 +198,8 @@ User.getCounts = (userId) => {
|
||||
countTotals[key] = count ? count : 0
|
||||
})
|
||||
|
||||
countTotals['currentVersion'] = '3.0.0'
|
||||
|
||||
resolve(countTotals)
|
||||
})
|
||||
|
||||
@@ -206,8 +207,9 @@ User.getCounts = (userId) => {
|
||||
}
|
||||
|
||||
//Log out user by deleting login token for that active session
|
||||
User.logout = (tokenId) => {
|
||||
return db.promise().query('DELETE FROM user_active_session WHERE (id = ?)', [tokenId])
|
||||
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) => {
|
||||
|
@@ -136,19 +136,4 @@ router.post('/disableshare', function (req, res) {
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
//
|
||||
// Testing Action
|
||||
//
|
||||
//Reindex all Note. Not a very good function, not public
|
||||
router.get('/reindex5yu43prchuj903mrc', function (req, res) {
|
||||
|
||||
Note.migrateNoteTextToNewTable().then(status => {
|
||||
return res.send(status)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
|
||||
module.exports = router
|
@@ -33,7 +33,7 @@ router.post('/login', function (req, res) {
|
||||
// Logout User
|
||||
router.post('/logout', function (req, res) {
|
||||
|
||||
User.logout(req.headers.tokenId)
|
||||
User.logout(req.headers.sessionId)
|
||||
.then( returnData => {
|
||||
res.send(true)
|
||||
})
|
||||
|
Reference in New Issue
Block a user