From d38937e672b5978d7614350b318eea8ba7646a4e Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Wed, 16 Apr 2014 17:03:24 -0400 Subject: [PATCH 1/3] Use PKCS#7 for encryption Closes #156 --- jrnl/Journal.py | 17 +++++++++++++---- jrnl/util.py | 4 ++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 7c83b2af..4fdffa14 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -66,11 +66,19 @@ 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 = ord(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: - return plain.decode("utf-8") + plain = plain[:-padding_length] + return plain.decode("utf-8") def _encrypt(self, plain): """Encrypt a plaintext string using self.key as the key""" @@ -80,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): diff --git a/jrnl/util.py b/jrnl/util.py index 4b252cbd..3a92f0e4 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -141,3 +141,7 @@ 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,)) From 8b7a37a196cf9de4a4ae1ef048631df04301660c Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Wed, 16 Apr 2014 17:03:29 -0400 Subject: [PATCH 2/3] Version bump & docs --- CHANGELOG.md | 1 + docs/encryption.rst | 4 +++- jrnl/__init__.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3aff2011..14b634d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Changelog ### 1.7 (December 22, 2013) +* __1.7.21__ jrnl now uses PKCS#7 padding. * __1.7.20__ Minor fixes when parsing DayOne journals * __1.7.19__ Creates full path to journal during installation if it doesn't exist yet * __1.7.18__ Small update to parsing regex diff --git a/docs/encryption.rst b/docs/encryption.rst index df4781f8..9d420df6 100644 --- a/docs/encryption.rst +++ b/docs/encryption.rst @@ -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") diff --git a/jrnl/__init__.py b/jrnl/__init__.py index 7bb44098..eef08dc0 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -8,7 +8,7 @@ jrnl is a simple journal application for your command line. from __future__ import absolute_import __title__ = 'jrnl' -__version__ = '1.7.20' +__version__ = '1.7.21' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 - 2014 Manuel Ebert' From 32f1d35c93483c50dad95e630946269aa5beb94c Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Wed, 16 Apr 2014 17:14:57 -0400 Subject: [PATCH 3/3] byte2int for PY3 --- jrnl/Journal.py | 2 +- jrnl/util.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 4fdffa14..bd1eab52 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -67,7 +67,7 @@ class Journal(object): util.prompt("ERROR: Your journal file seems to be corrupted. You do have a backup, don't you?") sys.exit(1) - padding_length = ord(plain[-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 diff --git a/jrnl/util.py b/jrnl/util.py index 3a92f0e4..315666b3 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -145,3 +145,10 @@ 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 +