Updated docs from master

This commit is contained in:
Manuel Ebert 2014-05-19 14:48:34 -07:00
parent 8a845e778f
commit fee1a95fcb
6 changed files with 53 additions and 22 deletions

View file

@ -29,7 +29,7 @@ If you don't initially store the password in the keychain but decide to do so at
Manual decryption
-----------------
Should you ever want to decrypt your journal manually, you can do so with any program that supports the AES algorithm. The key used for encryption is the SHA-256-hash of your password, and the IV (initialisation vector) is stored in the first 16 bytes of the encrypted file. So, to decrypt a journal file in python, run::
Should you ever want to decrypt your journal manually, you can do so with any program that supports the AES algorithm in CBC. The key used for encryption is the SHA-256-hash of your password, the IV (initialisation vector) is stored in the first 16 bytes of the encrypted file. The plain text is encoded in UTF-8 and padded according to PKCS#7 before being encrypted. So, to decrypt a journal file in python, run::
import hashlib, Crypto.Cipher
key = hashlib.sha256(my_password).digest()
@ -37,3 +37,5 @@ Should you ever want to decrypt your journal manually, you can do so with any pr
cipher = f.read()
crypto = AES.new(key, AES.MODE_CBC, iv = cipher[:16])
plain = crypto.decrypt(cipher[16:])
plain = plain.strip(plain[-1])
plain = plain.decode("utf-8")

View file

@ -24,6 +24,7 @@ import plistlib
import pytz
import uuid
import tzlocal
from xml.parsers.expat import ExpatError
class Journal(object):
@ -65,10 +66,18 @@ class Journal(object):
except ValueError:
util.prompt("ERROR: Your journal file seems to be corrupted. You do have a backup, don't you?")
sys.exit(1)
padding = " ".encode("utf-8")
if not plain.endswith(padding): # Journals are always padded
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):
@ -79,7 +88,8 @@ class Journal(object):
iv = Random.new().read(AES.block_size)
crypto = AES.new(self.key, AES.MODE_CBC, iv)
plain = plain.encode("utf-8")
plain += b" " * (AES.block_size - len(plain) % AES.block_size)
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):
@ -280,7 +290,7 @@ class Journal(object):
raw = raw.replace('\\n ', '\n').replace('\\n', '\n')
starred = False
# Split raw text into title and body
sep = re.search("\n|[\?!.]+ *\n?", raw)
sep = re.search("\n|[\?!.]+ +\n?", raw)
title, body = (raw[:sep.end()], raw[sep.end():]) if sep else (raw, "")
starred = False
if not date:
@ -332,15 +342,19 @@ class DayOne(Journal):
self.entries = []
for filename in filenames:
with open(filename, 'rb') as plist_entry:
try:
dict_entry = plistlib.readPlist(plist_entry)
except ExpatError:
pass
else:
try:
timezone = pytz.timezone(dict_entry['Time Zone'])
except (KeyError, pytz.exceptions.UnknownTimeZoneError):
timezone = tzlocal.get_localzone()
date = dict_entry['Creation Date']
date = date + timezone.utcoffset(date)
date = date + timezone.utcoffset(date, is_dst=False)
raw = dict_entry['Entry Text']
sep = re.search("[\n!?.]+", raw)
sep = re.search("\n|[\?!.]+ +\n?", raw)
title, body = (raw[:sep.end()], raw[sep.end():]) if sep else (raw, "")
entry = Entry.Entry(self, date, title, body, starred=dict_entry["Starred"])
entry.uuid = dict_entry["UUID"]

View file

@ -8,7 +8,7 @@ jrnl is a simple journal application for your command line.
from __future__ import absolute_import
__title__ = 'jrnl'
__version__ = '1.7.19'
__version__ = '1.7.22'
__author__ = 'Manuel Ebert'
__license__ = 'MIT License'
__copyright__ = 'Copyright 2013 - 2014 Manuel Ebert'

View file

@ -233,6 +233,9 @@ def run(manual_args=None):
install.save_config(original_config, config_path=CONFIG_PATH)
elif args.edit:
if not config['editor']:
util.prompt(u"[You need to specify an editor in {0} to use the --edit function.]".format(CONFIG_PATH))
sys.exit(1)
other_entries = [e for e in old_entries if e not in journal.entries]
# Edit
old_num_entries = len(journal)

View file

@ -5,6 +5,7 @@ from __future__ import absolute_import
import os
import json
from .util import u, slugify
import codecs
def get_tags_count(journal):
@ -81,7 +82,7 @@ def export(journal, format, output=None):
content = maps[format](journal)
if output:
try:
with open(output, 'w') as f:
with codecs.open(output, "w", "utf-8") as f:
f.write(content)
return "[Journal exported to {0}]".format(output)
except IOError as e:
@ -101,6 +102,6 @@ def write_files(journal, path, format):
content = e.to_md()
elif format == 'txt':
content = u(e)
with open(full_path, 'w') as f:
with codecs.open(full_path, "w", "utf-8") as f:
f.write(content)
return "[Journal exported individual files in {0}]".format(path)

View file

@ -141,3 +141,14 @@ def slugify(string):
slug = re.sub(r'[-\s]+', '-', no_punctuation)
return u(slug)
def int2byte(i):
"""Converts an integer to a byte.
This is equivalent to chr() in Python 2 and bytes((i,)) in Python 3."""
return chr(i) if PY2 else bytes((i,))
def byte2int(b):
"""Converts a byte to an integer.
This is equivalent to ord(bs[0]) on Python 2 and bs[0] on Python 3."""
return ord(b)if PY2 else b