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
bad doggie no biscuit
bad doggie no biscuit
"""
Then we should see the message "Password"
and the output should contain "2013-06-10 15:40 Life is good"

View file

@ -1,6 +1,8 @@
from . import Journal, util
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.backends import default_backend
import base64
@ -38,7 +40,9 @@ class EncryptedJournal(Journal.Journal):
def validate_password(password):
key = make_key(password)
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):
return None
if password:
@ -57,3 +61,33 @@ class EncryptedJournal(Journal.Journal):
dummy = Fernet(key).encrypt("")
with open(filename, 'w') as f:
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 re
from datetime import datetime
import logging
log = logging.getLogger("jrnl")
class Journal(object):
@ -33,6 +36,15 @@ class Journal(object):
"""Returns the number of 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):
self.entries = list(frozenset(self.entries) | frozenset(self._parse(other_journal_txt)))
self.sort()
@ -44,6 +56,7 @@ class Journal(object):
text = self._load(filename)
self.entries = self._parse(text)
self.sort()
log.debug("opened %s with %d entries", self.__class__.__name__, len(self))
return self
def write(self, filename=None):
@ -72,7 +85,6 @@ class Journal(object):
# Initialise our current entry
entries = []
current_entry = None
for line in journal_txt.splitlines():
line = line.rstrip()
try:
@ -94,7 +106,7 @@ class Journal(object):
# Happens when we can't parse the start of the line as an date.
# In this case, just append line to our body.
if current_entry:
current_entry.body += line + "\n"
current_entry.body += line + u"\n"
# Append last entry
if current_entry:
@ -243,10 +255,21 @@ class PlainJournal(Journal):
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.
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 config['journal'].strip("/").endswith(".dayone") or "entries" in os.listdir(config['journal']):
from . import DayOneJournal
@ -259,4 +282,6 @@ def open_journal(name, config):
return PlainJournal(name, **config).open()
else:
from . import EncryptedJournal
if legacy:
return EncryptedJournal.LegacyEncryptedJournal(name, **config).open()
return EncryptedJournal.EncryptedJournal(name, **config).open()

View file

@ -18,7 +18,7 @@ import argparse
import sys
import logging
log = logging.getLogger(__name__)
log = logging.getLogger("jrnl")
def parse_args(args=None):
@ -147,7 +147,6 @@ def run(manual_args=None):
sys.exit(0)
config = install.load_or_install_jrnl()
if args.ls:
util.prnt(u"Journals defined in {}".format(install.CONFIG_FILE_PATH))
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'
if journal_name is not 'default':
args.text = args.text[1:]
# 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("-"):
try:
@ -172,16 +172,7 @@ def run(manual_args=None):
pass
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)
log.debug('Using journal path %(journal)s', config)
# How to quit writing?
if "win32" in sys.platform:
@ -206,6 +197,7 @@ def run(manual_args=None):
else:
mode_compose = False
# This is where we finally open the journal!
journal = Journal.open_journal(journal_name, config)
# Import mode

View file

@ -65,7 +65,7 @@ def upgrade_config(config):
for key in missing_keys:
config[key] = default_config[key]
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):

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
from . import __version__
from . import EncryptedJournal
from . import Journal
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."""
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
from .EncryptedJournal import EncryptedJournal
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
older versions of jrnl anymore.
""".format(__version__))
encrypted_journals = {}
plain_journals = {}
for journal, journal_conf in config['journals'].items():
@ -86,9 +56,12 @@ older versions of jrnl anymore.
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))
for journal_name, path in encrypted_journals.items():
util.prompt("Upgrading {} journal (stored in {}).".format(journal_name, path))
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:
config_backup.write(config_file)