Fully Encrypted notes Beta
* Encrypts all notes going to the database * Creates encrypted snippets for loading note title cards * Creates an encrypted search index when note is changed * Migrates users to encrypted notes on login * Creates new encrypted master keys for newly logged in users
This commit is contained in:
		@@ -30,6 +30,7 @@ export default {
 | 
				
			|||||||
		//Puts token into state on page load
 | 
							//Puts token into state on page load
 | 
				
			||||||
		let token = localStorage.getItem('loginToken')
 | 
							let token = localStorage.getItem('loginToken')
 | 
				
			||||||
		let username = localStorage.getItem('username')
 | 
							let username = localStorage.getItem('username')
 | 
				
			||||||
 | 
							let masterKey = localStorage.getItem('masterKey')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// const socket = io({ path:'/socket' });
 | 
							// const socket = io({ path:'/socket' });
 | 
				
			||||||
		const socket = this.$io
 | 
							const socket = this.$io
 | 
				
			||||||
@@ -50,7 +51,7 @@ export default {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		//Put user data into global store on load
 | 
							//Put user data into global store on load
 | 
				
			||||||
		if(token){
 | 
							if(token){
 | 
				
			||||||
			this.$store.commit('setLoginToken', {token, username})
 | 
								this.$store.commit('setLoginToken', {token, username, masterKey})
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -86,17 +86,6 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
				<div class="edit-divide"></div>
 | 
									<div class="edit-divide"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				<!-- protect -->
 | 
					 | 
				
			||||||
				<div class="edit-button" v-if="!isEncrypted" 
 | 
					 | 
				
			||||||
					v-on:click="$router.push(`/notes/open/${noteid}/menu/passwordprotect`)" data-tooltip="Password Protect" data-position="bottom center" data-inverted>
 | 
					 | 
				
			||||||
					<i class="shield alternate icon"></i>
 | 
					 | 
				
			||||||
				</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				<!-- data-tooltip="Remove Password Protection" -->
 | 
					 | 
				
			||||||
				<div class="edit-button" v-if="isEncrypted && isDecrypted" v-on:click="disableEncryption()" data-tooltip="Close" data-position="bottom center" data-inverted>
 | 
					 | 
				
			||||||
					<i class="unlock icon"></i>
 | 
					 | 
				
			||||||
				</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				<div class="edit-button" v-on:click="onToggleArchived()" :data-tooltip="archived == 1?'Move to main list':'Move to Archive'" data-position="bottom center" data-inverted>
 | 
									<div class="edit-button" v-on:click="onToggleArchived()" :data-tooltip="archived == 1?'Move to main list':'Move to Archive'" data-position="bottom center" data-inverted>
 | 
				
			||||||
					<span v-if="archived == 1"><i class="green archive icon"></i></span>
 | 
										<span v-if="archived == 1"><i class="green archive icon"></i></span>
 | 
				
			||||||
					<span v-if="archived != 1"><i class="archive icon"></i></span>
 | 
										<span v-if="archived != 1"><i class="archive icon"></i></span>
 | 
				
			||||||
@@ -125,8 +114,9 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			<!-- Loading indicator  -->
 | 
								<!-- Loading indicator  -->
 | 
				
			||||||
			<div v-if="loading" class="loading-note">
 | 
								<div v-if="loading" class="loading-note">
 | 
				
			||||||
				<div class="ui active dimmer">
 | 
									<div class="loading-text">
 | 
				
			||||||
					<div class="ui text loader">{{loadingMessage}}</div>
 | 
										Decrypting Note & 
 | 
				
			||||||
 | 
										{{loadingMessage}}
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -267,55 +257,6 @@
 | 
				
			|||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		</side-slide-menu>
 | 
							</side-slide-menu>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		<side-slide-menu v-show="passwordprotect" v-on:close="passwordprotect = 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 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>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		<!-- Show side shades if user is on desktop only -->
 | 
							<!-- Show side shades if user is on desktop only -->
 | 
				
			||||||
		<div class="full-focus-shade shade1"
 | 
							<div class="full-focus-shade shade1"
 | 
				
			||||||
			:class="{ 'slide-out-left':(sizeDown == true) }"
 | 
								:class="{ 'slide-out-left':(sizeDown == true) }"
 | 
				
			||||||
@@ -1206,6 +1147,10 @@
 | 
				
			|||||||
						updated: this.updated
 | 
											updated: this.updated
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										console.log('Focus regained with note open.')
 | 
				
			||||||
 | 
										console.log('Attempting to fix diff text. fix this. Search spleen')
 | 
				
			||||||
 | 
										return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					axios.post('/api/note/difftext', postData)
 | 
										axios.post('/api/note/difftext', postData)
 | 
				
			||||||
					.then( ({data}) => {
 | 
										.then( ({data}) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1255,6 +1200,11 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
				this.save().then( result => {
 | 
									this.save().then( result => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										//If note was modified, trigger reindex on close
 | 
				
			||||||
 | 
										if(this.modified){
 | 
				
			||||||
 | 
											axios.post('/api/note/reindex')
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					this.sizeDown = true
 | 
										this.sizeDown = true
 | 
				
			||||||
					//This timeout allows animation to play before closing 
 | 
										//This timeout allows animation to play before closing 
 | 
				
			||||||
					setTimeout(() => {
 | 
										setTimeout(() => {
 | 
				
			||||||
@@ -1488,11 +1438,23 @@
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	.loading-note {
 | 
						.loading-note {
 | 
				
			||||||
		position: absolute;
 | 
							position: absolute;
 | 
				
			||||||
		top: 0;
 | 
							top: 20%;
 | 
				
			||||||
		left: 0;
 | 
							left: 20%;
 | 
				
			||||||
		right: 0;
 | 
							right: 20%;
 | 
				
			||||||
		bottom: 0;
 | 
							bottom: 20%;
 | 
				
			||||||
 | 
							background: transparent;
 | 
				
			||||||
 | 
							color: #5e6268;;
 | 
				
			||||||
 | 
							font-size: 1.3em;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						.loading-text {
 | 
				
			||||||
 | 
							margin: 0;
 | 
				
			||||||
 | 
						    position: absolute;
 | 
				
			||||||
 | 
						    top: 50%;
 | 
				
			||||||
 | 
						    left: 50%;
 | 
				
			||||||
 | 
						    margin-right: -50%;
 | 
				
			||||||
 | 
						    transform: translate(-50%, -50%);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/* One note open, in the middle of the screen */
 | 
						/* One note open, in the middle of the screen */
 | 
				
			||||||
	.master-note-edit.position-0 {
 | 
						.master-note-edit.position-0 {
 | 
				
			||||||
		left: 50%;
 | 
							left: 50%;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -68,7 +68,7 @@
 | 
				
			|||||||
			<div class="tool-bar" @click.self="cardClicked">
 | 
								<div class="tool-bar" @click.self="cardClicked">
 | 
				
			||||||
				<div class="icon-bar">
 | 
									<div class="icon-bar">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<!-- 					<span v-if="note.pinned == 1" data-position="top left" data-tooltip="Pinned" data-inverted>
 | 
					<!-- 				<span v-if="note.pinned == 1" data-position="top left" data-tooltip="Pinned" data-inverted>
 | 
				
			||||||
						<i class="green pin icon"></i>
 | 
											<i class="green pin icon"></i>
 | 
				
			||||||
					</span>
 | 
										</span>
 | 
				
			||||||
					<span v-if="note.archived == 1" data-position="top left" data-tooltip="Archived" data-inverted>
 | 
										<span v-if="note.archived == 1" data-position="top left" data-tooltip="Archived" data-inverted>
 | 
				
			||||||
@@ -80,7 +80,7 @@
 | 
				
			|||||||
						<br>
 | 
											<br>
 | 
				
			||||||
					</span>
 | 
										</span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					<span data-tooltip="Edited" class="time-ago-display" :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }">
 | 
										<span class="time-ago-display" :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }">
 | 
				
			||||||
						{{$helpers.timeAgo(note.updated)}}
 | 
											{{$helpers.timeAgo(note.updated)}}
 | 
				
			||||||
					</span>
 | 
										</span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,7 +13,7 @@
 | 
				
			|||||||
		<div class="ui form" v-if="!$store.getters.getIsUserOnMobile">
 | 
							<div class="ui form" v-if="!$store.getters.getIsUserOnMobile">
 | 
				
			||||||
			<!-- normal search menu  -->
 | 
								<!-- normal search menu  -->
 | 
				
			||||||
			<div class="ui left icon fluid input">
 | 
								<div class="ui left icon fluid input">
 | 
				
			||||||
				<input v-model="searchTerm" @keyup="searchKeyUp" @keyup.enter="search" placeholder="Search Notes and Files" ref="searchInput"/>
 | 
									<input v-model="searchTerm" @keyup.enter="search" placeholder="Search Notes and Files" ref="searchInput"/>
 | 
				
			||||||
				<i class="search icon"></i>
 | 
									<i class="search icon"></i>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -80,12 +80,14 @@
 | 
				
			|||||||
						
 | 
											
 | 
				
			||||||
						const token = response.data.token
 | 
											const token = response.data.token
 | 
				
			||||||
						const username = response.data.username
 | 
											const username = response.data.username
 | 
				
			||||||
 | 
											const masterKey = response.data.masterKey
 | 
				
			||||||
 | 
					
 | 
				
			||||||
						vm.$store.commit('setLoginToken', {token, username})
 | 
											vm.$store.commit('setLoginToken', {token, username, masterKey})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
						//Redirect user to notes section after login
 | 
											//Redirect user to notes section after login
 | 
				
			||||||
						vm.$router.push('/notes')
 | 
											vm.$router.push('/notes')
 | 
				
			||||||
					} else {
 | 
										} else {
 | 
				
			||||||
 | 
											// this.password = ''
 | 
				
			||||||
						this.$bus.$emit('notification', 'Incorrect Username or Password')
 | 
											this.$bus.$emit('notification', 'Incorrect Username or Password')
 | 
				
			||||||
						vm.$store.commit('destroyLoginToken')
 | 
											vm.$store.commit('destroyLoginToken')
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -64,7 +64,7 @@
 | 
				
			|||||||
			<h2 v-if="fastFilters['withTags'] == 1">Notes with Tags</h2>
 | 
								<h2 v-if="fastFilters['withTags'] == 1">Notes with Tags</h2>
 | 
				
			||||||
			<h2 v-if="fastFilters['onlyArchived'] == 1">Archived Notes</h2>
 | 
								<h2 v-if="fastFilters['onlyArchived'] == 1">Archived Notes</h2>
 | 
				
			||||||
			<h2 v-if="fastFilters['onlyShowSharedNotes'] == 1">Shared Notes</h2>
 | 
								<h2 v-if="fastFilters['onlyShowSharedNotes'] == 1">Shared Notes</h2>
 | 
				
			||||||
			<h2 v-if="fastFilters['onlyShowEncrypted'] == 1">Password Protected Notes</h2>
 | 
								<h2 v-if="fastFilters['onlyShowEncrypted'] == 1">Password Protected - No longer supported</h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			<!-- Note title card display  -->
 | 
								<!-- Note title card display  -->
 | 
				
			||||||
			<div class="sixteen wide column">
 | 
								<div class="sixteen wide column">
 | 
				
			||||||
@@ -154,14 +154,15 @@
 | 
				
			|||||||
				highlights: [],
 | 
									highlights: [],
 | 
				
			||||||
				searchDebounce: null,
 | 
									searchDebounce: null,
 | 
				
			||||||
				fastFilters: {},
 | 
									fastFilters: {},
 | 
				
			||||||
				working: false,
 | 
					
 | 
				
			||||||
				//Load up notes in batches
 | 
									//Load up notes in batches
 | 
				
			||||||
				firstLoadBatchSize: 30, //First set of rapidly loaded notes
 | 
									firstLoadBatchSize: 10, //First set of rapidly loaded notes
 | 
				
			||||||
				batchSize: 100, //Size of batch loaded when user scrolls through current batch
 | 
									batchSize: 25, //Size of batch loaded when user scrolls through current batch
 | 
				
			||||||
				batchOffset: 0, //Tracks the current batch that has been loaded
 | 
									batchOffset: 0, //Tracks the current batch that has been loaded
 | 
				
			||||||
				loadingBatchTimeout: null, //Limit how quickly batches can be loaded
 | 
									loadingBatchTimeout: null, //Limit how quickly batches can be loaded
 | 
				
			||||||
				loadingInProgress: false,
 | 
									loadingInProgress: false,
 | 
				
			||||||
				fetchTags: false,
 | 
									fetchTags: false,
 | 
				
			||||||
 | 
									scrollLoadEnabled: true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				//Clear button is not visible 
 | 
									//Clear button is not visible 
 | 
				
			||||||
				showClear: false,
 | 
									showClear: false,
 | 
				
			||||||
@@ -238,8 +239,8 @@
 | 
				
			|||||||
						}
 | 
											}
 | 
				
			||||||
					})
 | 
										})
 | 
				
			||||||
				})	
 | 
									})	
 | 
				
			||||||
				
 | 
					 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			this.$bus.$on('update_fast_filters', newFilter => {
 | 
								this.$bus.$on('update_fast_filters', newFilter => {
 | 
				
			||||||
				this.fastFilters = newFilter
 | 
									this.fastFilters = newFilter
 | 
				
			||||||
				//Fast filters always return all the results and tags
 | 
									//Fast filters always return all the results and tags
 | 
				
			||||||
@@ -254,7 +255,8 @@
 | 
				
			|||||||
				this.search(true, this.batchSize)
 | 
									this.search(true, this.batchSize)
 | 
				
			||||||
					.then( () => {
 | 
										.then( () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
						this.searchAttachments()
 | 
											console.log('Search attachments disabled for now')
 | 
				
			||||||
 | 
											// this.searchAttachments()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
						return this.fetchUserTags()
 | 
											return this.fetchUserTags()
 | 
				
			||||||
					})
 | 
										})
 | 
				
			||||||
@@ -275,6 +277,7 @@
 | 
				
			|||||||
				const id = this.$route.params.id
 | 
									const id = this.$route.params.id
 | 
				
			||||||
				this.openNote(id)
 | 
									this.openNote(id)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			window.addEventListener('scroll', this.onScroll)
 | 
								window.addEventListener('scroll', this.onScroll)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			//Close notes when back button is pressed
 | 
								//Close notes when back button is pressed
 | 
				
			||||||
@@ -411,7 +414,7 @@
 | 
				
			|||||||
					const percentageDown = Math.round( (bottomOfWindow/offsetHeight)*100 )
 | 
										const percentageDown = Math.round( (bottomOfWindow/offsetHeight)*100 )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					//If greater than 80 of the way down the page, load the next batch
 | 
										//If greater than 80 of the way down the page, load the next batch
 | 
				
			||||||
					if(percentageDown >= 80){
 | 
										if(percentageDown >= 65 && this.scrollLoadEnabled){
 | 
				
			||||||
						
 | 
											
 | 
				
			||||||
						this.search(false, this.batchSize, true)
 | 
											this.search(false, this.batchSize, true)
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
@@ -455,7 +458,7 @@
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			visibiltyChangeAction(event){
 | 
								visibiltyChangeAction(event){
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				//@TODO - set a timeout on this like 2 minutes or just dont do shit and update it via socket.io
 | 
									//@TODO - phase this out, update it via socket.io
 | 
				
			||||||
				//If user leaves page then returns to page, reload the first batch
 | 
									//If user leaves page then returns to page, reload the first batch
 | 
				
			||||||
				if(this.lastVisibilityState == 'hidden' && document.visibilityState == 'visible'){
 | 
									if(this.lastVisibilityState == 'hidden' && document.visibilityState == 'visible'){
 | 
				
			||||||
					//Load initial batch, then tags, then other batch
 | 
										//Load initial batch, then tags, then other batch
 | 
				
			||||||
@@ -589,12 +592,18 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
					//Perform search - or die
 | 
										//Perform search - or die
 | 
				
			||||||
					this.loadingInProgress = true
 | 
										this.loadingInProgress = true
 | 
				
			||||||
 | 
										console.time('Fetch TitleCard Batch '+notesInNextLoad)
 | 
				
			||||||
					axios.post('/api/note/search', postData)
 | 
										axios.post('/api/note/search', postData)
 | 
				
			||||||
					.then(response => {
 | 
										.then(response => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											console.timeEnd('Fetch TitleCard Batch '+notesInNextLoad)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
						//Save the number of notes just loaded
 | 
											//Save the number of notes just loaded
 | 
				
			||||||
						this.batchOffset += response.data.notes.length
 | 
											this.batchOffset += response.data.notes.length
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											//Enable or disable scroll loading
 | 
				
			||||||
 | 
											this.scrollLoadEnabled = response.data.notes.length > 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
						//Mush the two new sets of data together (set will be empty is reset is on)
 | 
											//Mush the two new sets of data together (set will be empty is reset is on)
 | 
				
			||||||
						if(response.data.tags.length > 0){
 | 
											if(response.data.tags.length > 0){
 | 
				
			||||||
							this.commonTags = response.data.tags
 | 
												this.commonTags = response.data.tags
 | 
				
			||||||
@@ -666,6 +675,7 @@
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
			reset(){
 | 
								reset(){
 | 
				
			||||||
				this.showClear = false
 | 
									this.showClear = false
 | 
				
			||||||
 | 
									this.scrollLoadEnabled = true
 | 
				
			||||||
				this.searchTerm = ''
 | 
									this.searchTerm = ''
 | 
				
			||||||
				this.searchTags = []
 | 
									this.searchTags = []
 | 
				
			||||||
				this.fastFilters = {}
 | 
									this.fastFilters = {}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -87,6 +87,7 @@ export default new Vuex.Store({
 | 
				
			|||||||
  			})(navigator.userAgent||navigator.vendor||window.opera, state);
 | 
					  			})(navigator.userAgent||navigator.vendor||window.opera, state);
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		toggleNoteSettingsPane(state){
 | 
							toggleNoteSettingsPane(state){
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			state.isNoteSettingsOpen = !state.isNoteSettingsOpen
 | 
								state.isNoteSettingsOpen = !state.isNoteSettingsOpen
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		setSocketIoSocket(state, socket){
 | 
							setSocketIoSocket(state, socket){
 | 
				
			||||||
@@ -103,7 +104,6 @@ export default new Vuex.Store({
 | 
				
			|||||||
			// 	console.log(key + ' -- ' + totalsObject[key])
 | 
								// 	console.log(key + ' -- ' + totalsObject[key])
 | 
				
			||||||
			// })
 | 
								// })
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	getters: {
 | 
						getters: {
 | 
				
			||||||
		getUsername: state => {
 | 
							getUsername: state => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,8 +4,8 @@ let Auth = {}
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const tokenSecretKey = process.env.JSON_KEY
 | 
					const tokenSecretKey = process.env.JSON_KEY
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Auth.createToken = (userId) => {
 | 
					Auth.createToken = (userId, masterKey) => {
 | 
				
			||||||
	const signedData = {'id': userId, 'date':Date.now()}
 | 
						const signedData = {'id':userId, 'date':Date.now(), 'masterKey':masterKey}
 | 
				
			||||||
	const token = jwt.sign(signedData, tokenSecretKey)
 | 
						const token = jwt.sign(signedData, tokenSecretKey)
 | 
				
			||||||
	return token
 | 
						return token
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -69,6 +69,10 @@ CryptoString.createSalt = () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	return crypto.randomBytes(SALT_BYTE_SIZE).toString('base64')
 | 
						return crypto.randomBytes(SALT_BYTE_SIZE).toString('base64')
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					CryptoString.createSmallSalt = () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return crypto.randomBytes(20).toString('base64')
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CryptoString.hash = (hashString) => {
 | 
					CryptoString.hash = (hashString) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -124,6 +124,7 @@ app.use(function(req, res, next){
 | 
				
			|||||||
		Auth.decodeToken(token)
 | 
							Auth.decodeToken(token)
 | 
				
			||||||
		.then(userData => {
 | 
							.then(userData => {
 | 
				
			||||||
			req.headers.userId = userData.id //Update headers for the rest of the application
 | 
								req.headers.userId = userData.id //Update headers for the rest of the application
 | 
				
			||||||
 | 
								req.headers.masterKey = userData.masterKey
 | 
				
			||||||
			next()
 | 
								next()
 | 
				
			||||||
		}).catch(error => {
 | 
							}).catch(error => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -135,17 +136,11 @@ app.use(function(req, res, next){
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Testing Area
 | 
					
 | 
				
			||||||
// let att = require('@models/Attachment')
 | 
					// Test Area
 | 
				
			||||||
// let testUrl = 'https://dba.stackexchange.com/questions/23908/how-to-search-a-mysql-database-with-encrypted-fields'
 | 
					// ->  right here
 | 
				
			||||||
// testUrl = 'https://www.solidscribe.com/#/'
 | 
					// Test Area
 | 
				
			||||||
// console.log('About to scrape: ', testUrl)
 | 
					
 | 
				
			||||||
// att.processUrl(61, 3213, testUrl)
 | 
					 | 
				
			||||||
// .then(results => {
 | 
					 | 
				
			||||||
// 	console.log('Scrape happened')
 | 
					 | 
				
			||||||
// })
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
//Test 
 | 
					//Test 
 | 
				
			||||||
app.get(prefix, (req, res) => res.send('The api is running'))
 | 
					app.get(prefix, (req, res) => res.send('The api is running'))
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,105 +16,95 @@ let Note = module.exports = {}
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const gm = require('gm')
 | 
					const gm = require('gm')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --------------
 | 
					//User doesn't have an encrypted note set. Encrypt all notes
 | 
				
			||||||
 | 
					Note.encryptEveryNote = (userId, masterKey) => {
 | 
				
			||||||
Note.migrateNoteTextToNewTable = () => {
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return new Promise((resolve, reject) => {
 | 
						return new Promise((resolve, reject) => {
 | 
				
			||||||
		db.promise()
 | 
					
 | 
				
			||||||
		.query('SELECT id, text FROM note WHERE note_raw_text_id IS NULL')
 | 
							//Select all the user notes
 | 
				
			||||||
 | 
							db.promise().query(`
 | 
				
			||||||
 | 
								SELECT * FROM note 
 | 
				
			||||||
 | 
								JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
 | 
				
			||||||
 | 
								WHERE salt IS NULL AND user_id = ? AND encrypted = 0 AND shared = 0`, [userId])
 | 
				
			||||||
		.then((rows, fields) => {
 | 
							.then((rows, fields) => {
 | 
				
			||||||
			rows[0].forEach( ({id, text}) => {
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
				db.promise()
 | 
								let foundNotes = rows[0]
 | 
				
			||||||
				.query('INSERT INTO note_raw_text (text) VALUES (?)', [text])
 | 
								console.log('Encrypting user notes ',rows[0].length)
 | 
				
			||||||
				.then((rows, fields) => {
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
					db.promise()
 | 
								// return resolve(true)
 | 
				
			||||||
					.query(`UPDATE note SET note_raw_text_id = ? WHERE (id = ?)`, [rows[0].insertId, id])
 | 
					 | 
				
			||||||
					.then((rows, fields) => {
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
						return 'Nice'
 | 
								let allTheUpdates = []
 | 
				
			||||||
					})
 | 
								let timeoutAdder = 0
 | 
				
			||||||
				})
 | 
								foundNotes.forEach(note => {
 | 
				
			||||||
 | 
									timeoutAdder += 100
 | 
				
			||||||
 | 
									const newUpdate = new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
										setTimeout(() => {
 | 
				
			||||||
 | 
											console.log('Encrypting Note ', note.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			})
 | 
											const created = Math.round((+new Date)/1000)
 | 
				
			||||||
 | 
											const salt = cs.createSmallSalt()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			resolve('Its probably running... :-D')
 | 
											const noteText = note.text
 | 
				
			||||||
		})
 | 
											const noteTitle = note.title
 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
Note.fixAttachmentThumbnails = () => {
 | 
											const snippet = JSON.stringify([noteTitle, noteText.substring(0, 500)])
 | 
				
			||||||
	const filePath = '../staticFiles/'
 | 
											const noteSnippet = cs.encrypt(masterKey, salt, snippet)
 | 
				
			||||||
	db.promise()
 | 
					 | 
				
			||||||
		.query(`SELECT * FROM attachment WHERE file_location NOT LIKE "%.%"`)
 | 
					 | 
				
			||||||
		.then( (rows, fields) => {
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			rows[0].forEach(line => {
 | 
											const textObject = JSON.stringify([noteTitle, noteText])
 | 
				
			||||||
 | 
											const encryptedText = cs.encrypt(masterKey, salt, textObject)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const rawFilename = line['file_location']
 | 
											db.promise()
 | 
				
			||||||
				const goodFileName = rawFilename+'.jpg'
 | 
											.query('UPDATE note_raw_text SET title = ?, text = ?, snippet = ?, salt = ? WHERE id = ?', 
 | 
				
			||||||
 | 
											[null, encryptedText, noteSnippet, salt, note.note_raw_text_id])
 | 
				
			||||||
				//Rename file to have jpg extension, create thumbnail, update database
 | 
											.then(() => {
 | 
				
			||||||
				fs.rename(filePath+rawFilename, filePath+goodFileName, (err) => {
 | 
												resolve(true)
 | 
				
			||||||
 | 
					 | 
				
			||||||
					db.promise()
 | 
					 | 
				
			||||||
						.query(`UPDATE attachment SET file_location = ? WHERE id = ?`,[goodFileName, line['id'] ])
 | 
					 | 
				
			||||||
						.then( (rows, fields) => {
 | 
					 | 
				
			||||||
							gm(filePath+goodFileName)
 | 
					 | 
				
			||||||
							.resize(550) //Resize to width of 550 px 
 | 
					 | 
				
			||||||
							.quality(75) //compression level 0 - 100 (best)
 | 
					 | 
				
			||||||
							.write(filePath + 'thumb_'+goodFileName, function (err) {
 | 
					 | 
				
			||||||
								console.log('Done for -> ', goodFileName)
 | 
					 | 
				
			||||||
							})
 | 
					 | 
				
			||||||
						})
 | 
											})
 | 
				
			||||||
 | 
										}, timeoutAdder)
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
 | 
									allTheUpdates.push(newUpdate)
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
		})
 | 
					
 | 
				
			||||||
}
 | 
								Promise.all(allTheUpdates).then(done => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Note.stressTest = () => {
 | 
									console.log('Indexing first 100')
 | 
				
			||||||
	return new Promise((resolve, reject) => {
 | 
									return Note.reindex(userId, masterKey)
 | 
				
			||||||
		db.promise()
 | 
					
 | 
				
			||||||
		.query(`
 | 
								}).then(results => {
 | 
				
			||||||
		
 | 
					
 | 
				
			||||||
			SELECT text FROM note;
 | 
									console.log('Done')
 | 
				
			||||||
 | 
									resolve(true)
 | 
				
			||||||
		`)
 | 
					 | 
				
			||||||
		.then((rows, fields) => {
 | 
					 | 
				
			||||||
			console.log()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			rows[0].forEach(item => {
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
				Note.create(68, item['text'])
 | 
					 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			resolve(true)
 | 
					 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
		.catch(console.log)	
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --------------
 | 
					Note.create = (userId, noteTitle, noteText, masterKey) => {
 | 
				
			||||||
 | 
					 | 
				
			||||||
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') }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const created = Math.round((+new Date)/1000)
 | 
							const created = Math.round((+new Date)/1000)
 | 
				
			||||||
 | 
							const salt = cs.createSmallSalt()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const textObject = JSON.stringify([noteTitle, noteText])
 | 
				
			||||||
 | 
							const encryptedText = cs.encrypt(masterKey, salt, textObject)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		db.promise()
 | 
							db.promise()
 | 
				
			||||||
		.query(`INSERT INTO note_raw_text (text, title, updated) VALUE (?, ?, ?)`, [noteText, noteTitle, created])
 | 
							.query(`INSERT INTO note_raw_text (text, salt, updated) VALUE (?, ?, ?)`, [encryptedText, salt, created])
 | 
				
			||||||
		.then( (rows, fields) => {
 | 
							.then( (rows, fields) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const rawTextId = rows[0].insertId
 | 
								const rawTextId = rows[0].insertId
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			return db.promise()
 | 
								return db.promise()
 | 
				
			||||||
			.query('INSERT INTO note (user_id, note_raw_text_id, created, quick_note) VALUES (?,?,?,?)', 
 | 
								.query('INSERT INTO note (user_id, note_raw_text_id, created, quick_note) VALUES (?,?,?,?)', 
 | 
				
			||||||
			[userId, rawTextId, created, quickNote])
 | 
								[userId, rawTextId, created, 0])
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
		.then((rows, fields) => {
 | 
							.then((rows, fields) => {
 | 
				
			||||||
			// Indexing is done on save
 | 
								// Indexing is done on save
 | 
				
			||||||
@@ -124,9 +114,193 @@ Note.create = (userId, noteTitle, noteText, quickNote = 0, ) => {
 | 
				
			|||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Note.reindex = (userId, noteId) => {
 | 
					Note.reindex = (userId, masterKey) => {
 | 
				
			||||||
	return new Promise((resolve, reject) => {
 | 
						return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if(!masterKey || masterKey.length == 0){
 | 
				
			||||||
 | 
								return reject('Master key needed for reindex')
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							let notIndexedNoteIds = []
 | 
				
			||||||
 | 
							let searchIndex = null
 | 
				
			||||||
 | 
							let searchIndexSalt = null
 | 
				
			||||||
 | 
							let foundNotes = null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							//First check if we have any notes to index
 | 
				
			||||||
 | 
							db.promise().query(`
 | 
				
			||||||
 | 
								SELECT note.id, text, salt FROM note
 | 
				
			||||||
 | 
								JOIN note_raw_text ON note.note_raw_text_id = note_raw_text.id
 | 
				
			||||||
 | 
								WHERE indexed = 0 AND encrypted = 0 AND salt IS NOT NULL
 | 
				
			||||||
 | 
								AND user_id = ? LIMIT 100`, [userId])
 | 
				
			||||||
 | 
							.then((rows, fields) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								//Halt execution if there are no new notes
 | 
				
			||||||
 | 
								foundNotes = rows[0]
 | 
				
			||||||
 | 
								if(foundNotes.length == 0){
 | 
				
			||||||
 | 
									throw new Error('No new notes to index')
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								//Select search index, if it doesn't exist, create it
 | 
				
			||||||
 | 
								return db.promise().query(`SELECT * FROM user_encrypted_search_index WHERE user_id = ? LIMIT 1`, [userId])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							.then((rows, fields) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if(rows[0].length == 0){
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									console.log('Creating a new index')
 | 
				
			||||||
 | 
									//Create search index entry, return an object
 | 
				
			||||||
 | 
									searchIndexSalt = cs.createSmallSalt()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									//Select all user notes to recreate index
 | 
				
			||||||
 | 
									return db.promise().query(`
 | 
				
			||||||
 | 
										SELECT note.id, text, salt FROM note
 | 
				
			||||||
 | 
										JOIN note_raw_text ON note.note_raw_text_id = note_raw_text.id
 | 
				
			||||||
 | 
										WHERE encrypted = 0 AND user_id = ?`, [userId])
 | 
				
			||||||
 | 
									.then((rows, fields) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										foundNotes = rows[0]
 | 
				
			||||||
 | 
										return db.promise().query("INSERT INTO user_encrypted_search_index (`user_id`, `salt`) VALUES (?,?)", [userId, searchIndexSalt])
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
									.then((rows, fields) => {
 | 
				
			||||||
 | 
										//return a fresh search index
 | 
				
			||||||
 | 
										return new Promise((resolve, reject) => { resolve('{}') })
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									const row = rows[0][0]
 | 
				
			||||||
 | 
									searchIndexSalt = row.salt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									//Decrypt search index and continue.
 | 
				
			||||||
 | 
									let decipheredSearchIndex = '{}'
 | 
				
			||||||
 | 
									if(row.index && row.index.length > 0){
 | 
				
			||||||
 | 
										//Decrypt json, do not parse json yet, we want raw text
 | 
				
			||||||
 | 
										decipheredSearchIndex = cs.decrypt(masterKey, searchIndexSalt, row.index)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									
 | 
				
			||||||
 | 
									return new Promise((resolve, reject) => { resolve( decipheredSearchIndex ) })
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							.then(rawSearchIndex => {
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
								searchIndex = rawSearchIndex
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								//Remove all instances of IDs from text
 | 
				
			||||||
 | 
								foundNotes.forEach(note => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									notIndexedNoteIds.push(note.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									//Remove every instance of note id
 | 
				
			||||||
 | 
									const removeId = new RegExp(note.id,"gm")
 | 
				
			||||||
 | 
									const removeDoubles = new RegExp(',,',"g")
 | 
				
			||||||
 | 
									// const removeTrail = new RegExp(',]',"g")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									searchIndex = searchIndex
 | 
				
			||||||
 | 
										.replace(removeId, '')
 | 
				
			||||||
 | 
										.replace(removeDoubles, ',')
 | 
				
			||||||
 | 
										.replace(/,]/g, ']')
 | 
				
			||||||
 | 
										.replace(/\[\,/g, '[') //search [,
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								searchIndex = JSON.parse(searchIndex)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								//Remove unused words, this may not be needed and it increases overhead
 | 
				
			||||||
 | 
								Object.keys(searchIndex).forEach(word => {
 | 
				
			||||||
 | 
									if(searchIndex[word].length == 0){
 | 
				
			||||||
 | 
										delete searchIndex[word]
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								//Process text of each note and add it to the index
 | 
				
			||||||
 | 
								let reindexQueue = []
 | 
				
			||||||
 | 
								let reindexTimer = 0
 | 
				
			||||||
 | 
								foundNotes.forEach(note => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									reindexTimer += 50
 | 
				
			||||||
 | 
									let reindexPromise = new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
										setTimeout(() => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											if(masterKey == null || note.salt == null){
 | 
				
			||||||
 | 
												console.log('Error indexing note', note.id)
 | 
				
			||||||
 | 
												return resolve(true)
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											const noteHtml = cs.decrypt(masterKey, note.salt, note.text)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											const rawText =  
 | 
				
			||||||
 | 
											ProcessText.removeHtml(noteHtml) //Remove HTML
 | 
				
			||||||
 | 
												.toLowerCase() 
 | 
				
			||||||
 | 
												.replace(/style=".*?"/g,'') //Remove inline styles
 | 
				
			||||||
 | 
												.replace (/&#{0,1}[a-z0-9]+;/ig, '') //remove HTML entities
 | 
				
			||||||
 | 
												.replace(/[^A-Za-z0-9]/g, ' ') //Convert all to a-z only
 | 
				
			||||||
 | 
												.replace(/ +(?= )/g,'') //Remove double spaces
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											rawText.split(' ').forEach(word => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
												//Skip small words
 | 
				
			||||||
 | 
												if(word.length <= 2){ return }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
												if(Array.isArray( searchIndex[word] )){
 | 
				
			||||||
 | 
													if(searchIndex[word].indexOf( note.id ) == -1){
 | 
				
			||||||
 | 
														searchIndex[word].push( note.id )
 | 
				
			||||||
 | 
													}
 | 
				
			||||||
 | 
												} else {
 | 
				
			||||||
 | 
													searchIndex[word] = [ note.id ]
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
											})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											return resolve(true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										}, reindexTimer)
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									reindexQueue.push(reindexPromise)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return Promise.all(reindexQueue)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							.then(rawSearchIndex => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								console.log('All notes indexed')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const created = Math.round((+new Date)/1000)
 | 
				
			||||||
 | 
								const jsonSearchIndex = JSON.stringify(searchIndex)
 | 
				
			||||||
 | 
								const encryptedJsonIndex = cs.encrypt(masterKey, searchIndexSalt, jsonSearchIndex)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return db.promise().query("UPDATE user_encrypted_search_index SET `index` = ?, `last_update` = ? WHERE (`user_id` = ?) LIMIT 1", 
 | 
				
			||||||
 | 
									[encryptedJsonIndex, created, userId])
 | 
				
			||||||
 | 
								.then((rows, fields) => {
 | 
				
			||||||
 | 
									
 | 
				
			||||||
 | 
									return db.promise().query('UPDATE note SET `indexed` = 1 WHERE (`id` IN (?))', [notIndexedNoteIds])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								.then((rows, fields) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									console.log('Indexd Note Count: ' + rows[0]['affectedRows'])
 | 
				
			||||||
 | 
									resolve(true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							}).catch(error => {
 | 
				
			||||||
 | 
								console.log('Reindex Error')
 | 
				
			||||||
 | 
								console.log(error)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							//Find all note Ids that need to be reindexed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// return resolve(true)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		Note.get(userId, noteId)
 | 
							Note.get(userId, noteId)
 | 
				
			||||||
		.then(note => {
 | 
							.then(note => {
 | 
				
			||||||
			
 | 
								
 | 
				
			||||||
@@ -163,15 +337,9 @@ Note.reindex = (userId, noteId) => {
 | 
				
			|||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Note.update = (io, userId, noteId, noteText, noteTitle, color, pinned, archived, password = '', passwordHint = '') => {
 | 
					Note.update = (io, userId, noteId, noteText, noteTitle, color, pinned, archived, password = '', passwordHint = '', masterKey) => {
 | 
				
			||||||
	return new Promise((resolve, reject) => {
 | 
						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)
 | 
					 | 
				
			||||||
		//}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		const now = Math.round((+new Date)/1000)
 | 
							const now = Math.round((+new Date)/1000)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		db.promise()
 | 
							db.promise()
 | 
				
			||||||
@@ -183,11 +351,7 @@ Note.update = (io, userId, noteId, noteText, noteTitle, color, pinned, archived,
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			const textId = rows[0][0]['note_raw_text_id']
 | 
								const textId = rows[0][0]['note_raw_text_id']
 | 
				
			||||||
			let salt = rows[0][0]['salt']
 | 
								let salt = rows[0][0]['salt']
 | 
				
			||||||
 | 
								let noteSnippet = ''
 | 
				
			||||||
			//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 a password is set, create a salt
 | 
				
			||||||
			if(password.length > 3 && !salt){
 | 
								if(password.length > 3 && !salt){
 | 
				
			||||||
@@ -206,11 +370,20 @@ Note.update = (io, userId, noteId, noteText, noteTitle, color, pinned, archived,
 | 
				
			|||||||
				//
 | 
									//
 | 
				
			||||||
				// @TODO - Do note save data if encryption goes wrong, do some validation
 | 
									// @TODO - Do note save data if encryption goes wrong, do some validation
 | 
				
			||||||
				//
 | 
									//
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									//Create encrypted snippet
 | 
				
			||||||
 | 
									const snippet = JSON.stringify([noteTitle, noteText.substring(0, 500)])
 | 
				
			||||||
 | 
									noteSnippet = cs.encrypt(masterKey, salt, snippet)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									//Encrypt note text
 | 
				
			||||||
 | 
									const textObject = JSON.stringify([noteTitle, noteText])
 | 
				
			||||||
 | 
									noteText = cs.encrypt(masterKey, salt, textObject)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			//Update Note text
 | 
								//Update Note text
 | 
				
			||||||
			return db.promise()
 | 
								return db.promise()
 | 
				
			||||||
			.query('UPDATE note_raw_text SET text = ?, title = ?, updated = ?, salt = ? WHERE id = ?', [noteText, noteTitle, now, salt, textId])
 | 
								.query('UPDATE note_raw_text SET text = ?, snippet = ? ,updated = ?, salt = ? WHERE id = ?', [noteText, noteSnippet, now, salt, textId])
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
		.then( (rows, fields) => {
 | 
							.then( (rows, fields) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -218,14 +391,14 @@ Note.update = (io, userId, noteId, noteText, noteTitle, color, pinned, archived,
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			//Update other note attributes
 | 
								//Update other note attributes
 | 
				
			||||||
			return db.promise()
 | 
								return db.promise()
 | 
				
			||||||
			.query('UPDATE note SET pinned = ?, archived = ?, color = ?, encrypted = ? WHERE id = ? AND user_id = ? LIMIT 1', 
 | 
								.query('UPDATE note SET pinned = ?, archived = ?, color = ?, encrypted = ?, indexed = 0 WHERE id = ? AND user_id = ? LIMIT 1', 
 | 
				
			||||||
			[pinned, archived, color, encrypted, noteId, userId])
 | 
								[pinned, archived, color, encrypted, noteId, userId])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
		.then((rows, fields) => {
 | 
							.then((rows, fields) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			//Async solr note reindex
 | 
								//Async solr note reindex
 | 
				
			||||||
			Note.reindex(userId, noteId)
 | 
								// Note.reindex(userId, noteId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			//Async attachment reindex
 | 
								//Async attachment reindex
 | 
				
			||||||
			Attachment.scanTextForWebsites(io, userId, noteId, noteText)
 | 
								Attachment.scanTextForWebsites(io, userId, noteId, noteText)
 | 
				
			||||||
@@ -392,12 +565,16 @@ Note.getDiffText = (userId, noteId, usersCurrentText, lastUpdated) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Note.get = (userId, noteId, password = '') => {
 | 
					Note.get = (userId, noteId, password = '', masterKey) => {
 | 
				
			||||||
	return new Promise((resolve, reject) => {
 | 
						return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if(!masterKey || masterKey.length == 0){
 | 
				
			||||||
 | 
								return reject('Get note called without master key')
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		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.salt, 
 | 
				
			||||||
				note_raw_text.password_hint,
 | 
									note_raw_text.password_hint,
 | 
				
			||||||
@@ -482,6 +659,17 @@ Note.get = (userId, noteId, password = '') => {
 | 
				
			|||||||
					db.promise().query('UPDATE note_raw_text SET decrypt_attempts_count = decrypt_attempts_count +1 WHERE id = ?', [rawTextId ])
 | 
										db.promise().query('UPDATE note_raw_text SET decrypt_attempts_count = decrypt_attempts_count +1 WHERE id = ?', [rawTextId ])
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
								if(noteData.encrypted == 0 && noteData.salt && noteData.salt.length > 0){
 | 
				
			||||||
 | 
									//Normal Encrypted note
 | 
				
			||||||
 | 
									const decipheredText = cs.decrypt(masterKey, noteData.salt, noteData.text)
 | 
				
			||||||
 | 
									if(decipheredText == null){
 | 
				
			||||||
 | 
										throw new Error('Unable to decropt note text')
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									//Parse title and text from encrypted data and update object
 | 
				
			||||||
 | 
									const textObject = JSON.parse(decipheredText)
 | 
				
			||||||
 | 
									noteData.title = textObject[0]
 | 
				
			||||||
 | 
									noteData.text = textObject[1]
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			db.promise().query(`UPDATE note SET opened = ? WHERE (id = ?)`, [nowTime, noteId])
 | 
								db.promise().query(`UPDATE note SET opened = ? WHERE (id = ?)`, [nowTime, noteId])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -511,56 +699,75 @@ Note.getShared = (noteId) => {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Searches text index, returns nothing if there is no search query
 | 
					// Searches text index, returns nothing if there is no search query
 | 
				
			||||||
Note.solrQuery = (userId, searchQuery, searchTags) => {
 | 
					Note.solrQuery = (userId, searchQuery, searchTags, masterKey) => {
 | 
				
			||||||
	return new Promise((resolve, reject) => {
 | 
						return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if(searchQuery.length == 0){
 | 
							if(searchQuery.length == 0){
 | 
				
			||||||
			resolve(null)
 | 
								resolve(null)
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			//Number of characters before and after search word
 | 
								if(!masterKey || masterKey == null){
 | 
				
			||||||
			const front = 20
 | 
									console.log('Attempting to search wiouth key')
 | 
				
			||||||
			const tail = 150
 | 
									return resolve(null)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			db.promise()
 | 
								//Search the search index
 | 
				
			||||||
			.query(`
 | 
								db.promise().query(`SELECT * FROM user_encrypted_search_index WHERE user_id = ? LIMIT 1`, [userId])
 | 
				
			||||||
			
 | 
					 | 
				
			||||||
				SELECT 
 | 
					 | 
				
			||||||
					note_id,
 | 
					 | 
				
			||||||
					substring(
 | 
					 | 
				
			||||||
							text,
 | 
					 | 
				
			||||||
					        IF(LOCATE(?, text) > ${tail}, LOCATE(?, text) - ${front}, 1),
 | 
					 | 
				
			||||||
					        ${tail} + LENGTH(?) + ${front}
 | 
					 | 
				
			||||||
						) as snippet
 | 
					 | 
				
			||||||
				FROM note_text_index 
 | 
					 | 
				
			||||||
				WHERE user_id = ?
 | 
					 | 
				
			||||||
				AND MATCH(text)
 | 
					 | 
				
			||||||
				AGAINST(? IN NATURAL LANGUAGE MODE)
 | 
					 | 
				
			||||||
				LIMIT 1000
 | 
					 | 
				
			||||||
				;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			`, [searchQuery, searchQuery, searchQuery, userId, searchQuery])
 | 
					 | 
				
			||||||
			.then((rows, fields) => {
 | 
								.then((rows, fields) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				let results = []
 | 
									if(rows[0].length == 1){
 | 
				
			||||||
				let snippets = {}
 | 
					
 | 
				
			||||||
				rows[0].forEach(item => {
 | 
										//Lookup,  decrypt and parse search index
 | 
				
			||||||
					let noteId = parseInt(item['note_id'])
 | 
										const row = rows[0][0]
 | 
				
			||||||
					//Setup array of ids to use for query
 | 
										const decipheredSearchIndex = cs.decrypt(masterKey, row.salt, row.index)
 | 
				
			||||||
					results.push( noteId )
 | 
										const searchIndex = JSON.parse(decipheredSearchIndex)
 | 
				
			||||||
					//Get text snippet and highlight the key word
 | 
					
 | 
				
			||||||
					snippets[noteId] = item['snippet'].replace(new RegExp(searchQuery,"ig"), '<em>'+searchQuery+'</em>');
 | 
										//Clean up search word
 | 
				
			||||||
					//.replace(searchQuery,'<em>'+searchQuery+'</em>')
 | 
										const word = searchQuery.toLowerCase().replace(/[^a-z0-9]/g, '')
 | 
				
			||||||
				})
 | 
					
 | 
				
			||||||
 | 
										let noteIds = []
 | 
				
			||||||
 | 
										let partials = []
 | 
				
			||||||
 | 
										Object.keys(searchIndex).forEach(wordIndex => {
 | 
				
			||||||
 | 
											if( wordIndex.indexOf(word) != -1 && wordIndex != word){
 | 
				
			||||||
 | 
												partials.push(wordIndex)
 | 
				
			||||||
 | 
												noteIds.push(...searchIndex[wordIndex])
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										const exactArray = searchIndex[word] ? searchIndex[word] : []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										let searchData = {
 | 
				
			||||||
 | 
											'word':word,
 | 
				
			||||||
 | 
											'exact': exactArray,
 | 
				
			||||||
 | 
											'partials': partials,
 | 
				
			||||||
 | 
											'partial': [...new Set(noteIds) ],
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										//Remove exact matches from partials set if there is overlap
 | 
				
			||||||
 | 
										if(searchData['exact'].length > 0 && searchData['partial'].length > 0){
 | 
				
			||||||
 | 
											searchData['partial'] = searchData['partial']
 | 
				
			||||||
 | 
											.filter( ( el ) => !searchData['exact'].includes( el ) )
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										searchData['ids'] = searchData['exact'].concat(searchData['partial'])
 | 
				
			||||||
 | 
										searchData['total'] = searchData['ids'].length
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										console.log(searchData['total'])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										return resolve({ 'ids':searchData['ids'] })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										return resolve(null)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				resolve({ 'ids':results, 'snippets':snippets })
 | 
					 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Note.search = (userId, searchQuery, searchTags, fastFilters) => {
 | 
					Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
 | 
				
			||||||
	return new Promise((resolve, reject) => {
 | 
						return new Promise((resolve, reject) => {
 | 
				
			||||||
		//Define return data objects
 | 
							//Define return data objects
 | 
				
			||||||
		let returnData = {
 | 
							let returnData = {
 | 
				
			||||||
@@ -568,17 +775,16 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
 | 
				
			|||||||
			'tags':[]
 | 
								'tags':[]
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		Note.solrQuery(userId, searchQuery, searchTags).then( (textSearchResults) => {
 | 
							Note.solrQuery(userId, searchQuery, searchTags, masterKey).then( (textSearchResults) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			//Pull out search results from previous query
 | 
								//Pull out search results from previous query
 | 
				
			||||||
			let textSearchIds = []
 | 
								let textSearchIds = []
 | 
				
			||||||
			let highlights = {}
 | 
					 | 
				
			||||||
			let returnTagResults = false
 | 
								let returnTagResults = false
 | 
				
			||||||
			let searchAllNotes = false
 | 
								let searchAllNotes = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if(textSearchResults != null){
 | 
								if(textSearchResults != null){
 | 
				
			||||||
				textSearchIds = textSearchResults['ids']
 | 
									textSearchIds = textSearchResults['ids']
 | 
				
			||||||
				highlights = textSearchResults['snippets']
 | 
									// highlights = textSearchResults['snippets']
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		    //No results, return empty data
 | 
							    //No results, return empty data
 | 
				
			||||||
@@ -588,11 +794,14 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			// Base of the query, modified with fastFilters
 | 
								// Base of the query, modified with fastFilters
 | 
				
			||||||
			// Add to query for character counts -> CHAR_LENGTH(note.text) as chars
 | 
								// Add to query for character counts -> CHAR_LENGTH(note.text) as chars
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								//SUBSTRING(note_raw_text.text, 1, 500) as text, 
 | 
				
			||||||
			let searchParams = [userId]
 | 
								let searchParams = [userId]
 | 
				
			||||||
			let noteSearchQuery = `
 | 
								let noteSearchQuery = `
 | 
				
			||||||
				SELECT note.id,
 | 
									SELECT note.id,
 | 
				
			||||||
					SUBSTRING(note_raw_text.text, 1, 500) as text, 
 | 
					 | 
				
			||||||
					note_raw_text.title as title,
 | 
										note_raw_text.title as title,
 | 
				
			||||||
 | 
										note_raw_text.snippet as snippet,
 | 
				
			||||||
 | 
										note_raw_text.salt as salt,
 | 
				
			||||||
					note_raw_text.updated as updated, 
 | 
										note_raw_text.updated as updated, 
 | 
				
			||||||
					opened,
 | 
										opened,
 | 
				
			||||||
					color, 
 | 
										color, 
 | 
				
			||||||
@@ -725,17 +934,24 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
 | 
				
			|||||||
					//Grab note ID for finding tags
 | 
										//Grab note ID for finding tags
 | 
				
			||||||
					noteIds.push(note.id)
 | 
										noteIds.push(note.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					if(note.text == null){ note.text = '' }
 | 
										if(note.encrypted == 1){ 
 | 
				
			||||||
					if(note.encrypted == 1){ note.text = '' }
 | 
											note.text = '' 
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										//Decrypt note text
 | 
				
			||||||
 | 
										if(note.snippet && note.salt){
 | 
				
			||||||
 | 
											const decipheredText = cs.decrypt(masterKey, note.salt, note.snippet)
 | 
				
			||||||
 | 
											const textObject = JSON.parse(decipheredText)
 | 
				
			||||||
 | 
											if(textObject != null && textObject.length == 2){
 | 
				
			||||||
 | 
												note.title = textObject[0]
 | 
				
			||||||
 | 
												note.text = textObject[1]
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					//Deduce note title
 | 
										//Deduce note title
 | 
				
			||||||
					const textData = ProcessText.deduceNoteTitle(note.title, note.text)
 | 
										const textData = ProcessText.deduceNoteTitle(note.title, note.text)
 | 
				
			||||||
					// console.log(textData)
 | 
					 | 
				
			||||||
					
 | 
										
 | 
				
			||||||
					note.title = textData.title
 | 
										note.title = textData.title
 | 
				
			||||||
					note.subtext = textData.sub
 | 
										note.subtext = textData.sub
 | 
				
			||||||
					note.titleLength = textData.titleLength
 | 
					 | 
				
			||||||
					note.subtextLength = textData.subtextLength
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
					note.note_highlights = []
 | 
										note.note_highlights = []
 | 
				
			||||||
					note.attachment_highlights = []
 | 
										note.attachment_highlights = []
 | 
				
			||||||
@@ -750,13 +966,10 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
 | 
				
			|||||||
						note.thumbs = thumbArray
 | 
											note.thumbs = thumbArray
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					//Push in search highlights
 | 
					 | 
				
			||||||
					if(highlights && highlights[note.id]){
 | 
					 | 
				
			||||||
						note['note_highlights'] = [highlights[note.id]]
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					//Clear out note.text before sending it to front end, its being used in title and subtext
 | 
										//Clear out note.text before sending it to front end, its being used in title and subtext
 | 
				
			||||||
					delete note.text
 | 
										delete note.text
 | 
				
			||||||
 | 
										delete note.snippet
 | 
				
			||||||
 | 
										delete note.salt
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				//If no notes are returned, there are no tags, return empty
 | 
									//If no notes are returned, there are no tags, return empty
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,10 @@
 | 
				
			|||||||
var crypto = require('crypto')
 | 
					var crypto = require('crypto')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let db = require('@config/database')
 | 
					const Note = require('@models/Note')
 | 
				
			||||||
let Auth = require('@helpers/Auth')
 | 
					
 | 
				
			||||||
 | 
					const db = require('@config/database')
 | 
				
			||||||
 | 
					const Auth = require('@helpers/Auth')
 | 
				
			||||||
 | 
					const cs = require('@helpers/CryptoString')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let User = module.exports = {}
 | 
					let User = module.exports = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -22,26 +25,32 @@ User.login = (username, password) => {
 | 
				
			|||||||
			//User not found, create a new account with set data
 | 
								//User not found, create a new account with set data
 | 
				
			||||||
			if(rows[0].length == 0){
 | 
								if(rows[0].length == 0){
 | 
				
			||||||
				User.create(lowerName, password)
 | 
									User.create(lowerName, password)
 | 
				
			||||||
				.then(loginToken => {
 | 
									.then( ({token, userId}) => {
 | 
				
			||||||
					resolve(loginToken)
 | 
										return resolve({ token, userId })
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
				return
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			//hash the password and check for a match
 | 
								if(lookedUpUser && lookedUpUser.salt){
 | 
				
			||||||
			const salt = new Buffer(lookedUpUser.salt, 'binary')
 | 
									//hash the password and check for a match
 | 
				
			||||||
			crypto.pbkdf2(password, salt, lookedUpUser.iterations, 512, 'sha512', function(err, delivered_key){
 | 
									const salt = new Buffer(lookedUpUser.salt, 'binary')
 | 
				
			||||||
				if(delivered_key.toString('hex') === lookedUpUser.password){
 | 
									crypto.pbkdf2(password, salt, lookedUpUser.iterations, 512, 'sha512', function(err, delivered_key){
 | 
				
			||||||
 | 
										if(delivered_key.toString('hex') === lookedUpUser.password){
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					//Passback a json web token
 | 
											User.generateMasterKey(lookedUpUser.id, password)
 | 
				
			||||||
					const token = Auth.createToken(lookedUpUser.id)
 | 
											.then( result => User.getMasterKey(lookedUpUser.id, password))
 | 
				
			||||||
					resolve(token)
 | 
											.then(masterKey => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				} else {
 | 
												//Passback a json web token
 | 
				
			||||||
 | 
												const token = Auth.createToken(lookedUpUser.id, masterKey)
 | 
				
			||||||
 | 
												resolve({ token: token, userId:lookedUpUser.id })
 | 
				
			||||||
 | 
											})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					reject('Password does not match database')
 | 
										} else {
 | 
				
			||||||
				}
 | 
					
 | 
				
			||||||
			})
 | 
											reject('Password does not match database')
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
		.catch(console.log)
 | 
							.catch(console.log)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -93,9 +102,15 @@ User.create = (username, password) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
						if(rows[0].affectedRows == 1){
 | 
											if(rows[0].affectedRows == 1){
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							const newUserId = rows[0].insertId
 | 
												const userId = rows[0].insertId
 | 
				
			||||||
							const loginToken = Auth.createToken(newUserId)
 | 
					
 | 
				
			||||||
							resolve(loginToken)
 | 
												User.generateMasterKey(userId, password)
 | 
				
			||||||
 | 
												.then( result => User.getMasterKey(userId, password))
 | 
				
			||||||
 | 
												.then(masterKey => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
													const token = Auth.createToken(userId, masterKey)
 | 
				
			||||||
 | 
													return resolve({token, userId})
 | 
				
			||||||
 | 
												})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
						} else {
 | 
											} else {
 | 
				
			||||||
							//Emit Error to user
 | 
												//Emit Error to user
 | 
				
			||||||
@@ -168,3 +183,82 @@ User.getCounts = (userId) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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()
 | 
				
			||||||
 | 
										console.log('Generating new key for user', userId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										//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((rows, fields)=>{
 | 
				
			||||||
 | 
											return Note.encryptEveryNote(userId, masterPassword)
 | 
				
			||||||
 | 
										})
 | 
				
			||||||
 | 
										.then(results => {
 | 
				
			||||||
 | 
											return new Promise((resolve, reject) => { resolve(true) })
 | 
				
			||||||
 | 
										})
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								.then((rows, fields) => {
 | 
				
			||||||
 | 
									return resolve(true)
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								.catch(error => {
 | 
				
			||||||
 | 
									console.log('Create Master Password Error')
 | 
				
			||||||
 | 
									console.log(error)
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					User.getMasterKey = (userId, password) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if(!userId || !password){
 | 
				
			||||||
 | 
							reject('Need userId and password to fetch key')
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							db.promise().query('SELECT * FROM user_key WHERE user_id = ? LIMIT 1', [userId])
 | 
				
			||||||
 | 
							.then((rows, fields) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const row = rows[0][0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const masterKey = cs.decrypt(password, row['salt'], row['key'])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if(masterKey == null){
 | 
				
			||||||
 | 
									return reject('Unable to decrypt key')
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return resolve(masterKey)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -5,15 +5,13 @@ let Notes = require('@models/Note');
 | 
				
			|||||||
let ShareNote = require('@models/ShareNote');
 | 
					let ShareNote = require('@models/ShareNote');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let userId = null
 | 
					let userId = null
 | 
				
			||||||
let socket = null
 | 
					let masterKey = null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// middleware that is specific to this router
 | 
					// middleware that is specific to this router
 | 
				
			||||||
router.use(function setUserId (req, res, next) {
 | 
					router.use(function setUserId (req, res, next) {
 | 
				
			||||||
	if(req.headers.userId){
 | 
						if(req.headers.userId){
 | 
				
			||||||
		userId = req.headers.userId
 | 
							userId = req.headers.userId
 | 
				
			||||||
	}
 | 
							masterKey = req.headers.masterKey
 | 
				
			||||||
	if(req.headers.socket){
 | 
					 | 
				
			||||||
		// socket = req.
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	next()
 | 
						next()
 | 
				
			||||||
@@ -23,11 +21,8 @@ router.use(function setUserId (req, res, next) {
 | 
				
			|||||||
// Note actions
 | 
					// Note actions
 | 
				
			||||||
//
 | 
					//
 | 
				
			||||||
router.post('/get', function (req, res) {
 | 
					router.post('/get', function (req, res) {
 | 
				
			||||||
	// req.io.emit('welcome_homie', 'Welcome, dont poop from excitement')
 | 
						Notes.get(userId, req.body.noteId, req.body.password, masterKey)
 | 
				
			||||||
	Notes.get(userId, req.body.noteId, req.body.password)
 | 
					 | 
				
			||||||
	.then( data => {
 | 
						.then( data => {
 | 
				
			||||||
		//Join room when user opens note
 | 
					 | 
				
			||||||
		// req.io.join('note_room')
 | 
					 | 
				
			||||||
		res.send(data)
 | 
							res.send(data)
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
@@ -38,17 +33,17 @@ router.post('/delete', function (req, res) {
 | 
				
			|||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router.post('/create', function (req, res) {
 | 
					router.post('/create', function (req, res) {
 | 
				
			||||||
	Notes.create(userId, req.body.title, req.body.text)
 | 
						Notes.create(userId, req.body.title, req.body.text, masterKey)
 | 
				
			||||||
	.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.title, req.body.color, req.body.pinned, req.body.archived, req.body.password, req.body.hint)
 | 
						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, masterKey)
 | 
				
			||||||
	.then( id => res.send({id}) )
 | 
						.then( id => res.send({id}) )
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router.post('/search', function (req, res) {
 | 
					router.post('/search', function (req, res) {
 | 
				
			||||||
	Notes.search(userId, req.body.searchQuery, req.body.searchTags, req.body.fastFilters)
 | 
						Notes.search(userId, req.body.searchQuery, req.body.searchTags, req.body.fastFilters, masterKey)
 | 
				
			||||||
	.then( notesAndTags => {
 | 
						.then( notesAndTags => {
 | 
				
			||||||
		res.send(notesAndTags)
 | 
							res.send(notesAndTags)
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
@@ -62,6 +57,14 @@ router.post('/difftext', function (req, res) {
 | 
				
			|||||||
	})
 | 
						})
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					router.post('/reindex', function (req, res) {
 | 
				
			||||||
 | 
						Notes.reindex(userId, masterKey)
 | 
				
			||||||
 | 
						.then( data => {
 | 
				
			||||||
 | 
							res.send(data)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//
 | 
					//
 | 
				
			||||||
// Update single note attributes
 | 
					// Update single note attributes
 | 
				
			||||||
//
 | 
					//
 | 
				
			||||||
@@ -116,5 +119,4 @@ router.get('/reindex5yu43prchuj903mrc', function (req, res) {
 | 
				
			|||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
module.exports = router
 | 
					module.exports = router
 | 
				
			||||||
@@ -2,6 +2,7 @@ var express = require('express')
 | 
				
			|||||||
var router = express.Router()
 | 
					var router = express.Router()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let User = require('@models/User');
 | 
					let User = require('@models/User');
 | 
				
			||||||
 | 
					const cs = require('@helpers/CryptoString')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// middleware that is specific to this router
 | 
					// middleware that is specific to this router
 | 
				
			||||||
router.use(function timeLog (req, res, next) {
 | 
					router.use(function timeLog (req, res, next) {
 | 
				
			||||||
@@ -31,19 +32,19 @@ router.post('/login', function (req, res) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	User.login(username, password)
 | 
						User.login(username, password)
 | 
				
			||||||
		.then(function(loginToken){
 | 
						.then( ({token, userId}) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			//Return json web token to user
 | 
							returnData['username'] = username
 | 
				
			||||||
			returnData['success'] = true
 | 
							returnData['token'] = token
 | 
				
			||||||
			returnData['token'] = loginToken
 | 
							returnData['success'] = true
 | 
				
			||||||
			returnData['username'] = username
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			res.send(returnData)
 | 
							res.send(returnData)
 | 
				
			||||||
		})
 | 
							return
 | 
				
			||||||
		.catch(e => {
 | 
						})
 | 
				
			||||||
			console.log(e)
 | 
						.catch(e => {
 | 
				
			||||||
			res.send(returnData)
 | 
							console.log(e)
 | 
				
			||||||
		})
 | 
							res.send(returnData)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// fetch counts of users notes
 | 
					// fetch counts of users notes
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user