diff --git a/jrnl/EncryptedJournal.py b/jrnl/EncryptedJournal.py index e0bce398..2a7092d2 100644 --- a/jrnl/EncryptedJournal.py +++ b/jrnl/EncryptedJournal.py @@ -1,57 +1,11 @@ import hashlib -import sys from . import Journal, util -try: - from Crypto.Cipher import AES - from Crypto import Random - crypto_installed = True -except ImportError: - crypto_installed = False +from cryptography.fernet import Fernet, InvalidToken +import base64 def make_key(password): - return hashlib.sha256(password.encode("utf-8")).digest() - - -def _decrypt(cipher, key): - """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(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(plain, key): - """Encrypt a plaintext string using 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(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) + return base64.urlsafe_b64encode(hashlib.sha256(password.encode("utf-8")).digest()) class EncryptedJournal(Journal.Journal): @@ -60,18 +14,20 @@ class EncryptedJournal(Journal.Journal): self.config['encrypt'] = True def _load(self, filename): - with open(filename, "rb") as f: + with open(filename) as f: journal_encrypted = f.read() def validate_password(password): key = make_key(password) - return _decrypt(journal_encrypted, key) + try: + return Fernet(key).decrypt(journal_encrypted) + except InvalidToken: + return None return util.get_password(keychain=self.name, validator=validate_password) def _store(self, filename, text): key = make_key(self.config['password']) - journal = _encrypt(text, key) - - with open(filename, 'wb') as f: + journal = Fernet(key).encrypt(text) + with open(filename, 'w') as f: f.write(journal) diff --git a/jrnl/upgrade.py b/jrnl/upgrade.py index 3039bd87..11e2f2a4 100644 --- a/jrnl/upgrade.py +++ b/jrnl/upgrade.py @@ -1,3 +1,30 @@ +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__ +import sys + +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()) + plain_padded = decryptor.update(cipher) + try: + plain_padded += decryptor.finalize() + unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder() + plain = unpadder.update(plain_padded) + plain += unpadder.finalize() + except ValueError: + return None + return plain + + def upgrade_jrnl_if_necessary(config_path): with open(config_path) as f: config = f.read() diff --git a/setup.py b/setup.py index 584c842c..03c8ce71 100644 --- a/setup.py +++ b/setup.py @@ -84,14 +84,12 @@ setup( "pyxdg>=0.19", "parsedatetime>=1.2", "pytz>=2013b", - "six>=1.6.1", + "six>=1.7.4", + "cryptography==0.5.2", "tzlocal>=1.1", "PyYAML>=3.11", "keyring>=3.3", ] + [p for p, cond in conditional_dependencies.items() if cond], - extras_require = { - "encrypted": "pycrypto>=2.6" - }, long_description=__doc__, entry_points={ 'console_scripts': [