Introduce legacy classes

This commit is contained in:
Manuel Ebert 2015-04-05 07:25:22 +10:00
parent df82ad1f4d
commit a5f08e6081
6 changed files with 77 additions and 52 deletions

View file

@ -36,6 +36,7 @@
""" """
Y Y
bad doggie no biscuit bad doggie no biscuit
bad doggie no biscuit
""" """
Then we should see the message "Password" Then we should see the message "Password"
and the output should contain "2013-06-10 15:40 Life is good" and the output should contain "2013-06-10 15:40 Life is good"

View file

@ -1,6 +1,8 @@
from . import Journal, util from . import Journal, util
from cryptography.fernet import Fernet, InvalidToken from cryptography.fernet import Fernet, InvalidToken
from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import hashes, padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
import hashlib
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
import base64 import base64
@ -38,7 +40,9 @@ class EncryptedJournal(Journal.Journal):
def validate_password(password): def validate_password(password):
key = make_key(password) key = make_key(password)
try: try:
return Fernet(key).decrypt(journal_encrypted).decode('utf-8') plain = Fernet(key).decrypt(journal_encrypted).decode('utf-8')
self.config['password'] = password
return plain
except (InvalidToken, IndexError): except (InvalidToken, IndexError):
return None return None
if password: if password:
@ -57,3 +61,33 @@ class EncryptedJournal(Journal.Journal):
dummy = Fernet(key).encrypt("") dummy = Fernet(key).encrypt("")
with open(filename, 'w') as f: with open(filename, 'w') as f:
f.write(dummy) f.write(dummy)
class LegacyEncryptedJournal(Journal.Journal):
def __init__(self, name='default', **kwargs):
super(LegacyEncryptedJournal, self).__init__(name, **kwargs)
self.config['encrypt'] = True
def _load(self, filename, password=None):
with open(filename) as f:
journal_encrypted = f.read()
iv, cipher = journal_encrypted[:16], journal_encrypted[16:]
def validate_password(password):
decryption_key = hashlib.sha256(password.encode('utf-8')).digest()
decryptor = Cipher(algorithms.AES(decryption_key), modes.CBC(iv), default_backend()).decryptor()
try:
plain_padded = decryptor.update(cipher) + decryptor.finalize()
self.config['password'] = password
if plain_padded[-1] == " ":
# Ancient versions of jrnl. Do not judge me.
plain = plain_padded.rstrip(" ")
else:
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
plain = unpadder.update(plain_padded) + unpadder.finalize()
return plain.decode('utf-8')
except ValueError:
return None
if password:
return validate_password(password)
return util.get_password(keychain=self.name, validator=validate_password)

View file

