Encrypted Notes Alpha!

fixes #28
This commit is contained in:
Max G
2020-03-13 23:34:32 +00:00
parent f7fc937d26
commit f481a97a8c
13 changed files with 547 additions and 60 deletions

View File

@@ -0,0 +1,109 @@
/*
Crypto String
Securely Encrypts and decrypts a string using a password
*/
const IV_BYTE_SIZE = 100 // Size of initialization vector
const SALT_BYTE_SIZE = 900 // Size of salt
const KEY_BYTE_SIZE = 32 // size of cipher key
const AUTH_TAG_SIZE = 16 // Size of authentication tag
const crypto = require('crypto')
let CryptoString = module.exports = {}
CryptoString.encrypt = (password, salt64, rawText) => {
const initializationVector = crypto.randomBytes(IV_BYTE_SIZE)
const salt = Buffer.from(salt64, 'base64')
const key = CryptoString.seasonedPassword(password, salt)
const cipher = crypto.createCipheriv('aes-256-gcm', key, initializationVector, { 'authTagLength': AUTH_TAG_SIZE })
let encryptedMessage = cipher.update(rawText)
encryptedMessage = Buffer.concat([encryptedMessage, cipher.final()])
return Buffer.concat([initializationVector, encryptedMessage, cipher.getAuthTag()]).toString('base64')
}
//Decrypt base64 string cipher text,
CryptoString.decrypt = (password, salt64, cipherTextString) => {
let cipherText = Buffer.from(cipherTextString, 'base64')
const salt = Buffer.from(salt64, 'base64')
const authTag = cipherText.slice(AUTH_TAG_SIZE*-1)
const initializationVector = cipherText.slice(0, IV_BYTE_SIZE)
const encryptedMessage = cipherText.slice(IV_BYTE_SIZE, AUTH_TAG_SIZE*-1)
const key = CryptoString.seasonedPassword(password, salt)
const decipher = crypto.createDecipheriv('aes-256-gcm', key, initializationVector, { 'authTagLength': AUTH_TAG_SIZE })
try {
decipher.setAuthTag(authTag)
let messagetext = decipher.update(encryptedMessage)
messagetext = Buffer.concat([messagetext, decipher.final()]).toString('utf8')
return messagetext
} catch(err) {
// console.log(err)
return null
}
}
//Salt the password - return {buffer}
CryptoString.seasonedPassword = (password, salt) => {
return crypto.scryptSync(password, salt, KEY_BYTE_SIZE)
}
//Create random salt - return {string}
CryptoString.createSalt = () => {
return crypto.randomBytes(SALT_BYTE_SIZE).toString('base64')
}
CryptoString.hash = (hashString) => {
return crypto.createHash('sha256').update(hashString).digest()
}
CryptoString.test = () => {
const pp = (title, output) => {
console.log('----------------'+title+'----------------')
console.log(output)
}
const password = 'ItsMePasswordio123'
const text = '<p>The genesis of&nbsp a new note. Very magical.<br></p><p>Quite a weonderful thing<br></p><p><br></p><p>Weonderful for sheore <br></p>'
const hashTest = CryptoString.createSalt('password')
const salt = CryptoString.createSalt()
pp('salt',salt.length)
const seasonPass = CryptoString.seasonedPassword(password, salt)
const cipherText = CryptoString.encrypt(password, salt, text)
const decipheredText = CryptoString.decrypt(password, salt, cipherText)
pp('Success Decrypt', decipheredText == text ? 'Pass 😁':'Fail' )
const wrongPass = CryptoString.decrypt('Wrong Password', salt, cipherText)
pp('Wrong Password', wrongPass === null ? 'Pass':'Fail')
const wrongSalt = CryptoString.decrypt(password, 'Wrong Salt', cipherText)
pp('Wrong Salt', wrongSalt === null ? 'Pass':'Fail')
const wrongCipher = CryptoString.decrypt(password, salt, Buffer.from('Hello there'))
pp('Wrong Cipher Text', wrongCipher === null ? 'Pass':'Fail')
}

View File

