From a1b5a4099ea3e02e1d883966b98544a382e60f44 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sat, 27 Sep 2014 13:15:46 -0700 Subject: [PATCH] Work on cryptography --- features/data/configs/encrypted_old.json | 13 ++++++++++ ...rypted_with_pw.yaml => encrypted_old.yaml} | 3 +-- features/data/journals/encrypted.journal | Bin 128 -> 228 bytes features/encryption.feature | 7 +++++- features/steps/core.py | 7 +++--- jrnl/EncryptedJournal.py | 23 +++++++++++++----- jrnl/Journal.py | 2 +- jrnl/cli.py | 5 +++- jrnl/install.py | 2 +- jrnl/upgrade.py | 19 +++++++++++++-- jrnl/util.py | 5 ++++ 11 files changed, 69 insertions(+), 17 deletions(-) create mode 100644 features/data/configs/encrypted_old.json rename features/data/configs/{encrypted_with_pw.yaml => encrypted_old.yaml} (64%) diff --git a/features/data/configs/encrypted_old.json b/features/data/configs/encrypted_old.json new file mode 100644 index 00000000..e69d9b79 --- /dev/null +++ b/features/data/configs/encrypted_old.json @@ -0,0 +1,13 @@ +{ + "default_hour": 9, + "default_minute": 0, + "editor": "", + "encrypt": true, + "highlight": true, + "journals": { + "default": "features/journals/encrypted_jrnl-1-9-5.journal" + }, + "linewrap": 80, + "tagsymbols": "@", + "timeformat": "%Y-%m-%d %H:%M" +} diff --git a/features/data/configs/encrypted_with_pw.yaml b/features/data/configs/encrypted_old.yaml similarity index 64% rename from features/data/configs/encrypted_with_pw.yaml rename to features/data/configs/encrypted_old.yaml index 3b4247a9..5324057c 100644 --- a/features/data/configs/encrypted_with_pw.yaml +++ b/features/data/configs/encrypted_old.yaml @@ -4,8 +4,7 @@ editor: '' encrypt: true highlight: true journals: - default: features/journals/encrypted.journal + default: features/journals/encrypted_jrnl1-9-5.journal linewrap: 80 -password: bad doggie no biscuit tagsymbols: '@' timeformat: '%Y-%m-%d %H:%M' diff --git a/features/data/journals/encrypted.journal b/features/data/journals/encrypted.journal index 339b47baf9671f4550efeb9b6a0cfcd5032255d6..f79be62c3b097fd82dee20bc6af2b13cb59479f5 100644 GIT binary patch literal 228 zcmWl`+mb^t007W0G9$FeLm$MVs@ZMHy7a}MD;7z%g2C5!_8e;eL*MQ(Q7Em{!dKd> z&eAjBBp;1eGA{Yy;ozqLdx`!m9sAEr2|^l)OKi?rxL5JS-6+#~Es%G$#PGaU+aN!P za~u3{dM$y5fC$|cD=#TfK% zo$vqds@GuQk)HRNeq9YXA2npvZNj&%rvy3er{G__OPisNYBb9W$#X72Ioqex8{isE ANdN!< literal 128 zcmV-`0Du3(bJIGVsY(mXmoW-2hF&*L`0NbJTYlTUr8*^Qm97}8E^3^1bZ$P^M diff --git a/features/encryption.feature b/features/encryption.feature index cbbf15e5..0ecb4c7b 100644 --- a/features/encryption.feature +++ b/features/encryption.feature @@ -1,11 +1,16 @@ Feature: Encrypted journals - Scenario: Loading an encrypted journal Given we use the config "encrypted.yaml" When we run "jrnl -n 1" and enter "bad doggie no biscuit" Then we should see the message "Password" and the output should contain "2013-06-10 15:40 Life is good" + Scenario: Upgrading a journal encrypted with jrnl 1.x + Given we use the config "encrypted_old.json" + When we run "jrnl -n 1" and enter "Y" + Then we should see the message "Password" + and the output should contain "2013-06-10 15:40 Life is good" + Scenario: Decrypting a journal Given we use the config "encrypted.yaml" When we run "jrnl --decrypt" and enter "bad doggie no biscuit" diff --git a/features/steps/core.py b/features/steps/core.py index 8e1e1652..416ec468 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -50,9 +50,10 @@ def open_journal(journal_name="default"): def set_config(context, config_file): full_path = os.path.join("features/configs", config_file) install.CONFIG_FILE_PATH = os.path.abspath(full_path) - # Add jrnl version to file - with open(install.CONFIG_FILE_PATH, 'a') as cf: - cf.write("version: {}".format(__version__)) + if config_file.endswith("yaml"): + # Add jrnl version to file for 2.x journals + with open(install.CONFIG_FILE_PATH, 'a') as cf: + cf.write("version: {}".format(__version__)) @when('we run "{command}" and enter') diff --git a/jrnl/EncryptedJournal.py b/jrnl/EncryptedJournal.py index c80bcc08..4d5cfe73 100644 --- a/jrnl/EncryptedJournal.py +++ b/jrnl/EncryptedJournal.py @@ -1,12 +1,22 @@ from . import Journal, util from cryptography.fernet import Fernet, InvalidToken +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from cryptography.hazmat.backends import default_backend import base64 -from passlib.hash import pbkdf2_sha256 +import os def make_key(password): - derived_key = pbkdf2_sha256.encrypt(password.encode("utf-8"), rounds=10000, salt_size=32) - return base64.urlsafe_b64encode(derived_key) + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=os.urandom(16), + iterations=100000, + backend=default_backend() + ) + key = kdf.derive(password) + return base64.urlsafe_b64encode(key) class EncryptedJournal(Journal.Journal): @@ -21,14 +31,15 @@ class EncryptedJournal(Journal.Journal): def validate_password(password): key = make_key(password) try: - return Fernet(key).decrypt(journal_encrypted) - except InvalidToken: + return Fernet(key).decrypt(journal_encrypted).decode('utf-8') + except (InvalidToken, IndexError): + print base64.urlsafe_b64decode(journal_encrypted) return None return util.get_password(keychain=self.name, validator=validate_password) def _store(self, filename, text): key = make_key(self.config['password']) - journal = Fernet(key).encrypt(text) + journal = Fernet(key).encrypt(text.encode('utf-8')) with open(filename, 'w') as f: f.write(journal) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 5eba9b0a..5fc92df4 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -45,7 +45,7 @@ class Journal(object): def write(self, filename=None): """Dumps the journal into the config file, overwriting it""" filename = filename or self.config['journal'] - text = u"\n".join([e.__unicode__() for e in self.entries]) + text = "\n".join([e.__unicode__() for e in self.entries]) self._store(filename, text) def _load(self, filename): diff --git a/jrnl/cli.py b/jrnl/cli.py index 76b9d43c..7fdb21a3 100644 --- a/jrnl/cli.py +++ b/jrnl/cli.py @@ -131,7 +131,10 @@ def run(manual_args=None): config = install.load_or_install_jrnl() if args.ls: - print(util.py2encode(list_journals(config))) + util.prnt(u"Journals defined in {}".format(install.CONFIG_FILE_PATH)) + ml = min(max(len(k) for k in config['journals']), 20) + for journal, cfg in config['journals'].items(): + print " * {:{}} -> {}".format(journal, ml, cfg['journal'] if isinstance(cfg, dict) else cfg) sys.exit(0) original_config = config.copy() diff --git a/jrnl/install.py b/jrnl/install.py index 9e7bf9df..924a73b5 100644 --- a/jrnl/install.py +++ b/jrnl/install.py @@ -77,8 +77,8 @@ def load_or_install_jrnl(): config_path = CONFIG_FILE_PATH if os.path.exists(CONFIG_FILE_PATH) else CONFIG_FILE_PATH_FALLBACK if os.path.exists(config_path): config = util.load_config(CONFIG_FILE_PATH) - upgrade_config(config) upgrade.upgrade_jrnl_if_necessary(CONFIG_FILE_PATH) + upgrade_config(config) return config else: install() diff --git a/jrnl/upgrade.py b/jrnl/upgrade.py index b14adf39..cc326bd2 100644 --- a/jrnl/upgrade.py +++ b/jrnl/upgrade.py @@ -4,12 +4,15 @@ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes import hashlib import util from . import __version__ +from . import EncryptedJournal import sys +from cryptography.fernet import Fernet def upgrade_encrypted_journal(filename, key_plain): """Decrypts a journal in memory using the jrnl 1.x encryption scheme and returns it in plain text.""" + util.prompt( "UPGRADING JOURNAL") with open(filename) as f: iv_cipher = f.read() iv, cipher = iv_cipher[:16], iv_cipher[16:] @@ -23,6 +26,11 @@ def upgrade_encrypted_journal(filename, key_plain): plain += unpadder.finalize() except ValueError: return None + + key = EncryptedJournal.make_key(key_plain) + journal = Fernet(key).encrypt(plain.encode('utf-8')) + with open(filename, 'w') as f: + f.write(journal) return plain @@ -44,12 +52,15 @@ def upgrade_jrnl_if_necessary(config_path): plain_journals = {} for journal, journal_conf in config['journals'].items(): if isinstance(journal_conf, dict): - if journal_conf.get("encrypted"): + if journal_conf.get("encrypt"): encrypted_journals[journal] = journal_conf.get("journal") else: plain_journals[journal] = journal_conf.get("journal") else: - plain_journals[journal] = journal_conf.get("journal") + if config.get('encrypt'): + encrypted_journals[journal] = journal_conf + else: + plain_journals[journal] = journal_conf if encrypted_journals: util.prompt("Following encrypted journals will be upgraded to jrnl {}:".format(__version__)) for journal, path in encrypted_journals.items(): @@ -63,3 +74,7 @@ def upgrade_jrnl_if_necessary(config_path): if not cont: util.prompt("jrnl NOT upgraded, exiting.") sys.exit(1) + + for journal, path in encrypted_journals.items(): + util.prompt("Enter password for {} journal (stored in {}).".format(journal, path)) + util.get_password(keychain=journal, validator=lambda pwd: upgrade_encrypted_journal(path, pwd)) diff --git a/jrnl/util.py b/jrnl/util.py index bc94f24b..a2216a2b 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -79,6 +79,11 @@ def py2encode(s): return s.encode("utf-8") if PY2 and type(s) is unicode else s +def prnt(s): + """Encode and print a string""" + STDOUT.write(u(s + "\n")) + + def prompt(msg): """Prints a message to the std err stream defined in util.""" if not msg.endswith("\n"):