@ -10,6 +10,9 @@ import sys
import codecs import codecs
import re import re
from datetime import datetime from datetime import datetime
import logging
log = logging.getLogger("jrnl")
class Journal(object): class Journal(object):
@ -33,6 +36,15 @@ class Journal(object):
"""Returns the number of entries""" """Returns the number of entries"""
return len(self.entries) return len(self.entries)
@classmethod
def from_journal(cls, other):
"""Creates a new journal by copying configuration and entries from
another journal object"""
new_journal = cls(other.name, **other.config)
new_journal.entries = other.entries
log.debug("Imported %d entries from %s to %s", len(new_journal), other.__class__.__name__, cls.__name__)
return new_journal
def import_(self, other_journal_txt): def import_(self, other_journal_txt):
self.entries = list(frozenset(self.entries) | frozenset(self._parse(other_journal_txt))) self.entries = list(frozenset(self.entries) | frozenset(self._parse(other_journal_txt)))
self.sort() self.sort()
@ -44,6 +56,7 @@ class Journal(object):
text = self._load(filename) text = self._load(filename)
self.entries = self._parse(text) self.entries = self._parse(text)
self.sort() self.sort()
log.debug("opened %s with %d entries", self.__class__.__name__, len(self))
return self return self
def write(self, filename=None): def write(self, filename=None):
@ -72,7 +85,6 @@ class Journal(object):
# Initialise our current entry # Initialise our current entry
entries = [] entries = []
current_entry = None current_entry = None
for line in journal_txt.splitlines(): for line in journal_txt.splitlines():
line = line.rstrip() line = line.rstrip()
try: try:
@ -94,7 +106,7 @@ class Journal(object):
# Happens when we can't parse the start of the line as an date. # Happens when we can't parse the start of the line as an date.
# In this case, just append line to our body. # In this case, just append line to our body.
if current_entry: if current_entry:
current_entry.body += line + "\n" current_entry.body += line + u"\n"
# Append last entry # Append last entry
if current_entry: if current_entry:
@ -243,10 +255,21 @@ class PlainJournal(Journal):
f.write(text) f.write(text)
def open_journal(name, config): def open_journal(name, config, legacy=False):
""" """
Creates a normal, encrypted or DayOne journal based on the passed config. Creates a normal, encrypted or DayOne journal based on the passed config.
If legacy is True, it will open Journals with legacy classes build for
backwards compatibility with jrnl 1.x
""" """
config = config.copy()
journal_conf = config['journals'].get(name)
if type(journal_conf) is dict: # We can override the default config on a by-journal basis
log.debug('Updating configuration with specific journal overrides %s', journal_conf)
config.update(journal_conf)
else: # But also just give them a string to point to the journal file
config['journal'] = journal_conf
config['journal'] = os.path.expanduser(os.path.expandvars(config['journal']))
if os.path.isdir(config['journal']): if os.path.isdir(config['journal']):
if config['journal'].strip("/").endswith(".dayone") or "entries" in os.listdir(config['journal']): if config['journal'].strip("/").endswith(".dayone") or "entries" in os.listdir(config['journal']):
from . import DayOneJournal from . import DayOneJournal
@ -259,4 +282,6 @@ def open_journal(name, config):
return PlainJournal(name, **config).open() return PlainJournal(name, **config).open()
else: else:
from . import EncryptedJournal from . import EncryptedJournal
if legacy:
return EncryptedJournal.LegacyEncryptedJournal(name, **config).open()
return EncryptedJournal.EncryptedJournal(name, **config).open() return EncryptedJournal.EncryptedJournal(name, **config).open()

View file

@ -18,7 +18,7 @@ import argparse
import sys import sys
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger("jrnl")
def parse_args(args=None): def parse_args(args=None):
@ -147,7 +147,6 @@ def run(manual_args=None):
sys.exit(0) sys.exit(0)
config = install.load_or_install_jrnl() config = install.load_or_install_jrnl()
if args.ls: if args.ls:
util.prnt(u"Journals defined in {}".format(install.CONFIG_FILE_PATH)) util.prnt(u"Journals defined in {}".format(install.CONFIG_FILE_PATH))
ml = min(max(len(k) for k in config['journals']), 20) ml = min(max(len(k) for k in config['journals']), 20)
@ -163,6 +162,7 @@ def run(manual_args=None):
journal_name = args.text[0] if (args.text and args.text[0] in config['journals']) else 'default' journal_name = args.text[0] if (args.text and args.text[0] in config['journals']) else 'default'
if journal_name is not 'default': if journal_name is not 'default':
args.text = args.text[1:] args.text = args.text[1:]
# If the first remaining argument looks like e.g. '-3', interpret that as a limiter # If the first remaining argument looks like e.g. '-3', interpret that as a limiter
if not args.limit and args.text and args.text[0].startswith("-"): if not args.limit and args.text and args.text[0].startswith("-"):
try: try:
@ -172,16 +172,7 @@ def run(manual_args=None):
pass pass
log.debug('Using journal "%s"', journal_name) log.debug('Using journal "%s"', journal_name)
journal_conf = config['journals'].get(journal_name)
if type(journal_conf) is dict: # We can override the default config on a by-journal basis
log.debug('Updating configuration with specific jourlnal overrides %s', journal_conf)
config.update(journal_conf)
else: # But also just give them a string to point to the journal file
config['journal'] = journal_conf
config['journal'] = os.path.expanduser(os.path.expandvars(config['journal']))
touch_journal(config['journal'])
mode_compose, mode_export, mode_import = guess_mode(args, config) mode_compose, mode_export, mode_import = guess_mode(args, config)
log.debug('Using journal path %(journal)s', config)
# How to quit writing? # How to quit writing?
if "win32" in sys.platform: if "win32" in sys.platform:
@ -206,6 +197,7 @@ def run(manual_args=None):
else: else:
mode_compose = False mode_compose = False
# This is where we finally open the journal!
journal = Journal.open_journal(journal_name, config) journal = Journal.open_journal(journal_name, config)
# Import mode # Import mode