@@ -56,7 +56,7 @@ ProcessText.deduceNoteTitle = (inString) => {
const tagFreeLength = ProcessText.removeHtml(inString).length
if(tagFreeLength < 100){
title = ProcessText.stripBlankHtmlLines(inString)
sub = ProcessText.stripBlankHtmlLines(inString)
return {title, sub}
}
@@ -178,7 +178,7 @@ ProcessText.deduceNoteTitle = (inString) => {
//Pull out title if its not an empty string
if(ProcessText.removeHtml(finalLines[0]).trim().replace('&nbsp','').length > 0 && !noTitleJustList){
title = finalLines.shift()
// title = finalLines.shift()
}
sub = finalLines.join('')

View File

@@ -8,7 +8,8 @@ let ProcessText = require('@helpers/ProcessText')
const DiffMatchPatch = require('@helpers/DiffMatchPatch')
var rp = require('request-promise');
const cs = require('@helpers/CryptoString')
const rp = require('request-promise');
const fs = require('fs')
let Note = module.exports = {}
@@ -98,7 +99,7 @@ Note.stressTest = () => {
// --------------
Note.create = (userId, noteText, quickNote = 0) => {
Note.create = (userId, noteTitle, noteText, quickNote = 0, ) => {
return new Promise((resolve, reject) => {
if(userId == null || userId < 10){ reject('User Id required to create note') }
@@ -106,7 +107,7 @@ Note.create = (userId, noteText, quickNote = 0) => {
const created = Math.round((+new Date)/1000)
db.promise()
.query(`INSERT INTO note_raw_text (text, updated) VALUE (?, ?)`, [noteText, created])
.query(`INSERT INTO note_raw_text (text, title, updated) VALUE (?, ?, ?)`, [noteText, noteTitle, created])
.then( (rows, fields) => {
const rawTextId = rows[0].insertId
@@ -129,7 +130,11 @@ Note.reindex = (userId, noteId) => {
Note.get(userId, noteId)
.then(note => {
const noteText = note.text
let noteText = note.text
if(note.encrypted == 1){
noteText = '' //Don't put note text in encrypted notes
}
//
// Update Solr index
@@ -137,7 +142,7 @@ Note.reindex = (userId, noteId) => {
Tags.string(userId, noteId)
.then(tagString => {
const fullText = ProcessText.removeHtml(noteText) +' '+ tagString
const fullText = note.title + ' ' + ProcessText.removeHtml(noteText) +' '+ tagString
db.promise()
.query(`
@@ -158,33 +163,59 @@ Note.reindex = (userId, noteId) => {
})
}
Note.update = (io, userId, noteId, noteText, color, pinned, archived) => {
Note.update = (io, userId, noteId, noteText, noteTitle, color, pinned, archived, password = '', passwordHint = '') => {
return new Promise((resolve, reject) => {
//Prevent note loss if it saves with empty text
if(ProcessText.removeHtml(noteText) == ''){
console.log('Not saving empty note')
resolve(false)
// console.log('Not saving empty note')
// resolve(false)
}
const now = Math.round((+new Date)/1000)
db.promise()
.query('SELECT note_raw_text_id FROM note WHERE id = ? AND user_id = ?', [noteId, userId])
.query(`
SELECT note_raw_text_id, salt FROM note
JOIN note_raw_text ON note_raw_text_id = note_raw_text.id
WHERE note.id = ? AND user_id = ?`, [noteId, userId])
.then((rows, fields) => {
const textId = rows[0][0]['note_raw_text_id']
let salt = rows[0][0]['salt']
//If password is removed, remove salt. generate a new one next time its encrypted
if(password.length == 0){
salt = null
}
//If a password is set, create a salt
if(password.length > 3 && !salt){
salt = cs.createSalt()
//Save password hint on first encryption
if(passwordHint.length > 0){
db.promise().query('UPDATE note_raw_text SET password_hint = ? WHERE id = ?', [passwordHint, textId])
}
}
//Encrypt note text if proper data is setup
if(password.length > 3 && salt.length > 1000){
noteText = cs.encrypt(password, salt, noteText)
}
//Update Note text
return db.promise()
.query('UPDATE note_raw_text SET text = ?, updated = ? WHERE id = ?', [noteText, now, textId])
.query('UPDATE note_raw_text SET text = ?, title = ?, updated = ?, salt = ? WHERE id = ?', [noteText, noteTitle, now, salt, textId])
})
.then( (rows, fields) => {
const encrypted = password.length > 3 ? 1:0
//Update other note attributes
return db.promise()
.query('UPDATE note SET pinned = ?, archived = ?, color = ? WHERE id = ? AND user_id = ? LIMIT 1',
[pinned, archived, color, noteId, userId])
.query('UPDATE note SET pinned = ?, archived = ?, color = ?, encrypted = ? WHERE id = ? AND user_id = ? LIMIT 1',
[pinned, archived, color, encrypted, noteId, userId])
})
.then((rows, fields) => {
@@ -309,6 +340,9 @@ Note.getDiffText = (userId, noteId, usersCurrentText, lastUpdated) => {
Note.get(userId, noteId)
.then(noteObject => {
if(!noteObject.text || !usersCurrentText){
resolve(null)
}
let oldText = noteObject.text.replace(/(\r\n|\n|\r)/gm,"")
let newText = usersCurrentText.replace(/(\r\n|\n|\r)/gm,"")
@@ -354,17 +388,23 @@ Note.getDiffText = (userId, noteId, usersCurrentText, lastUpdated) => {
}
Note.get = (userId, noteId) => {
Note.get = (userId, noteId, password = '') => {
return new Promise((resolve, reject) => {
db.promise()
.query(`
SELECT
SELECT
note_raw_text.title,
note_raw_text.text,
note_raw_text.salt,
note_raw_text.password_hint,
note_raw_text.updated as updated,
note_raw_text.decrypt_attempts_count,
note_raw_text.last_decrypted_date,
note.created,
note.pinned,
note.archived,
note.color,
note.encrypted,
count(distinct attachment.id) as attachment_count,
note.note_raw_text_id as rawTextId,
shareUser.username as shareUsername
@@ -372,15 +412,74 @@ Note.get = (userId, noteId) => {
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
LEFT JOIN attachment ON (note.id = attachment.note_id)
LEFT JOIN user as shareUser ON (note.share_user_id = shareUser.id)
WHERE note.user_id = ? AND note.id = ? LIMIT 1`, [userId,noteId])
WHERE note.user_id = ? AND note.id = ? LIMIT 1`, [userId, noteId])
.then((rows, fields) => {
const created = Math.round((+new Date)/1000)
const nowTime = Math.round((+new Date)/1000)
let noteLockedOut = false
let noteData = rows[0][0]
const rawTextId = noteData['rawTextId']
noteData.decrypted = true
db.promise().query(`UPDATE note SET opened = ? WHERE (id = ?)`, [created, noteId])
//If this is not and encrypted note, pass decrypted true, skip encryption stuff
if(noteData.encrypted == 1){
noteData.decrypted = false
}
//
//Rate Limiting
//
//Check if note is exceeding decrypt attempt limit
if(noteData.encrypted == 1){
const timeSinceLastUnlock = nowTime - noteData.last_decrypted_date
//To many attempts in less than 5 minutes, note is locked
if(noteData.decrypt_attempts_count > 3 && timeSinceLastUnlock < 300){
console.log('Locked Out')
noteLockedOut = true
}
//its been 5 minutes, reset attempt count
if(noteData.decrypt_attempts_count > 0 && timeSinceLastUnlock > 300){
noteLockedOut = false
noteData.decrypt_attempts_count = 0
console.log('Resseting Lockout')
db.promise().query('UPDATE note_raw_text SET last_decrypted_date = ?, decrypt_attempts_count = 0 WHERE id = ?', [nowTime, rawTextId ])
}
}
//Note is encrypted, lets try and decipher it with the given password
if(password.length > 3 && noteData.encrypted == 1 && !noteLockedOut){
const decipheredText = cs.decrypt(password, noteData.salt, noteData.text)
//Text was decrypted, return decrypted text
if(decipheredText !== null){
noteData.decrypted = true
noteData.text = decipheredText
//Save last decrypted date, reset decrypt atempts
db.promise().query('UPDATE note_raw_text SET last_decrypted_date = ?, decrypt_attempts_count = 0 WHERE id = ?', [nowTime, rawTextId ])
}
//Text was not deciphered, delete object, never return cipher text
if(decipheredText === null){
noteData.text = '' //Never return cipher text
noteData.decryptFail = true
noteData.decrypt_attempts_count++ //Update display for user
//Update decrypt attempts
db.promise().query('UPDATE note_raw_text SET decrypt_attempts_count = decrypt_attempts_count +1 WHERE id = ?', [rawTextId ])
}
}
db.promise().query(`UPDATE note SET opened = ? WHERE (id = ?)`, [nowTime, noteId])
//Return note data
resolve(rows[0][0])
delete noteData.salt //remove salt from return data
noteData.lockedOut = noteLockedOut
resolve(noteData)
})
.catch(console.log)
@@ -484,6 +583,7 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
let noteSearchQuery = `
SELECT note.id,
SUBSTRING(note_raw_text.text, 1, 1500) as text,
note_raw_text.title as title,
note_raw_text.updated as updated,
opened,
color,
@@ -491,6 +591,7 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
count(distinct attachment.id) as attachment_count,
note.pinned,
note.archived,
note.encrypted,
GROUP_CONCAT(DISTINCT tag.text) as tags,
GROUP_CONCAT(DISTINCT attachment.file_location) as thumbs,
shareUser.username as shareUsername,
@@ -527,6 +628,10 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
searchAllNotes = true
}
if(fastFilters.onlyShowEncrypted == 1){
noteSearchQuery += ' AND encrypted = 1'
}
//If tags are passed, use those tags in search
if(searchTags.length > 0){
searchParams.push(searchTags)
@@ -610,14 +715,18 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
noteIds.push(note.id)
if(note.text == null){ note.text = '' }
if(note.encrypted == 1){ note.text = '' }
//Deduce note title
const textData = ProcessText.deduceNoteTitle(note.text)
// console.log(textData)
// console.log(textData)
if(note.title == null){
note.title = ''
}
note.title = textData.title
note.subtext = textData.sub
note.titleLength = textData.titleLength
note.subtextLength = textData.subtextLength

View File

@@ -71,7 +71,7 @@ QuickNote.update = (userId, pushText) => {
let newText = broken +''+ d.text
//Save that, then return the new text
Note.update(null, userId, d.id, newText, d.color, d.pinned, d.archived)
Note.update(null, userId, d.id, newText, '', d.color, d.pinned, d.archived)
.then( saveResults => {
resolve({
id:d.id,

View File

@@ -124,11 +124,12 @@ User.getCounts = (userId) => {
`SELECT
SUM(pinned = 1 && archived = 0 && share_user_id IS NULL) AS pinnedNotes,
SUM(archived = 1 && share_user_id IS NULL) AS archivedNotes,
SUM(encrypted = 1) AS encryptedNotes,
SUM(share_user_id IS NULL) AS totalNotes,
SUM(share_user_id != ?) AS sharedToNotes,
SUM( (share_user_id != ? && opened IS null) || (share_user_id != ? && note_raw_text.updated > opened) ) AS unreadNotes
FROM note
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
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) => {

View File

@@ -24,7 +24,7 @@ router.use(function setUserId (req, res, next) {
//
router.post('/get', function (req, res) {
// req.io.emit('welcome_homie', 'Welcome, dont poop from excitement')
Notes.get(userId, req.body.noteId)
Notes.get(userId, req.body.noteId, req.body.password)
.then( data => {
//Join room when user opens note
// req.io.join('note_room')
@@ -38,12 +38,12 @@ router.post('/delete', function (req, res) {
})
router.post('/create', function (req, res) {
Notes.create(userId, req.body.title)
Notes.create(userId, req.body.title, req.body.text)
.then( id => res.send({id}) )
})
router.post('/update', function (req, res) {
Notes.update(req.io, userId, req.body.noteId, req.body.text, req.body.color, req.body.pinned, req.body.archived)
Notes.update(req.io, userId, req.body.noteId, req.body.text, req.body.title, req.body.color, req.body.pinned, req.body.archived, req.body.password, req.body.hint)
.then( id => res.send({id}) )
})