From 34b730a5c9f940d2705d51d955941c82e9ab33dc Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Thu, 17 Oct 2013 14:55:59 -0700 Subject: [PATCH 1/9] Saves password to keyring Closes #96 and deprecates password field in config --- CHANGELOG.md | 4 ++++ jrnl/Journal.py | 41 +++++++++++++---------------------------- jrnl/__init__.py | 2 +- jrnl/install.py | 7 ++++--- jrnl/jrnl.py | 22 +++++++++++----------- jrnl/util.py | 38 +++++++++++++++++++++++++++++++++++++- requirements.txt | 1 + setup.py | 3 ++- 8 files changed, 73 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e231dd9..e94bf47c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ Changelog ========= +#### 1.6.0 + +* [Improved] Passwords are now saved in the key-chain. The `password` field in `.jrnl_config` is soft-deprecated. + #### 1.5.7 * [Improved] The `~` in journal config paths will now expand properly to e.g. `/Users/maebert` diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 11b31952..116b6c9a 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -12,18 +12,13 @@ except ImportError: import parsedatetime.parsedatetime as pdt import re from datetime import datetime import time -try: import simplejson as json -except ImportError: import json import sys -import glob try: from Crypto.Cipher import AES from Crypto import Random crypto_installed = True except ImportError: crypto_installed = False -if "win32" in sys.platform: import pyreadline as readline -else: import readline import hashlib try: import colorama @@ -33,14 +28,13 @@ except ImportError: import plistlib import pytz import uuid - +from functools import partial class Journal(object): - def __init__(self, **kwargs): + def __init__(self, name='default', **kwargs): self.config = { 'journal': "journal.txt", 'encrypt': False, - 'password': "", 'default_hour': 9, 'default_minute': 0, 'timeformat': "%Y-%m-%d %H:%M", @@ -54,7 +48,8 @@ class Journal(object): consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday self.dateparse = pdt.Calendar(consts) self.key = None # used to decrypt and encrypt the journal - self.search_tags = None # Store tags we're highlighting + self.search_tags = None # Store tags we're highlighting + self.name = name journal_txt = self.open() self.entries = self.parse(journal_txt) @@ -77,7 +72,7 @@ class Journal(object): plain = crypto.decrypt(cipher[16:]) except ValueError: util.prompt("ERROR: Your journal file seems to be corrupted. You do have a backup, don't you?") - sys.exit(-1) + sys.exit(1) padding = " ".encode("utf-8") if not plain.endswith(padding): # Journals are always padded return None @@ -95,33 +90,23 @@ class Journal(object): plain += b" " * (AES.block_size - len(plain) % AES.block_size) return iv + crypto.encrypt(plain) - def make_key(self, prompt="Password: "): + def make_key(self, password): """Creates an encryption key from the default password or prompts for a new password.""" - password = self.config['password'] or util.getpass(prompt) self.key = hashlib.sha256(password.encode("utf-8")).digest() def open(self, filename=None): """Opens the journal file defined in the config and parses it into a list of Entries. Entries have the form (date, title, body).""" filename = filename or self.config['journal'] - journal = None + + def validate_password(journal, password): + self.make_key(password) + return self._decrypt(journal) + if self.config['encrypt']: with open(filename, "rb") as f: - journal = f.read() - decrypted = None - attempts = 0 - while decrypted is None: - self.make_key() - decrypted = self._decrypt(journal) - if decrypted is None: - attempts += 1 - self.config['password'] = None # This password doesn't work. - if attempts < 3: - util.prompt("Wrong password, try again.") - else: - util.prompt("Extremely wrong password.") - sys.exit(-1) - journal = decrypted + journal_encrypted = f.read() + journal = util.get_password(keychain=self.name, validator=partial(validate_password, journal_encrypted)) else: with codecs.open(filename, "r", "utf-8") as f: journal = f.read() diff --git a/jrnl/__init__.py b/jrnl/__init__.py index 42dce17e..6124d668 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line. """ __title__ = 'jrnl' -__version__ = '1.5.7' +__version__ = '1.6.0' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 Manuel Ebert' diff --git a/jrnl/install.py b/jrnl/install.py index 2037e8aa..d09e5150 100644 --- a/jrnl/install.py +++ b/jrnl/install.py @@ -73,18 +73,19 @@ def install_jrnl(config_path='~/.jrnl_config'): password = getpass.getpass("Enter password for journal (leave blank for no encryption): ") if password: default_config['encrypt'] = True + if util.yesno("Do you want to store the password in your keychain?", default=True): + util.set_keychain("default", password) print("Journal will be encrypted.") - print("If you want to, you can store your password in .jrnl_config and will never be bothered about it again.") else: password = None - print("PyCrypto not found. To encrypt your journal, install the PyCrypto package from http://www.pycrypto.org and run 'jrnl --encrypt'. For now, your journal will be stored in plain text.") + print("PyCrypto not found. To encrypt your journal, install the PyCrypto package from http://www.pycrypto.org or with 'pip install pycrypto' and run 'jrnl --encrypt'. For now, your journal will be stored in plain text.") # Use highlighting: if not module_exists("colorama"): print("colorama not found. To turn on highlighting, install colorama and set highlight to true in your .jrnl_conf.") default_config['highlight'] = False - open(default_config['journals']['default'], 'a').close() # Touch to make sure it's there + open(default_config['journals']['default'], 'a').close() # Touch to make sure it's there # Write config to ~/.jrnl_conf with open(config_path, 'w') as f: diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index acd5eff9..eec9b227 100755 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -86,10 +86,12 @@ def get_text_from_editor(config): def encrypt(journal, filename=None): """ Encrypt into new file. If filename is not set, we encrypt the journal file itself. """ - journal.config['password'] = "" - journal.make_key(prompt="Enter new password:") + password = util.getpass("Enter new password: ") + journal.make_key(password) journal.config['encrypt'] = True journal.write(filename) + if util.yesno("Do you want to store the password in your keychain?", default=True): + util.set_keychain(journal.name, password) util.prompt("Journal encrypted to {0}.".format(filename or journal.config['journal'])) def decrypt(journal, filename=None): @@ -109,12 +111,11 @@ def update_config(config, new_config, scope): """Updates a config dict with new values - either global if scope is None of config['journals'][scope] is just a string pointing to a journal file, or within the scope""" - if scope and type(config['journals'][scope]) is dict: # Update to journal specific + if scope and type(config['journals'][scope]) is dict: # Update to journal specific config['journals'][scope].update(new_config) else: config.update(new_config) - def cli(manual_args=None): if not os.path.exists(CONFIG_PATH): config = install.install_jrnl(CONFIG_PATH) @@ -142,9 +143,9 @@ def cli(manual_args=None): if journal_name is not 'default': args.text = args.text[1:] journal_conf = config['journals'].get(journal_name) - if type(journal_conf) is dict: # We can override the default config on a by-journal basis + if type(journal_conf) is dict: # We can override the default config on a by-journal basis config.update(journal_conf) - else: # But also just give them a string to point to the journal file + else: # But also just give them a string to point to the journal file config['journal'] = journal_conf config['journal'] = os.path.expanduser(config['journal']) touch_journal(config['journal']) @@ -159,7 +160,7 @@ def cli(manual_args=None): util.prompt("[Error: {0} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal'])) sys.exit(1) else: - journal = Journal.Journal(**config) + journal = Journal.Journal(journal_name, **config) if mode_compose and not args.text: if config['editor']: @@ -205,22 +206,21 @@ def cli(manual_args=None): encrypt(journal, filename=args.encrypt) # Not encrypting to a separate file: update config! if not args.encrypt: - update_config(original_config, {"encrypt": True, "password": ""}, journal_name) + update_config(original_config, {"encrypt": True}, journal_name) install.save_config(original_config, config_path=CONFIG_PATH) elif args.decrypt is not False: decrypt(journal, filename=args.decrypt) # Not decrypting to a separate file: update config! if not args.decrypt: - update_config(original_config, {"encrypt": False, "password": ""}, journal_name) + update_config(original_config, {"encrypt": False}, journal_name) install.save_config(original_config, config_path=CONFIG_PATH) elif args.delete_last: last_entry = journal.entries.pop() util.prompt("[Deleted Entry:]") - print(last_entry) + print(last_entry.pprint()) journal.write() if __name__ == "__main__": cli() - diff --git a/jrnl/util.py b/jrnl/util.py index 4f6031d5..e0667cab 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -4,6 +4,7 @@ import sys import os from tzlocal import get_localzone import getpass as gp +import keyring import pytz PY3 = sys.version_info[0] == 3 @@ -14,12 +15,42 @@ STDOUT = sys.stdout TEST = False __cached_tz = None -def getpass(prompt): +def getpass(prompt="Password: "): if not TEST: return gp.getpass(prompt) else: return py23_input(prompt) +def get_password(validator, keychain=None, max_attempts=3): + pwd_from_keychain = keychain and get_keychain(keychain) + password = pwd_from_keychain or getpass() + result = validator(password) + # Password is bad: + if not result and pwd_from_keychain: + set_keychain(keychain, None) + attempt = 1 + while not result and attempt < max_attempts: + prompt("Wrong password, try again.") + password = getpass() + result = validator(password) + attempt += 1 + if result: + return result + else: + prompt("Extremely wrong password.") + sys.exit(1) + +def get_keychain(journal_name): + return keyring.get_password('jrnl', journal_name) + +def set_keychain(journal_name, password): + if password is None: + try: + keyring.delete_password('jrnl', journal_name) + except: + pass + else: + keyring.set_password('jrnl', journal_name, password) def u(s): """Mock unicode function for python 2 and 3 compatibility.""" @@ -35,6 +66,11 @@ def py23_input(msg): STDERR.write(u(msg)) return STDIN.readline().strip() +def yesno(prompt, default=True): + prompt = prompt.strip() + (" [Yn]" if default else "[yN]") + raw = py23_input(prompt) + return {'y': True, 'n': False}.get(raw.lower(), default) + def get_local_timezone(): """Returns the Olson identifier of the local timezone. In a happy world, tzlocal.get_localzone would do this, but there's a bug on OS X diff --git a/requirements.txt b/requirements.txt index 999b9b05..64c1ed0e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ pycrypto >= 2.6 argparse==1.2.1 tzlocal == 1.0 slugify==0.0.1 +keyring==3.0.5 diff --git a/setup.py b/setup.py index 7f7e5d6c..668a44d9 100644 --- a/setup.py +++ b/setup.py @@ -73,7 +73,8 @@ setup( "pytz>=2013b", "tzlocal==1.0", "slugify>=0.0.1", - "colorama>=0.2.5" + "colorama>=0.2.5", + "keyring>=3.0.5" ] + [p for p, cond in conditional_dependencies.items() if cond], extras_require = { "encrypted": "pycrypto>=2.6" From a96435b6dc7079e64a45a1819ac4058751a4ed7b Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Thu, 17 Oct 2013 16:00:27 -0700 Subject: [PATCH 2/9] Disambiguates jrnl.update_config and install.update_config --- jrnl/install.py | 6 +++--- jrnl/jrnl.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/jrnl/install.py b/jrnl/install.py index d09e5150..0ff6159b 100644 --- a/jrnl/install.py +++ b/jrnl/install.py @@ -1,7 +1,8 @@ #!/usr/bin/env python # encoding: utf-8 -import readline, glob +import readline +import glob import getpass try: import simplejson as json except ImportError: import json @@ -25,7 +26,6 @@ default_config = { }, 'editor': "", 'encrypt': False, - 'password': "", 'default_hour': 9, 'default_minute': 0, 'timeformat': "%Y-%m-%d %H:%M", @@ -35,7 +35,7 @@ default_config = { } -def update_config(config, config_path=os.path.expanduser("~/.jrnl_conf")): +def upgrade_config(config, config_path=os.path.expanduser("~/.jrnl_conf")): """Checks if there are keys missing in a given config dict, and if so, updates the config file accordingly. This essentially automatically ports jrnl installations if new config parameters are introduced in later versions.""" diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index eec9b227..d9337308 100755 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -109,7 +109,7 @@ def touch_journal(filename): def update_config(config, new_config, scope): """Updates a config dict with new values - either global if scope is None - of config['journals'][scope] is just a string pointing to a journal file, + or config['journals'][scope] is just a string pointing to a journal file, or within the scope""" if scope and type(config['journals'][scope]) is dict: # Update to journal specific config['journals'][scope].update(new_config) @@ -127,7 +127,7 @@ def cli(manual_args=None): util.prompt("[There seems to be something wrong with your jrnl config at {0}: {1}]".format(CONFIG_PATH, e.message)) util.prompt("[Entry was NOT added to your journal]") sys.exit(1) - install.update_config(config, config_path=CONFIG_PATH) + install.upgrade_config(config, config_path=CONFIG_PATH) original_config = config.copy() # check if the configuration is supported by available modules From c6c425093e459173970e3200dd476074c6016dd6 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Thu, 17 Oct 2013 16:00:45 -0700 Subject: [PATCH 3/9] Remove tests for having password in config --- features/data/configs/encrypted_with_pw.json | 14 -------------- features/encryption.feature | 6 ------ jrnl/util.py | 4 ++-- 3 files changed, 2 insertions(+), 22 deletions(-) delete mode 100644 features/data/configs/encrypted_with_pw.json diff --git a/features/data/configs/encrypted_with_pw.json b/features/data/configs/encrypted_with_pw.json deleted file mode 100644 index 1a277240..00000000 --- a/features/data/configs/encrypted_with_pw.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "default_hour": 9, - "timeformat": "%Y-%m-%d %H:%M", - "linewrap": 80, - "encrypt": true, - "editor": "", - "default_minute": 0, - "highlight": true, - "password": "bad doggie no biscuit", - "journals": { - "default": "features/journals/encrypted.journal" - }, - "tagsymbols": "@" -} diff --git a/features/encryption.feature b/features/encryption.feature index d134c3bb..f28660b6 100644 --- a/features/encryption.feature +++ b/features/encryption.feature @@ -6,11 +6,6 @@ Then we should see the message "Password" and the output should contain "2013-06-10 15:40 Life is good" - Scenario: Loading an encrypted journal with password in config - Given we use the config "encrypted_with_pw.json" - When we run "jrnl -n 1" - Then the output should contain "2013-06-10 15:40 Life is good" - Scenario: Decrypting a journal Given we use the config "encrypted.json" When we run "jrnl --decrypt" and enter "bad doggie no biscuit" @@ -26,4 +21,3 @@ When we run "jrnl -n 1" and enter "swordfish" Then we should see the message "Password" and the output should contain "2013-06-10 15:40 Life is good" - diff --git a/jrnl/util.py b/jrnl/util.py index e0667cab..ddb4da59 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -49,7 +49,7 @@ def set_keychain(journal_name, password): keyring.delete_password('jrnl', journal_name) except: pass - else: + elif not TEST: keyring.set_password('jrnl', journal_name, password) def u(s): @@ -69,7 +69,7 @@ def py23_input(msg): def yesno(prompt, default=True): prompt = prompt.strip() + (" [Yn]" if default else "[yN]") raw = py23_input(prompt) - return {'y': True, 'n': False}.get(raw.lower(), default) + return {'y': True, 'n': False}.get(raw.lower(), default) def get_local_timezone(): """Returns the Olson identifier of the local timezone. From ab12294777bae65c3a542ec4c1ddcea9b262067d Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Thu, 17 Oct 2013 16:04:34 -0700 Subject: [PATCH 4/9] Only updates config locally on encrypting and decrypting --- jrnl/jrnl.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index d9337308..b82dd54c 100755 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -107,12 +107,15 @@ def touch_journal(filename): util.prompt("[Journal created at {0}]".format(filename)) open(filename, 'a').close() -def update_config(config, new_config, scope): +def update_config(config, new_config, scope, force_local=False): """Updates a config dict with new values - either global if scope is None or config['journals'][scope] is just a string pointing to a journal file, or within the scope""" if scope and type(config['journals'][scope]) is dict: # Update to journal specific config['journals'][scope].update(new_config) + elif scope and force_local: # Convert to dict + config['journals'][scope] = {"journal": config['journals'][scope]} + config['journals'][scope].update(new_config) else: config.update(new_config) @@ -206,14 +209,14 @@ def cli(manual_args=None): encrypt(journal, filename=args.encrypt) # Not encrypting to a separate file: update config! if not args.encrypt: - update_config(original_config, {"encrypt": True}, journal_name) + update_config(original_config, {"encrypt": True}, journal_name, force_local=True) install.save_config(original_config, config_path=CONFIG_PATH) elif args.decrypt is not False: decrypt(journal, filename=args.decrypt) # Not decrypting to a separate file: update config! if not args.decrypt: - update_config(original_config, {"encrypt": False}, journal_name) + update_config(original_config, {"encrypt": False}, journal_name, force_local=True) install.save_config(original_config, config_path=CONFIG_PATH) elif args.delete_last: From 13b7d1c1dffd22ba19081c64236e5622e1bbbfd6 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Thu, 17 Oct 2013 16:26:49 -0700 Subject: [PATCH 5/9] Tests for storing password in keychain --- features/data/configs/multiple.json | 1 + features/encryption.feature | 13 +++++++++++-- features/steps/core.py | 26 +++++++++++++++++++------- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/features/data/configs/multiple.json b/features/data/configs/multiple.json index af7a3e15..980c9353 100644 --- a/features/data/configs/multiple.json +++ b/features/data/configs/multiple.json @@ -9,6 +9,7 @@ "password": "", "journals": { "default": "features/journals/simple.journal", + "simple": "features/journals/simple.journal", "work": "features/journals/work.journal", "ideas": "features/journals/nothing.journal" }, diff --git a/features/encryption.feature b/features/encryption.feature index f28660b6..72749f23 100644 --- a/features/encryption.feature +++ b/features/encryption.feature @@ -9,15 +9,24 @@ Scenario: Decrypting a journal Given we use the config "encrypted.json" When we run "jrnl --decrypt" and enter "bad doggie no biscuit" + Then the config for journal "default" should have "encrypt" set to "bool:False" Then we should see the message "Journal decrypted" and the journal should have 2 entries - and the config should have "encrypt" set to "bool:False" Scenario: Encrypting a journal Given we use the config "basic.json" When we run "jrnl --encrypt" and enter "swordfish" Then we should see the message "Journal encrypted" - and the config should have "encrypt" set to "bool:True" + and the config for journal "default" should have "encrypt" set to "bool:True" When we run "jrnl -n 1" and enter "swordfish" Then we should see the message "Password" and the output should contain "2013-06-10 15:40 Life is good" + + Scenario: Storing a password in Keychain + Given we use the config "multiple.json" + When we run "jrnl simple --encrypt" and enter "sabertooth" + When we set the keychain password of "simple" to "sabertooth" + Then the config for journal "simple" should have "encrypt" set to "bool:True" + When we run "jrnl simple -n 1" + Then we should not see the message "Password" + and the output should contain "2013-06-10 15:40 Life is good" diff --git a/features/steps/core.py b/features/steps/core.py index da39387f..83a14017 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -5,6 +5,7 @@ import os import sys import json import pytz +import keyring try: from io import StringIO except ImportError: @@ -34,11 +35,11 @@ def read_journal(journal_name="default"): def open_journal(journal_name="default"): with open(jrnl.CONFIG_PATH) as config_file: config = json.load(config_file) - journals = config['journals'] - if type(journals) is dict: # We can override the default config on a by-journal basis - config['journal'] = journals.get(journal_name) - else: # But also just give them a string to point to the journal file - config['journal'] = journal + journal_conf = config['journals'][journal_name] + if type(journal_conf) is dict: # We can override the default config on a by-journal basis + config.update(journal_conf) + else: # But also just give them a string to point to the journal file + config['journal'] = journal_conf return Journal.Journal(**config) @given('we use the config "{config_file}"') @@ -68,6 +69,10 @@ def run(context, command): except SystemExit as e: context.exit_status = e.code +@when('we set the keychain password of "{journal}" to "{password}"') +def set_keychain(context, journal, password): + keyring.set_password('jrnl', journal, password) + @then('we should get an error') def has_error(context): assert context.exit_status != 0, context.exit_status @@ -134,6 +139,11 @@ def check_message(context, text): out = context.messages.getvalue() assert text in out, [text, out] +@then('we should not see the message "{text}"') +def check_not_message(context, text): + out = context.messages.getvalue() + assert text not in out, [text, out] + @then('the journal should contain "{text}"') @then('journal "{journal_name}" should contain "{text}"') def check_journal_content(context, text, journal_name="default"): @@ -148,7 +158,8 @@ def journal_doesnt_exist(context, journal_name="default"): assert not os.path.exists(journal_path) @then('the config should have "{key}" set to "{value}"') -def config_var(context, key, value): +@then('the config for journal "{journal}" should have "{key}" set to "{value}"') +def config_var(context, key, value, journal=None): t, value = value.split(":") value = { "bool": lambda v: v.lower() == "true", @@ -157,6 +168,8 @@ def config_var(context, key, value): }[t](value) with open(jrnl.CONFIG_PATH) as config_file: config = json.load(config_file) + if journal: + config = config["journals"][journal] assert key in config assert config[key] == value @@ -171,4 +184,3 @@ def check_journal_content(context, number, journal_name="default"): @then('fail') def debug_fail(context): assert False - From 65fab71bef295bc7fb254ce548b9960dd412ef53 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 20 Oct 2013 13:25:42 -0700 Subject: [PATCH 6/9] User plain text keyring for testing --- features/steps/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/features/steps/core.py b/features/steps/core.py index 83a14017..c66a921f 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -6,6 +6,7 @@ import sys import json import pytz import keyring +keyring.set_keyring(keyring.backends.file.PlaintextKeyring()) try: from io import StringIO except ImportError: From e81e38869647444f753bb6cce9467f0036042eb1 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 20 Oct 2013 13:30:27 -0700 Subject: [PATCH 7/9] Updates readme --- README.md | 1 - jrnl/__init__.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 90f896d5..77ef006f 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,6 @@ The configuration file is a simple JSON file with the following options. - `journals`: paths to your journal files - `editor`: if set, executes this command to launch an external editor for writing your entries, e.g. `vim` or `subl -w` (note the `-w` flag to make sure _jrnl_ waits for Sublime Text to close the file before writing into the journal). - `encrypt`: if `true`, encrypts your journal using AES. -- `password`: you may store the password you used to encrypt your journal in plaintext here. This is useful if your journal file lives in an unsecure space (ie. your Dropbox), but the config file itself is more or less safe. - `tagsymbols`: Symbols to be interpreted as tags. (__See note below__) - `default_hour` and `default_minute`: if you supply a date, such as `last thursday`, but no specific time, the entry will be created at this time - `timeformat`: how to format the timestamps in your journal, see the [python docs](http://docs.python.org/library/time.html#time.strftime) for reference diff --git a/jrnl/__init__.py b/jrnl/__init__.py index 6124d668..4d384eb8 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line. """ __title__ = 'jrnl' -__version__ = '1.6.0' +__version__ = '1.6.0-dev' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 Manuel Ebert' From 30275492d482841a0510d39db0c37045b0f3d033 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 20 Oct 2013 13:42:55 -0700 Subject: [PATCH 8/9] Only soft-deprecate passwords in config --- jrnl/Journal.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 116b6c9a..ecc9a4d7 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -99,14 +99,21 @@ class Journal(object): Entries have the form (date, title, body).""" filename = filename or self.config['journal'] - def validate_password(journal, password): - self.make_key(password) - return self._decrypt(journal) if self.config['encrypt']: with open(filename, "rb") as f: journal_encrypted = f.read() - journal = util.get_password(keychain=self.name, validator=partial(validate_password, journal_encrypted)) + + def validate_password(password): + self.make_key(password) + return self._decrypt(journal_encrypted) + + # Soft-deprecated: + journal = None + if 'password' in self.config: + journal = validate_password(self.config['password']) + if not journal: + journal = util.get_password(keychain=self.name, validator=validate_password) else: with codecs.open(filename, "r", "utf-8") as f: journal = f.read() From 49a540dbbfc72c9458f5c97626c2e73f75decad3 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 20 Oct 2013 13:43:04 -0700 Subject: [PATCH 9/9] Tests for soft-deprecating passwords in config --- features/data/configs/encrypted_with_pw.json | 14 ++++++++++++++ features/encryption.feature | 7 ++++++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 features/data/configs/encrypted_with_pw.json diff --git a/features/data/configs/encrypted_with_pw.json b/features/data/configs/encrypted_with_pw.json new file mode 100644 index 00000000..1a277240 --- /dev/null +++ b/features/data/configs/encrypted_with_pw.json @@ -0,0 +1,14 @@ +{ + "default_hour": 9, + "timeformat": "%Y-%m-%d %H:%M", + "linewrap": 80, + "encrypt": true, + "editor": "", + "default_minute": 0, + "highlight": true, + "password": "bad doggie no biscuit", + "journals": { + "default": "features/journals/encrypted.journal" + }, + "tagsymbols": "@" +} diff --git a/features/encryption.feature b/features/encryption.feature index 72749f23..43d07c26 100644 --- a/features/encryption.feature +++ b/features/encryption.feature @@ -1,4 +1,4 @@ - Feature: Multiple journals + Feature: Encrypted journals Scenario: Loading an encrypted journal Given we use the config "encrypted.json" @@ -22,6 +22,11 @@ Then we should see the message "Password" and the output should contain "2013-06-10 15:40 Life is good" + Scenario: Loading an encrypted journal with password in config + Given we use the config "encrypted_with_pw.json" + When we run "jrnl -n 1" + Then the output should contain "2013-06-10 15:40 Life is good" + Scenario: Storing a password in Keychain Given we use the config "multiple.json" When we run "jrnl simple --encrypt" and enter "sabertooth"