Encrypted Notes Alpha!

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

View File

@ -42,6 +42,12 @@ body {
font-family: 'Roboto', 'Helvetica Neue', Arial, Helvetica, sans-serif; font-family: 'Roboto', 'Helvetica Neue', Arial, Helvetica, sans-serif;
} }
.ui.segment {
color: var(--text_color);
background-color: var(--background_color);
border-color: var(--border_color);
}
.ui.form input:not([type]), .ui.form input:not([type]),
.ui.form input:not([type]):focus, .ui.form input:not([type]):focus,
.ui.form textarea:not([type]), .ui.form textarea:not([type]),
@ -216,10 +222,6 @@ a:hover {
scrollbar-width: none; scrollbar-width: none;
} }
/*Makes the first line real big */ /*Makes the first line real big */
.squire-box > p:first-child {
font-size: 1.4em;
line-height: 1.7em;
}
.squire-box:focus { .squire-box:focus {
outline: none; outline: none;
} }

View File

@ -13,8 +13,8 @@
border-top-right-radius: 4px; border-top-right-radius: 4px;
border-top-left-radius: 4px; border-top-left-radius: 4px;
color: var(--text_color); color: white;
background-color: var(--background_color); background-color: #21ba45;
} }
.popup-row { .popup-row {
padding: 1em 5px; padding: 1em 5px;
@ -30,7 +30,7 @@
font-size: 1.25em; font-size: 1.25em;
} }
.popup-row + .popup-row { .popup-row + .popup-row {
border-top: 1px solid #000; border-top: 1px solid #FFF;
} }
</style> </style>
@ -64,6 +64,9 @@
}, },
mounted(){ mounted(){
// this.$bus.$emit('notification', 'Password Protection Removed')
// this.$bus.$emit('notification', 'Password Protection Removed')
// this.$bus.$emit('notification', 'Password Protection Removed')
}, },
methods: { methods: {

View File

@ -41,12 +41,69 @@
<!-- Squire box grows --> <!-- Squire box grows -->
<div class="note-wrapper"> <div class="note-wrapper">
<div id="squire-id" class="squire-box" ref="squirebox"></div> <textarea
ref="titleTextarea"
v-on:keyup="titleResize"
v-on:keydown="titleResize"
@keydown.enter.exact.prevent="editor.focus()"
rows="1"
:style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText']}"
v-on:blur="save" type="text" v-model="noteTitle" placeholder="Title" class="stealth-input">
</textarea>
<div v-show="isDecrypted" id="squire-id" class="squire-box" ref="squirebox" placeholder="Note Text"></div>
<!-- Decrypt note prompt -->
<div v-if="isEncrypted && !isDecrypted" class="ui basic padded segment">
<div class="ui raised segment">
<h3 class="ui center aligned icon header">
<i class="green lock alternate icon"></i>
<span v-if="!lockedOut" :data-tooltip="`Unlock Attempts: ${decryptAttempts}`">
This note is encrypted and requires a password to be opened.
</span>
<!-- note is locked for 5 minutes -->
<span v-if="lockedOut">
To many unlock attempts. Note is locked for 5 minutes.
</span>
</h3>
<!-- Decrypt note -->
<div class="ui form" v-if="!lockedOut">
<h5 class="ui horizontal divider header" v-if="passwordHint && passwordHint.length > 0">
Hint: {{ passwordHint }}
</h5>
<div class="field">
<input :name="`randomThing-${noteid}`" :id="`yupper-${noteid}`"type="password" v-model="password" placeholder="Note Password" v-on:keyup.enter="decryptNote" autofocus ref="decryptNotePrompt">
</div>
<div class="field">
<div v-on:click="decryptNote" class="ui green fluid button" v-if="password.length >= 3">
Unlock Note
</div>
<div class="ui disabled fluid button" v-if="password.length < 3">
Unlock Note
</div>
</div>
</div>
</div>
</div>
<!-- bottom stats --> <!-- bottom stats -->
<div class="ui basic segment"> <div class="ui basic segment">
<div class="ui grid"> <div class="ui grid">
<div class="sixteen wide column"> <div class="sixteen wide column">
<div class="ui basic button"v-if="!isEncrypted" v-on:click="passwordEnterVisible = true">
<i class="shield alternate icon"></i>
Password Protect
</div>
<div class="ui icon basic button" v-if="isEncrypted && isDecrypted" v-on:click="disableEncryption">
<i class="unlock icon"></i>
Remove Password
</div>
<div class="ui basic compact button"> <div class="ui basic compact button">
Status: {{ statusText }} Status: {{ statusText }}
</div> </div>
@ -56,6 +113,8 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- && this.$store.getters.getIsUserOnMobile --> <!-- && this.$store.getters.getIsUserOnMobile -->
@ -202,7 +261,56 @@
</div> </div>
</side-slide-menu> </side-slide-menu>
<div class="full-focus-shade"></div> <side-slide-menu v-if="passwordEnterVisible" v-on:close="passwordEnterVisible = false" :fullShadow="true" name="encrypt note">
<div class="ui basic segment" v-if="isDecrypted && isEncrypted">
<p>Note Decrypted</p>
<div class="ui green button" v-on:click="lockNote">Lock Note</div>
</div>
<div v-if="!isEncrypted" class="ui basic segment">
<div class="ui top attached segment">
<h2><i class="green lock alternate icon"></i>Password protect this Note</h2>
<p>Password protection will prevent anyone from reading the text of this note, unless they enter the correct password.</p>
<p><b>Only the note text is protected. Title, tags, and files are not encrypted and remain visible without a password.</b></p>
<p>The password you select will only be used for this note. You can use the same password on multiple notes. The note will be encrypted using the password entered. A longer password is will be more secure.</p>
<h4><i class="red icon exclamation triangle"></i> Warning. There is no way to recover a lost password.</h4>
</div>
<div class="ui bottom attached segment">
<div class="ui form">
<div class="field">
<label>New Password to lock this note</label>
<input :name="`randomThing-${noteid}`" :id="`yupper-${noteid}`"type="password" v-model="password" placeholder="Note Password">
</div>
<div class="field" v-if="password.length > 3">
<label>Confirm Password</label>
<input :name="`randomStuff-${noteid}`" :id="`heckye-${noteid}`"type="password" v-model="passwordConfirm" placeholder="Confirm Password">
</div>
<div class="field" v-if="password.length > 3">
<label>Password Hint - visible when unlocking note</label>
<input :name="`randomStuzz-${noteid}`" :id="`heckyo-${noteid}`"type="text" v-model="passwordHint" placeholder="Optional Password Hint" v-on:keyup.enter="enableEncryption">
</div>
<div class="field" v-if="passwordConfirm.length > 3 && password != passwordConfirm">
<div v-on:click="enableEncryption" class="ui disabled green button">
Passwords do not match
</div>
</div>
<div class="field" v-if="passwordConfirm.length > 3 && password == passwordConfirm">
<div v-on:click="enableEncryption" class="ui green button">
Protect!
</div>
</div>
</div>
</div>
</div>
</side-slide-menu>
<!-- <div class="full-focus-shade"></div> -->
</div> </div>
</template> </template>
@ -210,6 +318,7 @@
<script> <script>
import axios from 'axios' import axios from 'axios'
const crypto = require('crypto')
const DiffMatchPatch = require('../../../server/helpers/DiffMatchPatch') const DiffMatchPatch = require('../../../server/helpers/DiffMatchPatch')
export default { export default {
@ -233,6 +342,7 @@
currentNoteId: 0, currentNoteId: 0,
modified: false, modified: false,
noteText: '', noteText: '',
noteTitle: '',
rawTextId: 0, rawTextId: 0,
created: '', created: '',
updated: '', updated: '',
@ -270,6 +380,18 @@
colorPickerVisible: false, colorPickerVisible: false,
showFilesSideMenu: false, showFilesSideMenu: false,
showNoteOptions: false, showNoteOptions: false,
//Encryption options
passwordHint: '',
password: '', //Field Variables, only for form
passwordConfirm: '', //Only a form variable
hashedPass: '', //sha-256 password hash, sends to server for decryption
isEncrypted: false,
isDecrypted: false,
passwordEnterVisible: false,
decryptAttempts: 0,
lockedOut: false,
autoLockTimeout: null,
} }
}, },
watch: { watch: {
@ -299,6 +421,11 @@
}, },
beforeDestroy(){ beforeDestroy(){
this.password = ''
this.passwordConfirm = ''
this.hashedPass = ''
clearTimeout(this.autoLockTimeout)
this.$io.emit('leave_room', this.rawTextId) this.$io.emit('leave_room', this.rawTextId)
document.removeEventListener('visibilitychange', this.checkForUpdatedNote) document.removeEventListener('visibilitychange', this.checkForUpdatedNote)
@ -326,7 +453,14 @@
//focus on open, not on mobile, thats annoying //focus on open, not on mobile, thats annoying
if(!this.$store.getters.getIsUserOnMobile){ if(!this.$store.getters.getIsUserOnMobile){
this.editor.focus() // this.editor.focus()
if(this.noteTitle && this.noteTitle.length == 0){
this.$refs.titleTextarea.focus()
} else {
this.editor.moveCursorToEnd()
}
} }
//Click Event - Open links when clicked in editor or toggle checks //Click Event - Open links when clicked in editor or toggle checks
@ -705,48 +839,68 @@
}, },
loadNote(noteId){ loadNote(noteId){
let vm = this //Generate a random loading message
let doing = ['Loading','Loading','Getting','Fetching','Grabbing','Sequencing','Organizing','Untangling','Processing','Refining','Extracting','Fusing','Pruning','Expanding','Enlarging','Transfiguring','Quantizing','Ingratiating','Lumping'] let doing = ['Loading','Loading','Getting','Fetching','Grabbing','Sequencing','Organizing','Untangling','Processing','Refining','Extracting','Fusing','Pruning','Expanding','Enlarging','Transfiguring','Quantizing','Ingratiating','Lumping']
let thing = ['Note','Note','Note','Note','Data','Text','Document','Algorithm','Buffer','Client','Download','File','Frame','Graphics','Hardware','HTML','Interface','Logic','Mainframe','Memory','Media','Nodes','Network','Chaos'] let thing = ['Note','Note','Note','Note','Data','Text','Document','Algorithm','Buffer','Client','Download','File','Frame','Graphics','Hardware','HTML','Interface','Logic','Mainframe','Memory','Media','Nodes','Network','Chaos']
let p1 = doing[Math.floor(Math.random() * doing.length)] let p1 = doing[Math.floor(Math.random() * doing.length)]
let p2 = thing[Math.floor(Math.random() * thing.length)] let p2 = thing[Math.floor(Math.random() * thing.length)]
vm.loadingMessage = p1 + ' ' + p2 this.loadingMessage = p1 + ' ' + p2
//Component is activated with NoteId in place, lookup text with associated ID //Component is activated with NoteId in place, lookup text with associated ID
if(this.$store.getters.getLoggedIn){ if(this.$store.getters.getLoggedIn){
axios.post('/api/note/get', {'noteId': noteId}) axios.post('/api/note/get', { 'noteId': this.noteid, 'password':this.hashedPass })
.then(response => { .then(response => {
//Set up local data //Set up local data
vm.currentNoteId = noteId this.currentNoteId = this.noteid
this.rawTextId = response.data.rawTextId this.rawTextId = response.data.rawTextId
this.shareUsername = response.data.shareUsername this.shareUsername = response.data.shareUsername
this.passwordHint = response.data.password_hint
this.created = response.data.created this.created = response.data.created
this.updated = response.data.updated this.updated = response.data.updated
this.noteTitle = response.data.title
vm.noteText = response.data.text this.noteText = response.data.text
vm.diffNoteText = response.data.text this.diffNoteText = response.data.text
vm.lastNoteHash = vm.hashString(response.data.text) this.lastNoteHash = this.hashString(response.data.text)
//Set up note colors //Set up note colors
if(response.data.color){ if(response.data.color){
vm.styleObject = JSON.parse(response.data.color) this.styleObject = JSON.parse(response.data.color)
} }
if(response.data.pinned != null){ if(response.data.pinned != null){
vm.pinned = response.data.pinned this.pinned = response.data.pinned
} }
vm.archived = response.data.archived this.archived = response.data.archived
vm.attachmentCount = response.data.attachment_count this.attachmentCount = response.data.attachment_count
this.loading = false this.loading = false
vm.$nextTick(() => { this.isDecrypted = response.data.decrypted
this.isEncrypted = response.data.encrypted == 1
this.decryptAttempts = response.data.decrypt_attempts_count
this.lockedOut = response.data.lockedOut
//If password is required, display a prompt and focus on it
if(this.password.length == 0 && this.isEncrypted && !this.isDecrypted){
this.$nextTick(() => {
if(this.$refs.decryptNotePrompt){
// this.editor.moveCursorToEnd()
this.$refs.decryptNotePrompt.focus()
}
})
}
this.$nextTick(() => {
//Adjust note title size after load
this.titleResize()
this.setupWebSockets() this.setupWebSockets()
this.initSquire() this.initSquire()
this.startAutolockTimer()
}) })
}) })
@ -917,7 +1071,7 @@
.createExpression(xpath) .createExpression(xpath)
.evaluate(document, XPathResult.FIRST_ORDERED_NODE_TYPE) .singleNodeValue .evaluate(document, XPathResult.FIRST_ORDERED_NODE_TYPE) .singleNodeValue
}, },
onKeyup(event){ onKeyup(){
this.statusText = 'Save' this.statusText = 'Save'
@ -943,6 +1097,12 @@
// return resolve(true) // return resolve(true)
//Encrypted notes that are not decrypted should not be saved
if(this.isEncrypted && !this.isDecrypted){
return resolve(true)
}
//Don't save note if its hash doesn't change //Don't save note if its hash doesn't change
const currentNoteText = this.getText() const currentNoteText = this.getText()
if( this.lastNoteHash == this.hashString( currentNoteText )){ if( this.lastNoteHash == this.hashString( currentNoteText )){
@ -958,11 +1118,14 @@
} }
const postData = { const postData = {
'noteId':this.currentNoteId, 'noteId': this.currentNoteId,
'title': this.noteTitle,
'text': currentNoteText, 'text': currentNoteText,
'color': JSON.stringify(this.styleObject), //Save little json color object 'color': JSON.stringify(this.styleObject), //Save little json color object
'pinned': this.pinned, 'pinned': this.pinned,
'archived':this.archived, 'archived': this.archived,
'password': this.hashedPass,
'hint': this.passwordHint,
} }
this.statusText = 'Saving' this.statusText = 'Saving'
@ -971,8 +1134,11 @@
this.updated = Math.round((+new Date)/1000) this.updated = Math.round((+new Date)/1000)
this.modified = true this.modified = true
console.log('Saved')
//Update last saved note hash //Update last saved note hash
this.lastNoteHash = this.hashString( currentNoteText ) this.lastNoteHash = this.hashString( currentNoteText )
this.startAutolockTimer()
return resolve(true) return resolve(true)
}) })
}) })
@ -1010,6 +1176,8 @@
}, },
hashString(text){ hashString(text){
text = this.noteTitle + text
var hash = 0; var hash = 0;
if (text == null || text.length == 0) { if (text == null || text.length == 0) {
return hash; return hash;
@ -1055,7 +1223,80 @@
this.$io.on('incoming_diff', incomingDiffData => { this.$io.on('incoming_diff', incomingDiffData => {
this.patchText(incomingDiffData) this.patchText(incomingDiffData)
}) })
},
decryptNote(){
const hashed = crypto.createHash('sha256').update(this.password).digest().toString('base64')
//Remove plaintext password
this.hashedPass = hashed
this.password = ''
this.passwordConfirm = ''
this.loadNote()
},
lockNote(){
this.save().then(results => {
this.isDecrypted = false
this.password = ''
this.hashedPass = ''
this.passwordEnterVisible = false
this.setText('')
})
},
enableEncryption(){
if(this.noteText == ''){
this.noteText = 'Text Typed here is encrypted.'
} }
const hashed = crypto.createHash('sha256').update(this.password).digest().toString('base64')
//Remove plaintext password
this.hashedPass = hashed
this.lastNoteHash = 0
this.password = ''
this.passwordConfirm = ''
this.passwordEnterVisible = false
this.save()
.then(results => {
this.$bus.$emit('notification', 'Password Protection Enabled')
this.loadNote()
})
},
disableEncryption(){
this.lastNoteHash = 0
this.isEncrypted = false
this.password = ''
this.passwordConfirm = ''
this.hashedPass = ''
this.passwordEnterVisible = false
//Reload Note
this.save()
.then(results => {
this.loadNote()
this.$bus.$emit('notification', 'Password Protection Removed')
})
},
titleResize(){
//Resize the title field
let element = this.$refs.titleTextarea
let padding = 0
element.style.height = 'auto';
element.style.height = (element.scrollHeight + padding) +'px';
},
startAutolockTimer(){
//Start autolock timer on encrypted notes that are encrypted and in a decrypted state
if(this.isEncrypted && this.isDecrypted){
clearTimeout(this.autoLockTimeout)
this.autoLockTimeout = setTimeout(() => {
this.lockNote()
}, (60 * 1000 * 20) ) //Autolock after 20 min
}
},
} }
} }
</script> </script>
@ -1090,7 +1331,18 @@
} }
.stealth-input {
width: 100%;
padding: 10px 15px 5px;
background-color: rgba(255,255,255,0.1);
border: none;
font-size: 1.7em;
/*line-height: 1.7em;*/
color: var(--text_color);
resize: none;
overflow: hidden;
}
/*Settings manager styles */ /*Settings manager styles */
.all-settings { .all-settings {

View File

@ -26,7 +26,7 @@
</span> </span>
</span> </span>
<span v-if="note.title == '' && note.subtext == ''"> <span v-if="note.title == '' && note.subtext == '' && note.encrypted == 0">
Empty Note Empty Note
</span> </span>
@ -37,8 +37,7 @@
<!-- Title display --> <!-- Title display -->
<span v-if="note.title.length > 0" <span v-if="note.title.length > 0"
data-test-id="title" data-test-id="title"
class="big-text" class="big-text"><p>{{ note.title }}</p></span>
v-html="note.title"></span>
<!-- Sub text display --> <!-- Sub text display -->
<span v-if="note.subtext.length > 0 && !isShowingSearchResults()" <span v-if="note.subtext.length > 0 && !isShowingSearchResults()"
@ -46,6 +45,12 @@
class="small-text" class="small-text"
v-html="note.subtext"></span> v-html="note.subtext"></span>
<p v-if="note.encrypted == 1">
<i class="green lock icon"></i>
Locked
</p>
<!-- Display highlights from solr results --> <!-- Display highlights from solr results -->
<span v-if="note.note_highlights.length > 0" class="term-usage"> <span v-if="note.note_highlights.length > 0" class="term-usage">
<span <span

View File

@ -29,6 +29,11 @@
<!-- <span>{{ $store.getters.totals['archivedNotes'] }}</span> --> <!-- <span>{{ $store.getters.totals['archivedNotes'] }}</span> -->
</div> </div>
<div class="ui basic button" v-on:click="updateFastFilters(4)" v-if="$store.getters.totals && $store.getters.totals['encryptedNotes'] > 0">
<i class="green lock alternate icon"></i>Locked
<!-- <span>{{ $store.getters.totals['encryptedNotes'] }}</span> -->
</div>
</div> </div>
<div class="eight wide column" v-if="showClear"> <div class="eight wide column" v-if="showClear">
@ -679,6 +684,7 @@
'withTags', // 'Only Show Notes with Tags' 'withTags', // 'Only Show Notes with Tags'
'onlyArchived', //'Only Show Archived Notes' 'onlyArchived', //'Only Show Archived Notes'
'onlyShowSharedNotes', //Only show shared notes 'onlyShowSharedNotes', //Only show shared notes
'onlyShowEncrypted',
] ]
let filter = {} let filter = {}

