Encrypted Notes Alpha!

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

View File

@@ -42,6 +42,12 @@ body {
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]):focus,
.ui.form textarea:not([type]),
@@ -216,10 +222,6 @@ a:hover {
scrollbar-width: none;
}
/*Makes the first line real big */
.squire-box > p:first-child {
font-size: 1.4em;
line-height: 1.7em;
}
.squire-box:focus {
outline: none;
}

View File

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

View File

@@ -41,12 +41,69 @@
<!-- Squire box grows -->
<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 -->
<div class="ui basic segment">
<div class="ui grid">
<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">
Status: {{ statusText }}
</div>
@@ -56,6 +113,8 @@
</div>
</div>
</div>
</div>
<!-- && this.$store.getters.getIsUserOnMobile -->
@@ -202,7 +261,56 @@
</div>
</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>
</template>
@@ -210,6 +318,7 @@
<script>
import axios from 'axios'
const crypto = require('crypto')
const DiffMatchPatch = require('../../../server/helpers/DiffMatchPatch')
export default {
@@ -233,6 +342,7 @@
currentNoteId: 0,
modified: false,
noteText: '',
noteTitle: '',
rawTextId: 0,
created: '',
updated: '',
@@ -270,6 +380,18 @@
colorPickerVisible: false,
showFilesSideMenu: 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: {
@@ -299,6 +421,11 @@
},
beforeDestroy(){
this.password = ''
this.passwordConfirm = ''
this.hashedPass = ''
clearTimeout(this.autoLockTimeout)
this.$io.emit('leave_room', this.rawTextId)
document.removeEventListener('visibilitychange', this.checkForUpdatedNote)
@@ -326,7 +453,14 @@
//focus on open, not on mobile, thats annoying
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
@@ -705,48 +839,68 @@
},
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 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 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
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 => {
//Set up local data
vm.currentNoteId = noteId
this.currentNoteId = this.noteid
this.rawTextId = response.data.rawTextId
this.shareUsername = response.data.shareUsername
this.passwordHint = response.data.password_hint
this.created = response.data.created
this.updated = response.data.updated
this.noteTitle = response.data.title
vm.noteText = response.data.text
vm.diffNoteText = response.data.text
this.noteText = 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
if(response.data.color){
vm.styleObject = JSON.parse(response.data.color)
this.styleObject = JSON.parse(response.data.color)
}
if(response.data.pinned != null){
vm.pinned = response.data.pinned
this.pinned = response.data.pinned
}
vm.archived = response.data.archived
vm.attachmentCount = response.data.attachment_count
this.archived = response.data.archived
this.attachmentCount = response.data.attachment_count
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.initSquire()
this.startAutolockTimer()
})
})
@@ -917,7 +1071,7 @@
.createExpression(xpath)
.evaluate(document, XPathResult.FIRST_ORDERED_NODE_TYPE) .singleNodeValue
},
onKeyup(event){
onKeyup(){
this.statusText = 'Save'
@@ -943,6 +1097,12 @@
// 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
const currentNoteText = this.getText()
if( this.lastNoteHash == this.hashString( currentNoteText )){
@@ -958,11 +1118,14 @@
}
const postData = {
'noteId':this.currentNoteId,
'noteId': this.currentNoteId,
'title': this.noteTitle,
'text': currentNoteText,
'color': JSON.stringify(this.styleObject), //Save little json color object
'pinned': this.pinned,
'archived':this.archived,
'archived': this.archived,
'password': this.hashedPass,
'hint': this.passwordHint,
}
this.statusText = 'Saving'
@@ -971,8 +1134,11 @@
this.updated = Math.round((+new Date)/1000)
this.modified = true
console.log('Saved')
//Update last saved note hash
this.lastNoteHash = this.hashString( currentNoteText )
this.startAutolockTimer()
return resolve(true)
})
})
@@ -1010,6 +1176,8 @@
},
hashString(text){
text = this.noteTitle + text
var hash = 0;
if (text == null || text.length == 0) {
return hash;
@@ -1055,7 +1223,80 @@
this.$io.on('incoming_diff', 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>
@@ -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 */
.all-settings {

View File

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

View File

@@ -28,6 +28,11 @@
<i class="green archive icon"></i>Archived
<!-- <span>{{ $store.getters.totals['archivedNotes'] }}</span> -->
</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>
@@ -679,6 +684,7 @@
'withTags', // 'Only Show Notes with Tags'
'onlyArchived', //'Only Show Archived Notes'
'onlyShowSharedNotes', //Only show shared notes
'onlyShowEncrypted',
]
let filter = {}

View File

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