View file

@ -65,7 +65,7 @@ def upgrade_config(config):
for key in missing_keys: for key in missing_keys:
config[key] = default_config[key] config[key] = default_config[key]
save_config(config) save_config(config)
print("[.jrnl_conf updated to newest version at {}]".format(CONFIG_FILE_PATH)) print("[Configuration updated to newest version at {}]".format(CONFIG_FILE_PATH))
def save_config(config): def save_config(config):

View file

@ -1,37 +1,8 @@
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
import hashlib
import util import util
from . import __version__ from . import __version__
from . import EncryptedJournal from . import Journal
import sys import sys
from cryptography.fernet import Fernet from .EncryptedJournal import EncryptedJournal
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."""
with open(filename) as f:
iv_cipher = f.read()
iv, cipher = iv_cipher[:16], iv_cipher[16:]
decryption_key = hashlib.sha256(key_plain.encode('utf-8')).digest()
decryptor = Cipher(algorithms.AES(decryption_key), modes.CBC(iv), default_backend()).decryptor()
try:
plain_padded = decryptor.update(cipher) + decryptor.finalize()
if plain_padded[-1] == " ":
# Ancient versions of jrnl. Do not judge me.
plain = plain_padded.rstrip(" ")
else:
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
plain = unpadder.update(plain_padded) + unpadder.finalize()
except ValueError:
return None
key = EncryptedJournal.make_key(key_plain)
journal = Fernet(key).encrypt(plain)
with open(filename, 'w') as f:
f.write(journal)
return plain
def upgrade_jrnl_if_necessary(config_path): def upgrade_jrnl_if_necessary(config_path):
@ -57,7 +28,6 @@ Please note that jrnl 1.x is NOT forward compatible with this version of jrnl.
If you choose to proceed, you will not be able to use your journals with If you choose to proceed, you will not be able to use your journals with
older versions of jrnl anymore. older versions of jrnl anymore.
""".format(__version__)) """.format(__version__))
encrypted_journals = {} encrypted_journals = {}
plain_journals = {} plain_journals = {}
for journal, journal_conf in config['journals'].items(): for journal, journal_conf in config['journals'].items():
@ -86,9 +56,12 @@ older versions of jrnl anymore.
util.prompt("jrnl NOT upgraded, exiting.") util.prompt("jrnl NOT upgraded, exiting.")
sys.exit(1) sys.exit(1)
for journal, path in encrypted_journals.items(): for journal_name, path in encrypted_journals.items():
util.prompt("Enter password for {} journal (stored in {}).".format(journal, path)) util.prompt("Upgrading {} journal (stored in {}).".format(journal_name, path))
util.get_password(keychain=journal, validator=lambda pwd: upgrade_encrypted_journal(path, pwd)) old_journal = Journal.open_journal(journal_name, config, legacy=True)
new_journal = EncryptedJournal.from_journal(old_journal)
new_journal.write()
# util.get_password(keychain=journal, validator=lambda pwd: upgrade_encrypted_journal(path, pwd))
with open(config_path + ".backup", 'w') as config_backup: with open(config_path + ".backup", 'w') as config_backup:
config_backup.write(config_file) config_backup.write(config_file)