View File

@ -94,9 +94,9 @@ export default new Vuex.Store({
//Save all the totals for the user //Save all the totals for the user
state.userTotals = totalsObject state.userTotals = totalsObject
// Object.keys(totalsObject).forEach( key => { Object.keys(totalsObject).forEach( key => {
// console.log(key + ' -- ' + totalsObject[key]) console.log(key + ' -- ' + totalsObject[key])
// }) })
} }
}, },

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

View File

@ -8,7 +8,8 @@ let ProcessText = require('@helpers/ProcessText')
const DiffMatchPatch = require('@helpers/DiffMatchPatch') const DiffMatchPatch = require('@helpers/DiffMatchPatch')
var rp = require('request-promise'); const cs = require('@helpers/CryptoString')
const rp = require('request-promise');
const fs = require('fs') const fs = require('fs')
let Note = module.exports = {} 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) => { return new Promise((resolve, reject) => {
if(userId == null || userId < 10){ reject('User Id required to create note') } 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) const created = Math.round((+new Date)/1000)
db.promise() 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) => { .then( (rows, fields) => {
const rawTextId = rows[0].insertId const rawTextId = rows[0].insertId
@ -129,7 +130,11 @@ Note.reindex = (userId, noteId) => {
Note.get(userId, noteId) Note.get(userId, noteId)
.then(note => { .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 // Update Solr index
@ -137,7 +142,7 @@ Note.reindex = (userId, noteId) => {
Tags.string(userId, noteId) Tags.string(userId, noteId)
.then(tagString => { .then(tagString => {
const fullText = ProcessText.removeHtml(noteText) +' '+ tagString const fullText = note.title + ' ' + ProcessText.removeHtml(noteText) +' '+ tagString
db.promise() db.promise()
.query(` .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) => { return new Promise((resolve, reject) => {
//Prevent note loss if it saves with empty text //Prevent note loss if it saves with empty text
if(ProcessText.removeHtml(noteText) == ''){ if(ProcessText.removeHtml(noteText) == ''){
console.log('Not saving empty note') // console.log('Not saving empty note')
resolve(false) // resolve(false)
} }
const now = Math.round((+new Date)/1000) const now = Math.round((+new Date)/1000)
db.promise() 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) => { .then((rows, fields) => {
const textId = rows[0][0]['note_raw_text_id'] 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 //Update Note text
return db.promise() 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) => { .then( (rows, fields) => {
const encrypted = password.length > 3 ? 1:0
//Update other note attributes //Update other note attributes
return db.promise() return db.promise()
.query('UPDATE note SET pinned = ?, archived = ?, color = ? WHERE id = ? AND user_id = ? LIMIT 1', .query('UPDATE note SET pinned = ?, archived = ?, color = ?, encrypted = ? WHERE id = ? AND user_id = ? LIMIT 1',
[pinned, archived, color, noteId, userId]) [pinned, archived, color, encrypted, noteId, userId])
}) })
.then((rows, fields) => { .then((rows, fields) => {
@ -309,6 +340,9 @@ Note.getDiffText = (userId, noteId, usersCurrentText, lastUpdated) => {
Note.get(userId, noteId) Note.get(userId, noteId)
.then(noteObject => { .then(noteObject => {
if(!noteObject.text || !usersCurrentText){
resolve(null)
}
let oldText = noteObject.text.replace(/(\r\n|\n|\r)/gm,"") let oldText = noteObject.text.replace(/(\r\n|\n|\r)/gm,"")
let newText = usersCurrentText.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) => { return new Promise((resolve, reject) => {
db.promise() db.promise()
.query(` .query(`
SELECT SELECT
note_raw_text.title,
note_raw_text.text, note_raw_text.text,
note_raw_text.salt,
note_raw_text.password_hint,
note_raw_text.updated as updated, note_raw_text.updated as updated,
note_raw_text.decrypt_attempts_count,
note_raw_text.last_decrypted_date,
note.created, note.created,
note.pinned, note.pinned,
note.archived, note.archived,
note.color, note.color,
note.encrypted,
count(distinct attachment.id) as attachment_count, count(distinct attachment.id) as attachment_count,
note.note_raw_text_id as rawTextId, note.note_raw_text_id as rawTextId,
shareUser.username as shareUsername 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) 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 attachment ON (note.id = attachment.note_id)
LEFT JOIN user as shareUser ON (note.share_user_id = shareUser.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) => { .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 //Return note data
resolve(rows[0][0]) delete noteData.salt //remove salt from return data
noteData.lockedOut = noteLockedOut
resolve(noteData)
}) })
.catch(console.log) .catch(console.log)
@ -484,6 +583,7 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
let noteSearchQuery = ` let noteSearchQuery = `
SELECT note.id, SELECT note.id,
SUBSTRING(note_raw_text.text, 1, 1500) as text, SUBSTRING(note_raw_text.text, 1, 1500) as text,
note_raw_text.title as title,
note_raw_text.updated as updated, note_raw_text.updated as updated,
opened, opened,
color, color,
@ -491,6 +591,7 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
count(distinct attachment.id) as attachment_count, count(distinct attachment.id) as attachment_count,
note.pinned, note.pinned,
note.archived, note.archived,
note.encrypted,
GROUP_CONCAT(DISTINCT tag.text) as tags, GROUP_CONCAT(DISTINCT tag.text) as tags,
GROUP_CONCAT(DISTINCT attachment.file_location) as thumbs, GROUP_CONCAT(DISTINCT attachment.file_location) as thumbs,
shareUser.username as shareUsername, shareUser.username as shareUsername,
@ -527,6 +628,10 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
searchAllNotes = true searchAllNotes = true
} }
if(fastFilters.onlyShowEncrypted == 1){
noteSearchQuery += ' AND encrypted = 1'
}
//If tags are passed, use those tags in search //If tags are passed, use those tags in search
if(searchTags.length > 0){ if(searchTags.length > 0){
searchParams.push(searchTags) searchParams.push(searchTags)
@ -610,6 +715,7 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
noteIds.push(note.id) noteIds.push(note.id)
if(note.text == null){ note.text = '' } if(note.text == null){ note.text = '' }
if(note.encrypted == 1){ note.text = '' }
//Deduce note title //Deduce note title
const textData = ProcessText.deduceNoteTitle(note.text) const textData = ProcessText.deduceNoteTitle(note.text)
@ -617,7 +723,10 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
// console.log(textData) // console.log(textData)
note.title = textData.title if(note.title == null){
note.title = ''
}
note.subtext = textData.sub note.subtext = textData.sub
note.titleLength = textData.titleLength note.titleLength = textData.titleLength
note.subtextLength = textData.subtextLength note.subtextLength = textData.subtextLength

View File

@ -71,7 +71,7 @@ QuickNote.update = (userId, pushText) => {
let newText = broken +''+ d.text let newText = broken +''+ d.text
//Save that, then return the new 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 => { .then( saveResults => {
resolve({ resolve({
id:d.id, id:d.id,

View File

@ -124,11 +124,12 @@ User.getCounts = (userId) => {
`SELECT `SELECT
SUM(pinned = 1 && archived = 0 && share_user_id IS NULL) AS pinnedNotes, SUM(pinned = 1 && archived = 0 && share_user_id IS NULL) AS pinnedNotes,
SUM(archived = 1 && share_user_id IS NULL) AS archivedNotes, 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 IS NULL) AS totalNotes,
SUM(share_user_id != ?) AS sharedToNotes, SUM(share_user_id != ?) AS sharedToNotes,
SUM( (share_user_id != ? && opened IS null) || (share_user_id != ? && note_raw_text.updated > opened) ) AS unreadNotes SUM( (share_user_id != ? && opened IS null) || (share_user_id != ? && note_raw_text.updated > opened) ) AS unreadNotes
FROM note 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]) WHERE user_id = ?`, [userId, userId, userId, userId])
.then( (rows, fields) => { .then( (rows, fields) => {

View File

@ -24,7 +24,7 @@ router.use(function setUserId (req, res, next) {
// //
router.post('/get', function (req, res) { router.post('/get', function (req, res) {
// req.io.emit('welcome_homie', 'Welcome, dont poop from excitement') // 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 => { .then( data => {
//Join room when user opens note //Join room when user opens note
// req.io.join('note_room') // req.io.join('note_room')
@ -38,12 +38,12 @@ router.post('/delete', function (req, res) {
}) })
router.post('/create', 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}) ) .then( id => res.send({id}) )
}) })
router.post('/update', function (req, res) { 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}) ) .then( id => res.send({id}) )
}) })

View File

@ -12,4 +12,4 @@
# z - Compress for speed # z - Compress for speed
# h - Human Readable file sizes # h - Human Readable file sizes
rsync -e 'ssh' --exclude-from=dontSync.txt -havzC --update mab@marvin.local:/home/mab/pi/ /Users/maxgialanella/Code/privateInternet rsync -e 'ssh' --exclude-from=dontSync.txt -havzC --update mab@marvin.local:/home/mab/pi/ .