From a76f066a7c06b898f2041889530971bd699959be Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Sat, 24 Sep 2022 08:08:11 -0700 Subject: [PATCH] more cleaning --- jrnl/EncryptedJournal.py | 49 ----------------------- jrnl/Journal.py | 19 +++++---- jrnl/commands.py | 24 +++++------ jrnl/encryption/BaseEncryption.py | 8 ++-- jrnl/encryption/BasePasswordEncryption.py | 24 ++++++++--- jrnl/encryption/Jrnlv1Encryption.py | 37 +++++++++++++++-- jrnl/encryption/Jrnlv2Encryption.py | 32 ++++++++++----- jrnl/upgrade.py | 3 +- 8 files changed, 101 insertions(+), 95 deletions(-) delete mode 100644 jrnl/EncryptedJournal.py diff --git a/jrnl/EncryptedJournal.py b/jrnl/EncryptedJournal.py deleted file mode 100644 index 2d59c147..00000000 --- a/jrnl/EncryptedJournal.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright © 2012-2022 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import hashlib - -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import padding -from cryptography.hazmat.primitives.ciphers import Cipher -from cryptography.hazmat.primitives.ciphers import algorithms -from cryptography.hazmat.primitives.ciphers import modes - -from jrnl.Journal import LegacyJournal - - -class LegacyEncryptedJournal(LegacyJournal): - """Legacy class to support opening journals encrypted with the jrnl 1.x - standard. You'll not be able to save these journals anymore.""" - - def __init__(self, name="default", **kwargs): - super().__init__(name, **kwargs) - self.config["encrypt"] = True - self.password = None - - def _load(self, filename): - with open(filename, "rb") as f: - journal_encrypted = f.read() - iv, cipher = journal_encrypted[:16], journal_encrypted[16:] - - def decrypt_journal(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.password = password - if plain_padded[-1] in (" ", 32): - # Ancient versions of jrnl. Do not judge me. - return plain_padded.decode("utf-8").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 self.password: - return decrypt_journal(self.password) - return decrypt_content(keychain=self.name, decrypt_func=decrypt_journal) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 626fdd7d..0e4faeed 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -6,7 +6,6 @@ import logging import os import re -from jrnl import EncryptedJournal from jrnl import Entry from jrnl import time from jrnl.encryption import determine_encryption_method @@ -48,7 +47,7 @@ class Journal: self.search_tags = None # Store tags we're highlighting self.name = name self.entries = [] - self._encryption_method = None + self.encryption_method = None def __len__(self): """Returns the number of entries""" @@ -81,21 +80,20 @@ class Journal: self.sort() def _get_encryption_method(self): - self._encryption_method = determine_encryption_method(self.config["encrypt"])( - self.config - ) + encryption_method = determine_encryption_method(self.config["encrypt"]) + self.encryption_method = encryption_method(self.config) def _decrypt(self, text): - if not self._encryption_method: + if self.encryption_method is None: self._get_encryption_method() - return self._encryption_method.decrypt(text) + return self.encryption_method.decrypt(text) def _encrypt(self, text): - if not self._encryption_method: + if self.encryption_method is None: self._get_encryption_method() - return self._encryption_method.encrypt(text) + return self.encryption_method.encrypt(text) def open(self, filename=None): """Opens the journal file defined in the config and parses it into a list of Entries. @@ -488,5 +486,6 @@ def open_journal(journal_name, config, legacy=False): return PlainJournal(journal_name, **config).open() if legacy: - return EncryptedJournal.LegacyEncryptedJournal(journal_name, **config).open() + config["encrypt"] = "jrnlv1" + return LegacyJournal(journal_name, **config).open() return PlainJournal(journal_name, **config).open() diff --git a/jrnl/commands.py b/jrnl/commands.py index 78c32c09..af373531 100644 --- a/jrnl/commands.py +++ b/jrnl/commands.py @@ -14,6 +14,7 @@ run. Also, please note that all (non-builtin) imports should be scoped to each function to avoid any possible overhead for these standalone commands. """ +import logging import platform import sys @@ -22,7 +23,6 @@ from jrnl.messages import Message from jrnl.messages import MsgStyle from jrnl.messages import MsgText from jrnl.output import print_msg -from jrnl.prompt import create_password def preconfig_diagnostic(_): @@ -79,7 +79,6 @@ def postconfig_encrypt(args, config, original_config, **kwargs): """ from jrnl.config import update_config from jrnl.install import save_config - from jrnl.Journal import PlainJournal from jrnl.Journal import open_journal # Open the journal @@ -97,21 +96,21 @@ def postconfig_encrypt(args, config, original_config, **kwargs): ) ) - new_journal = PlainJournal.from_journal(journal) - # If journal is encrypted, create new password if journal.config["encrypt"] is True: + logging.debug("Journal already encrypted. Re-encrypting...") print(f"Journal {journal.name} is already encrypted. Create a new password.") - new_journal.password = create_password(new_journal.name) + logging.debug("Clearing encryption method...") + journal.encryption_method = None journal.config["encrypt"] = True - new_journal.write(args.filename) + journal.write(args.filename) print_msg( Message( MsgText.JournalEncryptedTo, MsgStyle.NORMAL, - {"path": args.filename or new_journal.config["journal"]}, + {"path": args.filename or journal.config["journal"]}, ) ) @@ -127,19 +126,20 @@ def postconfig_decrypt(args, config, original_config, **kwargs): """Decrypts into new file. If filename is not set, we encrypt the journal file itself.""" from jrnl.config import update_config from jrnl.install import save_config - from jrnl.Journal import PlainJournal from jrnl.Journal import open_journal journal = open_journal(args.journal_name, config) - journal.config["encrypt"] = False - new_journal = PlainJournal.from_journal(journal) - new_journal.write(args.filename) + logging.debug("Clearing encryption method...") + journal.config["encrypt"] = False + journal.encryption_method = None + + journal.write(args.filename) print_msg( Message( MsgText.JournalDecryptedTo, MsgStyle.NORMAL, - {"path": args.filename or new_journal.config["journal"]}, + {"path": args.filename or journal.config["journal"]}, ) ) diff --git a/jrnl/encryption/BaseEncryption.py b/jrnl/encryption/BaseEncryption.py index 34f59bcb..d54440b4 100644 --- a/jrnl/encryption/BaseEncryption.py +++ b/jrnl/encryption/BaseEncryption.py @@ -9,15 +9,15 @@ class BaseEncryption(ABC): self._config = config @abstractmethod - def encrypt(self, text: str) -> bytes: + def encrypt(self, text: str) -> str: pass @abstractmethod - def decrypt(self, text: bytes) -> str | None: + def decrypt(self, text: str) -> str | None: pass @abstractmethod - def _encrypt(self, text: bytes) -> str: + def _encrypt(self, text: str) -> str: """ This is needed because self.decrypt might need to perform actions (e.g. prompt for password) @@ -26,7 +26,7 @@ class BaseEncryption(ABC): pass @abstractmethod - def _decrypt(self, text: bytes) -> str: + def _decrypt(self, text: str) -> str: """ This is needed because self.decrypt might need to perform actions (e.g. prompt for password) diff --git a/jrnl/encryption/BasePasswordEncryption.py b/jrnl/encryption/BasePasswordEncryption.py index f6ad4c1c..f8698671 100644 --- a/jrnl/encryption/BasePasswordEncryption.py +++ b/jrnl/encryption/BasePasswordEncryption.py @@ -7,6 +7,7 @@ from jrnl.messages import Message from jrnl.messages import MsgStyle from jrnl.messages import MsgText from jrnl.output import print_msg +from jrnl.prompt import create_password class BasePasswordEncryption(BaseEncryption): @@ -14,26 +15,37 @@ class BasePasswordEncryption(BaseEncryption): _journal_name: str _max_attempts: int _password: str | None + _encoding: str def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._attempts = 0 self._max_attempts = 3 self._password = None + self._encoding = "utf-8" # Check keyring first to be ready for decryption get_keyring_password(self._config["journal"]) # Prompt for password if keyring didn't work - if self._password is None: + if self.password is None: self._prompt_password() - def encrypt(self, text: str) -> bytes: + @property + def password(self): + return self._password + + @password.setter + def password(self, value): + self._password = value + + def encrypt(self, text): + if self.password is None: + self.password = create_password(self._config["journal"]) return self._encrypt(text) - def decrypt(self, text: bytes) -> str: - encoded_text = text.encode(self._encoding) - while (result := self._decrypt(encoded_text)) is None: + def decrypt(self, text): + while (result := self._decrypt(text)) is None: self._prompt_password() return result @@ -48,7 +60,7 @@ class BasePasswordEncryption(BaseEncryption): print_msg(Message(MsgText.WrongPasswordTryAgain, MsgStyle.WARNING)) self._attempts += 1 - self._password = print_msg( + self.password = print_msg( Message(MsgText.Password, MsgStyle.PROMPT), get_input=True, hide_input=True, diff --git a/jrnl/encryption/Jrnlv1Encryption.py b/jrnl/encryption/Jrnlv1Encryption.py index 41413788..2e7569db 100644 --- a/jrnl/encryption/Jrnlv1Encryption.py +++ b/jrnl/encryption/Jrnlv1Encryption.py @@ -1,7 +1,38 @@ # Copyright © 2012-2022 jrnl contributors # License: https://www.gnu.org/licenses/gpl-3.0.html -from jrnl.encryption.BaseEncryption import BaseEncryption +import hashlib + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import padding +from cryptography.hazmat.primitives.ciphers import Cipher +from cryptography.hazmat.primitives.ciphers import algorithms +from cryptography.hazmat.primitives.ciphers import modes + +from jrnl.encryption.BasePasswordEncryption import BasePasswordEncryption -class Jrnlv1Encryption(BaseEncryption): - pass +class Jrnlv1Encryption(BasePasswordEncryption): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + # def _encrypt(self, text: str) -> bytes: + # raise NotImplementedError + + def _decrypt(self, text: bytes) -> str | None: + iv, cipher = text[:16], text[16:] + decryption_key = hashlib.sha256(self._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._password = password + if plain_padded[-1] in (" ", 32): + # Ancient versions of jrnl. Do not judge me. + return plain_padded.decode("utf-8").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 diff --git a/jrnl/encryption/Jrnlv2Encryption.py b/jrnl/encryption/Jrnlv2Encryption.py index 13d2b362..c27e6abe 100644 --- a/jrnl/encryption/Jrnlv2Encryption.py +++ b/jrnl/encryption/Jrnlv2Encryption.py @@ -13,19 +13,25 @@ from .BasePasswordEncryption import BasePasswordEncryption class Jrnlv2Encryption(BasePasswordEncryption): _salt: bytes - _encoding: str _key: bytes def __init__(self, *args, **kwargs) -> None: + # Salt is hard-coded + self._salt: bytes = b"\xf2\xd5q\x0e\xc1\x8d.\xde\xdc\x8e6t\x89\x04\xce\xf8" + super().__init__(*args, **kwargs) - # Salt is hard-coded - self._salt = b"\xf2\xd5q\x0e\xc1\x8d.\xde\xdc\x8e6t\x89\x04\xce\xf8" - self._encoding = "utf-8" + @property + def password(self): + return self._password + + @password.setter + def password(self, value): + self._password = value self._make_key() def _make_key(self) -> None: - password = self._password.encode(self._encoding) + password = self.password.encode(self._encoding) kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), length=32, @@ -36,11 +42,19 @@ class Jrnlv2Encryption(BasePasswordEncryption): key = kdf.derive(password) self._key = base64.urlsafe_b64encode(key) - def _encrypt(self, text: str) -> bytes: - return Fernet(self._key).encrypt(text.encode(self._encoding)) + def _encrypt(self, text: str) -> str: + return ( + Fernet(self._key) + .encrypt(text.encode(self._encoding)) + .decode(self._encoding) + ) - def _decrypt(self, text: bytes) -> str | None: + def _decrypt(self, text: str) -> str | None: try: - return Fernet(self._key).decrypt((text)).decode(self._encoding) + return ( + Fernet(self._key) + .decrypt(text.encode(self._encoding)) + .decode(self._encoding) + ) except (InvalidToken, IndexError): return None diff --git a/jrnl/upgrade.py b/jrnl/upgrade.py index 97f742a4..bb81266d 100644 --- a/jrnl/upgrade.py +++ b/jrnl/upgrade.py @@ -4,7 +4,6 @@ import os from jrnl import Journal -from jrnl import PlainJournal from jrnl import __version__ from jrnl.config import is_config_json from jrnl.config import load_config @@ -132,7 +131,7 @@ def upgrade_jrnl(config_path): old_journal = Journal.open_journal( journal_name, scope_config(config, journal_name), legacy=True ) - all_journals.append(PlainJournal.from_journal(old_journal)) + all_journals.append(Journal.PlainJournal.from_journal(old_journal)) for journal_name, path in plain_journals.items(): print_msg(