diff --git a/client/src/assets/semantic-helper.css b/client/src/assets/semantic-helper.css
index 6eddc84..65f15a5 100644
--- a/client/src/assets/semantic-helper.css
+++ b/client/src/assets/semantic-helper.css
@@ -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;
}
diff --git a/client/src/components/GlobalNotificationComponent.vue b/client/src/components/GlobalNotificationComponent.vue
index e7d5bc1..b63d22e 100644
--- a/client/src/components/GlobalNotificationComponent.vue
+++ b/client/src/components/GlobalNotificationComponent.vue
@@ -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;
}
@@ -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: {
diff --git a/client/src/components/NoteInputPanel.vue b/client/src/components/NoteInputPanel.vue
index 0b26081..d552ccd 100644
--- a/client/src/components/NoteInputPanel.vue
+++ b/client/src/components/NoteInputPanel.vue
@@ -41,12 +41,69 @@
-
+
+
+
+
+
+
+
+
+
+
+ Password Protect
+
+
+
+ Remove Password
+
+
Status: {{ statusText }}
@@ -56,6 +113,8 @@
+
+
@@ -202,7 +261,56 @@
-
+
+
+
Note Decrypted
+
Lock Note
+
+
+
+
+
+
+
Password protect this Note
+
Password protection will prevent anyone from reading the text of this note, unless they enter the correct password.
+
Only the note text is protected. Title, tags, and files are not encrypted and remain visible without a password.
+
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.
+
Warning. There is no way to recover a lost password.
+
+
+
+
+
+
+
+
+
@@ -210,6 +318,7 @@
@@ -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 {
diff --git a/client/src/components/NoteTitleDisplayCard.vue b/client/src/components/NoteTitleDisplayCard.vue
index 746cd95..a3d601d 100644
--- a/client/src/components/NoteTitleDisplayCard.vue
+++ b/client/src/components/NoteTitleDisplayCard.vue
@@ -26,7 +26,7 @@
-
+
Empty Note
@@ -37,8 +37,7 @@
+ class="big-text">{{ note.title }}
+
+
+
+ Locked
+
+
Archived
+
+
+ Locked
+
+
@@ -679,6 +684,7 @@
'withTags', // 'Only Show Notes with Tags'
'onlyArchived', //'Only Show Archived Notes'
'onlyShowSharedNotes', //Only show shared notes
+ 'onlyShowEncrypted',
]
let filter = {}
diff --git a/client/src/stores/mainStore.js b/client/src/stores/mainStore.js
index a36932a..4c42a15 100644
--- a/client/src/stores/mainStore.js
+++ b/client/src/stores/mainStore.js
@@ -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])
+ })
}
},
diff --git a/server/helpers/CryptoString.js b/server/helpers/CryptoString.js
new file mode 100644
index 0000000..8b57798
--- /dev/null
+++ b/server/helpers/CryptoString.js
@@ -0,0 +1,109 @@
+/*
+ Crypto String
+ Securely Encrypts and decrypts a string using a password
+*/
+
+
+const IV_BYTE_SIZE = 100 // Size of initialization vector
+const SALT_BYTE_SIZE = 900 // Size of salt
+const KEY_BYTE_SIZE = 32 // size of cipher key
+const AUTH_TAG_SIZE = 16 // Size of authentication tag
+
+
+const crypto = require('crypto')
+
+let CryptoString = module.exports = {}
+
+CryptoString.encrypt = (password, salt64, rawText) => {
+
+ const initializationVector = crypto.randomBytes(IV_BYTE_SIZE)
+ const salt = Buffer.from(salt64, 'base64')
+ const key = CryptoString.seasonedPassword(password, salt)
+
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, initializationVector, { 'authTagLength': AUTH_TAG_SIZE })
+ let encryptedMessage = cipher.update(rawText)
+
+ encryptedMessage = Buffer.concat([encryptedMessage, cipher.final()])
+ return Buffer.concat([initializationVector, encryptedMessage, cipher.getAuthTag()]).toString('base64')
+
+}
+
+//Decrypt base64 string cipher text,
+CryptoString.decrypt = (password, salt64, cipherTextString) => {
+
+ let cipherText = Buffer.from(cipherTextString, 'base64')
+ const salt = Buffer.from(salt64, 'base64')
+
+ const authTag = cipherText.slice(AUTH_TAG_SIZE*-1)
+ const initializationVector = cipherText.slice(0, IV_BYTE_SIZE)
+ const encryptedMessage = cipherText.slice(IV_BYTE_SIZE, AUTH_TAG_SIZE*-1)
+
+ const key = CryptoString.seasonedPassword(password, salt)
+
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, initializationVector, { 'authTagLength': AUTH_TAG_SIZE })
+
+ try {
+ decipher.setAuthTag(authTag)
+
+ let messagetext = decipher.update(encryptedMessage)
+ messagetext = Buffer.concat([messagetext, decipher.final()]).toString('utf8')
+
+ return messagetext
+
+ } catch(err) {
+
+ // console.log(err)
+ return null
+ }
+
+}
+
+//Salt the password - return {buffer}
+CryptoString.seasonedPassword = (password, salt) => {
+
+ return crypto.scryptSync(password, salt, KEY_BYTE_SIZE)
+}
+
+//Create random salt - return {string}
+CryptoString.createSalt = () => {
+
+ return crypto.randomBytes(SALT_BYTE_SIZE).toString('base64')
+}
+
+CryptoString.hash = (hashString) => {
+
+ return crypto.createHash('sha256').update(hashString).digest()
+}
+
+CryptoString.test = () => {
+
+ const pp = (title, output) => {
+ console.log('----------------'+title+'----------------')
+ console.log(output)
+ }
+
+ const password = 'ItsMePasswordio123'
+ const text = 'The genesis of  a new note. Very magical.
Quite a weonderful thing
Weonderful for sheore
'
+
+ const hashTest = CryptoString.createSalt('password')
+
+ const salt = CryptoString.createSalt()
+ pp('salt',salt.length)
+
+ const seasonPass = CryptoString.seasonedPassword(password, salt)
+
+ const cipherText = CryptoString.encrypt(password, salt, text)
+
+ const decipheredText = CryptoString.decrypt(password, salt, cipherText)
+ pp('Success Decrypt', decipheredText == text ? 'Pass 😁':'Fail' )
+
+ const wrongPass = CryptoString.decrypt('Wrong Password', salt, cipherText)
+ pp('Wrong Password', wrongPass === null ? 'Pass':'Fail')
+
+ const wrongSalt = CryptoString.decrypt(password, 'Wrong Salt', cipherText)
+ pp('Wrong Salt', wrongSalt === null ? 'Pass':'Fail')
+
+ const wrongCipher = CryptoString.decrypt(password, salt, Buffer.from('Hello there'))
+ pp('Wrong Cipher Text', wrongCipher === null ? 'Pass':'Fail')
+
+}
diff --git a/server/helpers/ProcessText.js b/server/helpers/ProcessText.js
index e6878b6..3bc4370 100644
--- a/server/helpers/ProcessText.js
+++ b/server/helpers/ProcessText.js
@@ -56,7 +56,7 @@ ProcessText.deduceNoteTitle = (inString) => {
const tagFreeLength = ProcessText.removeHtml(inString).length
if(tagFreeLength < 100){
- title = ProcessText.stripBlankHtmlLines(inString)
+ sub = ProcessText.stripBlankHtmlLines(inString)
return {title, sub}
}
@@ -178,7 +178,7 @@ ProcessText.deduceNoteTitle = (inString) => {
//Pull out title if its not an empty string
if(ProcessText.removeHtml(finalLines[0]).trim().replace(' ','').length > 0 && !noTitleJustList){
- title = finalLines.shift()
+ // title = finalLines.shift()
}
sub = finalLines.join('')
diff --git a/server/models/Note.js b/server/models/Note.js
index 67c142b..0156818 100644
--- a/server/models/Note.js
+++ b/server/models/Note.js
@@ -8,7 +8,8 @@ let ProcessText = require('@helpers/ProcessText')
const DiffMatchPatch = require('@helpers/DiffMatchPatch')
-var rp = require('request-promise');
+const cs = require('@helpers/CryptoString')
+const rp = require('request-promise');
const fs = require('fs')
let Note = module.exports = {}
@@ -98,7 +99,7 @@ Note.stressTest = () => {
// --------------
-Note.create = (userId, noteText, quickNote = 0) => {
+Note.create = (userId, noteTitle, noteText, quickNote = 0, ) => {
return new Promise((resolve, reject) => {
if(userId == null || userId < 10){ reject('User Id required to create note') }
@@ -106,7 +107,7 @@ Note.create = (userId, noteText, quickNote = 0) => {
const created = Math.round((+new Date)/1000)
db.promise()
- .query(`INSERT INTO note_raw_text (text, updated) VALUE (?, ?)`, [noteText, created])
+ .query(`INSERT INTO note_raw_text (text, title, updated) VALUE (?, ?, ?)`, [noteText, noteTitle, created])
.then( (rows, fields) => {
const rawTextId = rows[0].insertId
@@ -129,7 +130,11 @@ Note.reindex = (userId, noteId) => {
Note.get(userId, noteId)
.then(note => {
- const noteText = note.text
+ let noteText = note.text
+ if(note.encrypted == 1){
+ noteText = '' //Don't put note text in encrypted notes
+ }
+
//
// Update Solr index
@@ -137,7 +142,7 @@ Note.reindex = (userId, noteId) => {
Tags.string(userId, noteId)
.then(tagString => {
- const fullText = ProcessText.removeHtml(noteText) +' '+ tagString
+ const fullText = note.title + ' ' + ProcessText.removeHtml(noteText) +' '+ tagString
db.promise()
.query(`
@@ -158,33 +163,59 @@ Note.reindex = (userId, noteId) => {
})
}
-Note.update = (io, userId, noteId, noteText, color, pinned, archived) => {
+Note.update = (io, userId, noteId, noteText, noteTitle, color, pinned, archived, password = '', passwordHint = '') => {
return new Promise((resolve, reject) => {
//Prevent note loss if it saves with empty text
if(ProcessText.removeHtml(noteText) == ''){
- console.log('Not saving empty note')
- resolve(false)
+ // console.log('Not saving empty note')
+ // resolve(false)
}
const now = Math.round((+new Date)/1000)
db.promise()
- .query('SELECT note_raw_text_id FROM note WHERE id = ? AND user_id = ?', [noteId, userId])
+ .query(`
+ SELECT note_raw_text_id, salt FROM note
+ JOIN note_raw_text ON note_raw_text_id = note_raw_text.id
+ WHERE note.id = ? AND user_id = ?`, [noteId, userId])
.then((rows, fields) => {
const textId = rows[0][0]['note_raw_text_id']
+ let salt = rows[0][0]['salt']
+
+ //If password is removed, remove salt. generate a new one next time its encrypted
+ if(password.length == 0){
+ salt = null
+ }
+
+ //If a password is set, create a salt
+ if(password.length > 3 && !salt){
+ salt = cs.createSalt()
+
+ //Save password hint on first encryption
+ if(passwordHint.length > 0){
+ db.promise().query('UPDATE note_raw_text SET password_hint = ? WHERE id = ?', [passwordHint, textId])
+ }
+ }
+
+ //Encrypt note text if proper data is setup
+ if(password.length > 3 && salt.length > 1000){
+ noteText = cs.encrypt(password, salt, noteText)
+ }
//Update Note text
return db.promise()
- .query('UPDATE note_raw_text SET text = ?, updated = ? WHERE id = ?', [noteText, now, textId])
+ .query('UPDATE note_raw_text SET text = ?, title = ?, updated = ?, salt = ? WHERE id = ?', [noteText, noteTitle, now, salt, textId])
})
.then( (rows, fields) => {
+ const encrypted = password.length > 3 ? 1:0
+
//Update other note attributes
return db.promise()
- .query('UPDATE note SET pinned = ?, archived = ?, color = ? WHERE id = ? AND user_id = ? LIMIT 1',
- [pinned, archived, color, noteId, userId])
+ .query('UPDATE note SET pinned = ?, archived = ?, color = ?, encrypted = ? WHERE id = ? AND user_id = ? LIMIT 1',
+ [pinned, archived, color, encrypted, noteId, userId])
})
.then((rows, fields) => {
@@ -309,6 +340,9 @@ Note.getDiffText = (userId, noteId, usersCurrentText, lastUpdated) => {
Note.get(userId, noteId)
.then(noteObject => {
+ if(!noteObject.text || !usersCurrentText){
+ resolve(null)
+ }
let oldText = noteObject.text.replace(/(\r\n|\n|\r)/gm,"")
let newText = usersCurrentText.replace(/(\r\n|\n|\r)/gm,"")
@@ -354,17 +388,23 @@ Note.getDiffText = (userId, noteId, usersCurrentText, lastUpdated) => {
}
-Note.get = (userId, noteId) => {
+Note.get = (userId, noteId, password = '') => {
return new Promise((resolve, reject) => {
db.promise()
.query(`
- SELECT
+ SELECT
+ note_raw_text.title,
note_raw_text.text,
+ note_raw_text.salt,
+ note_raw_text.password_hint,
note_raw_text.updated as updated,
+ note_raw_text.decrypt_attempts_count,
+ note_raw_text.last_decrypted_date,
note.created,
note.pinned,
note.archived,
note.color,
+ note.encrypted,
count(distinct attachment.id) as attachment_count,
note.note_raw_text_id as rawTextId,
shareUser.username as shareUsername
@@ -372,15 +412,74 @@ Note.get = (userId, noteId) => {
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
LEFT JOIN attachment ON (note.id = attachment.note_id)
LEFT JOIN user as shareUser ON (note.share_user_id = shareUser.id)
- WHERE note.user_id = ? AND note.id = ? LIMIT 1`, [userId,noteId])
+ WHERE note.user_id = ? AND note.id = ? LIMIT 1`, [userId, noteId])
.then((rows, fields) => {
- const created = Math.round((+new Date)/1000)
+ const nowTime = Math.round((+new Date)/1000)
+ let noteLockedOut = false
+ let noteData = rows[0][0]
+ const rawTextId = noteData['rawTextId']
+ noteData.decrypted = true
- db.promise().query(`UPDATE note SET opened = ? WHERE (id = ?)`, [created, noteId])
+
+ //If this is not and encrypted note, pass decrypted true, skip encryption stuff
+ if(noteData.encrypted == 1){
+ noteData.decrypted = false
+ }
+
+ //
+ //Rate Limiting
+ //
+ //Check if note is exceeding decrypt attempt limit
+ if(noteData.encrypted == 1){
+ const timeSinceLastUnlock = nowTime - noteData.last_decrypted_date
+
+ //To many attempts in less than 5 minutes, note is locked
+ if(noteData.decrypt_attempts_count > 3 && timeSinceLastUnlock < 300){
+ console.log('Locked Out')
+ noteLockedOut = true
+ }
+
+ //its been 5 minutes, reset attempt count
+ if(noteData.decrypt_attempts_count > 0 && timeSinceLastUnlock > 300){
+ noteLockedOut = false
+ noteData.decrypt_attempts_count = 0
+ console.log('Resseting Lockout')
+ db.promise().query('UPDATE note_raw_text SET last_decrypted_date = ?, decrypt_attempts_count = 0 WHERE id = ?', [nowTime, rawTextId ])
+ }
+ }
+
+ //Note is encrypted, lets try and decipher it with the given password
+ if(password.length > 3 && noteData.encrypted == 1 && !noteLockedOut){
+
+ const decipheredText = cs.decrypt(password, noteData.salt, noteData.text)
+
+ //Text was decrypted, return decrypted text
+ if(decipheredText !== null){
+ noteData.decrypted = true
+ noteData.text = decipheredText
+
+ //Save last decrypted date, reset decrypt atempts
+ db.promise().query('UPDATE note_raw_text SET last_decrypted_date = ?, decrypt_attempts_count = 0 WHERE id = ?', [nowTime, rawTextId ])
+
+ }
+ //Text was not deciphered, delete object, never return cipher text
+ if(decipheredText === null){
+ noteData.text = '' //Never return cipher text
+ noteData.decryptFail = true
+ noteData.decrypt_attempts_count++ //Update display for user
+
+ //Update decrypt attempts
+ db.promise().query('UPDATE note_raw_text SET decrypt_attempts_count = decrypt_attempts_count +1 WHERE id = ?', [rawTextId ])
+ }
+ }
+
+ db.promise().query(`UPDATE note SET opened = ? WHERE (id = ?)`, [nowTime, noteId])
//Return note data
- resolve(rows[0][0])
+ delete noteData.salt //remove salt from return data
+ noteData.lockedOut = noteLockedOut
+ resolve(noteData)
})
.catch(console.log)
@@ -484,6 +583,7 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
let noteSearchQuery = `
SELECT note.id,
SUBSTRING(note_raw_text.text, 1, 1500) as text,
+ note_raw_text.title as title,
note_raw_text.updated as updated,
opened,
color,
@@ -491,6 +591,7 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
count(distinct attachment.id) as attachment_count,
note.pinned,
note.archived,
+ note.encrypted,
GROUP_CONCAT(DISTINCT tag.text) as tags,
GROUP_CONCAT(DISTINCT attachment.file_location) as thumbs,
shareUser.username as shareUsername,
@@ -527,6 +628,10 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
searchAllNotes = true
}
+ if(fastFilters.onlyShowEncrypted == 1){
+ noteSearchQuery += ' AND encrypted = 1'
+ }
+
//If tags are passed, use those tags in search
if(searchTags.length > 0){
searchParams.push(searchTags)
@@ -610,14 +715,18 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
noteIds.push(note.id)
if(note.text == null){ note.text = '' }
+ if(note.encrypted == 1){ note.text = '' }
//Deduce note title
const textData = ProcessText.deduceNoteTitle(note.text)
// console.log(textData)
// console.log(textData)
+
+ if(note.title == null){
+ note.title = ''
+ }
- note.title = textData.title
note.subtext = textData.sub
note.titleLength = textData.titleLength
note.subtextLength = textData.subtextLength
diff --git a/server/models/QuickNote.js b/server/models/QuickNote.js
index 25be442..a7e7205 100644
--- a/server/models/QuickNote.js
+++ b/server/models/QuickNote.js
@@ -71,7 +71,7 @@ QuickNote.update = (userId, pushText) => {
let newText = broken +''+ d.text
//Save that, then return the new text
- Note.update(null, userId, d.id, newText, d.color, d.pinned, d.archived)
+ Note.update(null, userId, d.id, newText, '', d.color, d.pinned, d.archived)
.then( saveResults => {
resolve({
id:d.id,
diff --git a/server/models/User.js b/server/models/User.js
index f9dd030..19ab820 100644
--- a/server/models/User.js
+++ b/server/models/User.js
@@ -124,11 +124,12 @@ User.getCounts = (userId) => {
`SELECT
SUM(pinned = 1 && archived = 0 && share_user_id IS NULL) AS pinnedNotes,
SUM(archived = 1 && share_user_id IS NULL) AS archivedNotes,
+ SUM(encrypted = 1) AS encryptedNotes,
SUM(share_user_id IS NULL) AS totalNotes,
SUM(share_user_id != ?) AS sharedToNotes,
SUM( (share_user_id != ? && opened IS null) || (share_user_id != ? && note_raw_text.updated > opened) ) AS unreadNotes
FROM note
- JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
+ LEFT JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
WHERE user_id = ?`, [userId, userId, userId, userId])
.then( (rows, fields) => {
diff --git a/server/routes/noteController.js b/server/routes/noteController.js
index c371200..19e9cbd 100644
--- a/server/routes/noteController.js
+++ b/server/routes/noteController.js
@@ -24,7 +24,7 @@ router.use(function setUserId (req, res, next) {
//
router.post('/get', function (req, res) {
// req.io.emit('welcome_homie', 'Welcome, dont poop from excitement')
- Notes.get(userId, req.body.noteId)
+ Notes.get(userId, req.body.noteId, req.body.password)
.then( data => {
//Join room when user opens note
// req.io.join('note_room')
@@ -38,12 +38,12 @@ router.post('/delete', function (req, res) {
})
router.post('/create', function (req, res) {
- Notes.create(userId, req.body.title)
+ Notes.create(userId, req.body.title, req.body.text)
.then( id => res.send({id}) )
})
router.post('/update', function (req, res) {
- Notes.update(req.io, userId, req.body.noteId, req.body.text, req.body.color, req.body.pinned, req.body.archived)
+ Notes.update(req.io, userId, req.body.noteId, req.body.text, req.body.title, req.body.color, req.body.pinned, req.body.archived, req.body.password, req.body.hint)
.then( id => res.send({id}) )
})
diff --git a/syncDown.sh b/syncDown.sh
index 7527046..1008b9f 100755
--- a/syncDown.sh
+++ b/syncDown.sh
@@ -12,4 +12,4 @@
# z - Compress for speed
# h - Human Readable file sizes
-rsync -e 'ssh' --exclude-from=dontSync.txt -havzC --update mab@marvin.local:/home/mab/pi/ /Users/maxgialanella/Code/privateInternet
+rsync -e 'ssh' --exclude-from=dontSync.txt -havzC --update mab@marvin.local:/home/mab/pi/ .