Make Journal a general interface

This change moves the loading and saving mechanisms into its own
Plain- and EncryptedJournal subclasses for easier maintenance and lazy loading
of all the crypto modules.
This commit is contained in:
Matthias Vogelgesang 2014-06-26 12:41:30 +02:00
parent beeda149d4
commit 9d589f3e64
3 changed files with 148 additions and 101 deletions

View file

@ -8,14 +8,6 @@ from . import time
import codecs
import re
from datetime import datetime
import sys
try:
from Crypto.Cipher import AES
from Crypto import Random
crypto_installed = True
except ImportError:
crypto_installed = False
import hashlib
class Journal(object):
@ -32,82 +24,33 @@ class Journal(object):
}
self.config.update(kwargs)
# Set up date parser
self.key = None # used to decrypt and encrypt the journal
self.search_tags = None # Store tags we're highlighting
self.name = name
self.open()
def __len__(self):
"""Returns the number of entries"""
return len(self.entries)
def _decrypt(self, cipher):
"""Decrypts a cipher string using self.key as the key and the first 16 byte of the cipher as the IV"""
if not crypto_installed:
sys.exit("Error: PyCrypto is not installed.")
if not cipher:
return ""
crypto = AES.new(self.key, AES.MODE_CBC, cipher[:16])
try:
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)
padding_length = util.byte2int(plain[-1])
if padding_length > AES.block_size and padding_length != 32:
# 32 is the space character and is kept for backwards compatibility
return None
elif padding_length == 32:
plain = plain.strip()
elif plain[-padding_length:] != util.int2byte(padding_length) * padding_length:
# Invalid padding!
return None
else:
plain = plain[:-padding_length]
return plain.decode("utf-8")
def _encrypt(self, plain):
"""Encrypt a plaintext string using self.key as the key"""
if not crypto_installed:
sys.exit("Error: PyCrypto is not installed.")
Random.atfork() # A seed for PyCrypto
iv = Random.new().read(AES.block_size)
crypto = AES.new(self.key, AES.MODE_CBC, iv)
plain = plain.encode("utf-8")
padding_length = AES.block_size - len(plain) % AES.block_size
plain += util.int2byte(padding_length) * padding_length
return iv + crypto.encrypt(plain)
def make_key(self, password):
"""Creates an encryption key from the default password or prompts for a new password."""
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']
if self.config['encrypt']:
with open(filename, "rb") as f:
journal_encrypted = f.read()
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 journal is None:
journal = util.get_password(keychain=self.name, validator=validate_password)
else:
with codecs.open(filename, "r", "utf-8") as f:
journal = f.read()
self.entries = self._parse(journal)
text = self._load(filename)
self.entries = self._parse(text)
self.sort()
return self
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])
self._store(filename, text)
def _load(self, filename):
raise NotImplementedError
def _store(self, filename, text):
raise NotImplementedError
def _parse(self, journal_txt):
"""Parses a journal that's stored in a string and returns a list of entries"""
@ -173,18 +116,6 @@ class Journal(object):
def __repr__(self):
return "<Journal with {0} entries>".format(len(self.entries))
def write(self, filename=None):
"""Dumps the journal into the config file, overwriting it"""
filename = filename or self.config['journal']
journal = "\n".join([e.__unicode__() for e in self.entries])
if self.config['encrypt']:
journal = self._encrypt(journal)
with open(filename, 'wb') as journal_file:
journal_file.write(journal)
else:
with codecs.open(filename, 'w', "utf-8") as journal_file:
journal_file.write(journal)
def sort(self):
"""Sorts the Journal's entries by date"""
self.entries = sorted(self.entries, key=lambda entry: entry.date)
@ -280,3 +211,16 @@ class Journal(object):
for entry in mod_entries:
entry.modified = not any(entry == old_entry for old_entry in self.entries)
self.entries = mod_entries
class PlainJournal(Journal):
def __init__(self, name='default', **kwargs):
super(PlainJournal, self).__init__(name, **kwargs)
def _load(self, filename):
with codecs.open(filename, "r", "utf-8") as f:
return f.read()
def _store(self, filename, text):
with codecs.open(filename, 'w', "utf-8") as f:
f.write(text)