From 1d395badfaf88d7f110cc5d4433dac1c7c4e5e56 Mon Sep 17 00:00:00 2001 From: azivner Date: Sat, 16 Sep 2017 23:21:46 -0400 Subject: [PATCH] password now encrypts random "data key" which is then used for encryption of the actual data --- setup.py | 24 ++++++++-- src/app.py | 3 +- src/change_password.py | 8 ++-- src/my_scrypt.py | 6 +-- src/password_api.py | 2 +- src/tree_api.py | 5 +- static/js/encryption.js | 101 ++++++++++++++++++++++++++++------------ static/js/tree.js | 6 +-- 8 files changed, 107 insertions(+), 48 deletions(-) diff --git a/setup.py b/setup.py index 22d361047..525340595 100644 --- a/setup.py +++ b/setup.py @@ -10,6 +10,9 @@ from builtins import input import src.config_provider import src.sql import src.my_scrypt +from Crypto.Cipher import AES +from Crypto.Util import Counter +import hashlib config = src.config_provider.getConfig() src.sql.connect(config['Document']['documentPath']) @@ -29,14 +32,25 @@ password2 = getpass.getpass(prompt='Repeat the same password: ') if password1 == password2: # urandom is secure enough, see https://docs.python.org/2/library/os.html - src.sql.setOption('flask_secret_key', base64.b64encode(os.urandom(24))) - src.sql.setOption('verification_salt', base64.b64encode(os.urandom(24))) - src.sql.setOption('encryption_salt', base64.b64encode(os.urandom(24))) + src.sql.setOption('flask_secret_key', base64.b64encode(os.urandom(32))) + src.sql.setOption('password_verification_salt', base64.b64encode(os.urandom(32))) + src.sql.setOption('password_derived_key_salt', base64.b64encode(os.urandom(32))) - hash = src.my_scrypt.getVerificationHash(password1) + password_derived_key = src.my_scrypt.getPasswordDerivedKey(password1) + + aes = AES.new(password_derived_key, AES.MODE_CTR, counter=Counter.new(128, initial_value=5)) + + data_key = os.urandom(32) + data_key_digest = hashlib.sha256(data_key).digest()[:4] + + encrypted_data_key = aes.encrypt(data_key_digest + data_key) + + src.sql.setOption('encrypted_data_key', base64.b64encode(encrypted_data_key)) + + verification_hash = src.my_scrypt.getVerificationHash(password1) src.sql.setOption('username', username) - src.sql.setOption('password', binascii.hexlify(hash)) + src.sql.setOption('password_verification_hash', base64.b64encode(verification_hash)) src.sql.commit() diff --git a/src/app.py b/src/app.py index d54f532bd..393b766a6 100644 --- a/src/app.py +++ b/src/app.py @@ -1,6 +1,7 @@ import os import binascii +import base64 from flask import Flask, request, send_from_directory from flask import render_template, redirect from flask_cors import CORS @@ -61,7 +62,7 @@ certPath = config['Network']['certPath'] certKeyPath = config['Network']['certKeyPath'] def verify_password(guessed_password): - hashed_password = binascii.unhexlify(getOption('password')) + hashed_password = base64.b64decode(getOption('password_verification_hash')) guess_hashed = my_scrypt.getVerificationHash(guessed_password) diff --git a/src/change_password.py b/src/change_password.py index 3f0fe9ec4..41d0e186f 100644 --- a/src/change_password.py +++ b/src/change_password.py @@ -11,16 +11,16 @@ import src.my_scrypt def change_password(current_password, new_password): current_password_hash = binascii.hexlify(src.my_scrypt.getVerificationHash(current_password)) - if current_password_hash != src.sql.getOption('password'): + if current_password_hash != src.sql.getOption('password_verification_hash'): return { 'success': False, 'message': "Given current password doesn't match hash" } - current_password_encryption_key = src.my_scrypt.getEncryptionHash(current_password) + current_password_encryption_key = src.my_scrypt.getPasswordDerivedKey(current_password) new_password_verification_key = binascii.hexlify(src.my_scrypt.getVerificationHash(new_password)) - new_password_encryption_key = src.my_scrypt.getEncryptionHash(new_password) + new_password_encryption_key = src.my_scrypt.getPasswordDerivedKey(new_password) encrypted_notes = src.sql.getResults("select note_id, note_title, note_text from notes where encryption = 1") @@ -49,7 +49,7 @@ def change_password(current_password, new_password): src.sql.execute("update notes set note_title = ?, note_text = ? where note_id = ?", [re_encrypted_title, re_encrypted_text, note['note_id']]) - src.sql.setOption('password', new_password_verification_key) + src.sql.setOption('password_verification_hash', new_password_verification_key) src.sql.commit() return { diff --git a/src/my_scrypt.py b/src/my_scrypt.py index b1fce1110..4ca9fa9a0 100644 --- a/src/my_scrypt.py +++ b/src/my_scrypt.py @@ -2,12 +2,12 @@ import scrypt # pip install scrypt import sql def getVerificationHash(password): - salt = sql.getOption('verification_salt') + salt = sql.getOption('password_verification_salt') return getScryptHash(password, salt) -def getEncryptionHash(password): - salt = sql.getOption('encryption_salt') +def getPasswordDerivedKey(password): + salt = sql.getOption('password_derived_key_salt') return getScryptHash(password, salt) diff --git a/src/password_api.py b/src/password_api.py index e2626b36a..25500287f 100644 --- a/src/password_api.py +++ b/src/password_api.py @@ -12,7 +12,7 @@ password_api = Blueprint('password_api', __name__) def verifyPassword(): req = request.get_json(force=True) - hashedPassword = sql.getOption('password') + hashedPassword = sql.getOption('password_verification_hash') hashedPasswordBytes = binascii.unhexlify(hashedPassword) hashedPasswordSha = hashlib.sha256(hashedPasswordBytes).hexdigest() diff --git a/src/tree_api.py b/src/tree_api.py index 28729c54b..ac31f5a8d 100644 --- a/src/tree_api.py +++ b/src/tree_api.py @@ -40,8 +40,9 @@ def getTree(): retObject = {} retObject['notes'] = rootNotes retObject['start_note_id'] = getSingleResult('select * from options where opt_name = "start_node"')['opt_value']; - retObject['verification_salt'] = getOption('verification_salt') - retObject['encryption_salt'] = getOption('encryption_salt') + retObject['password_verification_salt'] = getOption('password_verification_salt') + retObject['password_derived_key_salt'] = getOption('password_derived_key_salt') + retObject['encrypted_data_key'] = getOption('encrypted_data_key') retObject['encryption_session_timeout'] = getOption('encryption_session_timeout') return jsonify(retObject) \ No newline at end of file diff --git a/static/js/encryption.js b/static/js/encryption.js index 1c6856b76..4c7ed3935 100644 --- a/static/js/encryption.js +++ b/static/js/encryption.js @@ -28,30 +28,17 @@ let globalEncryptionKey = null; let globalLastEncryptionOperationDate = null; function deriveEncryptionKey(password) { - const verificationPromise = computeScrypt(password, globalVerificationSalt, (key, resolve, reject) => { - $.ajax({ - url: baseUrl + 'password/verify', - type: 'POST', - data: JSON.stringify({ - password: sha256(key) - }), - contentType: "application/json", - success: function (result) { - if (result.valid) { - resolve(); - } - else { - alert("Wrong password"); + return computeScrypt(password, globalEncryptionSalt, (key, resolve, reject) => { + const dataKeyAes = getDataKeyAes(key); - reject(); - } - } - }); + const decryptedDataKey = decrypt(dataKeyAes, globalEncryptedDataKey); + + if (decryptedDataKey === false) { + reject("Wrong password."); + } + + return decryptedDataKey; }); - - const encryptionKeyPromise = computeScrypt(password, globalEncryptionSalt, (key, resolve, reject) => resolve(key)); - - return Promise.all([ verificationPromise, encryptionKeyPromise ]).then(results => results[1]); } function computeScrypt(password, salt, callback) { @@ -110,7 +97,8 @@ $("#encryptionPasswordForm").submit(function() { globalEncryptionCallback = null; } - }); + }) + .catch(reason => alert(reason)); return false; }); @@ -141,12 +129,16 @@ function isEncryptionAvailable() { return globalEncryptionKey !== null; } -function getAes() { +function getDataAes() { globalLastEncryptionOperationDate = new Date(); return new aesjs.ModeOfOperation.ctr(globalEncryptionKey, new aesjs.Counter(5)); } +function getDataKeyAes(key) { + return new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5)); +} + function encryptNoteIfNecessary(note) { if (note.detail.encryption === 0) { return note; @@ -157,21 +149,72 @@ function encryptNoteIfNecessary(note) { } function encryptString(str) { - const aes = getAes(); - const bytes = aesjs.utils.utf8.toBytes(str); + return encrypt(getDataAes(), str); +} - const encryptedBytes = aes.encrypt(bytes); +function encrypt(aes, str) { + const payload = aesjs.utils.utf8.toBytes(str); + const digest = sha256Array(payload).slice(0, 4); + + const digestWithPayload = concat(digest, payload); + + const encryptedBytes = aes.encrypt(digestWithPayload); return uint8ToBase64(encryptedBytes); } function decryptString(encryptedBase64) { - const aes = getAes(); + const decryptedBytes = decrypt(getDataAes(), encryptedBase64); + + return aesjs.utils.utf8.fromBytes(decryptedBytes); +} + +function decrypt(aes, encryptedBase64) { const encryptedBytes = base64ToUint8Array(encryptedBase64); const decryptedBytes = aes.decrypt(encryptedBytes); - return aesjs.utils.utf8.fromBytes(decryptedBytes); + const digest = decryptedBytes.slice(0, 4); + const payload = decryptedBytes.slice(4); + + const hashArray = sha256Array(payload); + + const computedDigest = hashArray.slice(0, 4); + + if (!arraysIdentical(digest, computedDigest)) { + return false; + } + + return payload; +} + +function concat(a, b) { + const result = []; + + for (let key in a) { + result.push(a[key]); + } + + for (let key in b) { + result.push(b[key]); + } + + return result; +} + +function sha256Array(content) { + const hash = sha256.create(); + hash.update(content); + return hash.array(); +} + +function arraysIdentical(a, b) { + let i = a.length; + if (i !== b.length) return false; + while (i--) { + if (a[i] !== b[i]) return false; + } + return true; } function encryptNote(note) { diff --git a/static/js/tree.js b/static/js/tree.js index 121aa893d..7b61988c4 100644 --- a/static/js/tree.js +++ b/static/js/tree.js @@ -83,17 +83,17 @@ function setExpandedToServer(note_id, is_expanded) { }); } -let globalVerificationSalt; let globalEncryptionSalt; let globalEncryptionSessionTimeout; +let globalEncryptedDataKey; $(function(){ $.get(baseUrl + 'tree').then(resp => { const notes = resp.notes; let startNoteId = resp.start_note_id; - globalVerificationSalt = resp.verification_salt; - globalEncryptionSalt = resp.encryption_salt; + globalEncryptionSalt = resp.password_derived_key_salt; globalEncryptionSessionTimeout = resp.encryption_session_timeout; + globalEncryptedDataKey = resp.encrypted_data_key; if (document.location.hash) { startNoteId = document.location.hash.substr(1); // strip initial #