more cleaning

This commit is contained in:
Jonathan Wren 2022-09-24 08:08:11 -07:00
parent bb1f263d6c
commit a76f066a7c
8 changed files with 101 additions and 95 deletions

View file

@ -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)

View file

@ -6,7 +6,6 @@ import logging
import os import os
import re import re
from jrnl import EncryptedJournal
from jrnl import Entry from jrnl import Entry
from jrnl import time from jrnl import time
from jrnl.encryption import determine_encryption_method from jrnl.encryption import determine_encryption_method
@ -48,7 +47,7 @@ class Journal:
self.search_tags = None # Store tags we're highlighting self.search_tags = None # Store tags we're highlighting
self.name = name self.name = name
self.entries = [] self.entries = []
self._encryption_method = None self.encryption_method = None
def __len__(self): def __len__(self):
"""Returns the number of entries""" """Returns the number of entries"""
@ -81,21 +80,20 @@ class Journal:
self.sort() self.sort()
def _get_encryption_method(self): def _get_encryption_method(self):
self._encryption_method = determine_encryption_method(self.config["encrypt"])( encryption_method = determine_encryption_method(self.config["encrypt"])
self.config self.encryption_method = encryption_method(self.config)
)
def _decrypt(self, text): def _decrypt(self, text):
if not self._encryption_method: if self.encryption_method is None:
self._get_encryption_method() self._get_encryption_method()
return self._encryption_method.decrypt(text) return self.encryption_method.decrypt(text)
def _encrypt(self, text): def _encrypt(self, text):
if not self._encryption_method: if self.encryption_method is None:
self._get_encryption_method() self._get_encryption_method()
return self._encryption_method.encrypt(text) return self.encryption_method.encrypt(text)
def open(self, filename=None): def open(self, filename=None):
"""Opens the journal file defined in the config and parses it into a list of Entries. """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() return PlainJournal(journal_name, **config).open()
if legacy: if legacy:
return EncryptedJournal.LegacyEncryptedJournal(journal_name, **config).open() config["encrypt"] = "jrnlv1"
return LegacyJournal(journal_name, **config).open()
return PlainJournal(journal_name, **config).open() return PlainJournal(journal_name, **config).open()

View file

@ -14,6 +14,7 @@ run.
Also, please note that all (non-builtin) imports should be scoped to each function to Also, please note that all (non-builtin) imports should be scoped to each function to
avoid any possible overhead for these standalone commands. avoid any possible overhead for these standalone commands.
""" """
import logging
import platform import platform
import sys import sys
@ -22,7 +23,6 @@ from jrnl.messages import Message
from jrnl.messages import MsgStyle from jrnl.messages import MsgStyle
from jrnl.messages import MsgText from jrnl.messages import MsgText
from jrnl.output import print_msg from jrnl.output import print_msg
from jrnl.prompt import create_password
def preconfig_diagnostic(_): def preconfig_diagnostic(_):
@ -79,7 +79,6 @@ def postconfig_encrypt(args, config, original_config, **kwargs):
""" """
from jrnl.config import update_config from jrnl.config import update_config
from jrnl.install import save_config from jrnl.install import save_config
from jrnl.Journal import PlainJournal
from jrnl.Journal import open_journal from jrnl.Journal import open_journal
# Open the 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 is encrypted, create new password
if journal.config["encrypt"] is True: 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.") 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 journal.config["encrypt"] = True
new_journal.write(args.filename) journal.write(args.filename)
print_msg( print_msg(
Message( Message(
MsgText.JournalEncryptedTo, MsgText.JournalEncryptedTo,
MsgStyle.NORMAL, 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.""" """Decrypts into new file. If filename is not set, we encrypt the journal file itself."""
from jrnl.config import update_config from jrnl.config import update_config
from jrnl.install import save_config from jrnl.install import save_config
from jrnl.Journal import PlainJournal
from jrnl.Journal import open_journal from jrnl.Journal import open_journal
journal = open_journal(args.journal_name, config) journal = open_journal(args.journal_name, config)
journal.config["encrypt"] = False
new_journal = PlainJournal.from_journal(journal) logging.debug("Clearing encryption method...")
new_journal.write(args.filename) journal.config["encrypt"] = False
journal.encryption_method = None
journal.write(args.filename)
print_msg( print_msg(
Message( Message(
MsgText.JournalDecryptedTo, MsgText.JournalDecryptedTo,
MsgStyle.NORMAL, MsgStyle.NORMAL,
{"path": args.filename or new_journal.config["journal"]}, {"path": args.filename or journal.config["journal"]},
) )
) )

View file

@ -9,15 +9,15 @@ class BaseEncryption(ABC):
self._config = config self._config = config
@abstractmethod @abstractmethod
def encrypt(self, text: str) -> bytes: def encrypt(self, text: str) -> str:
pass pass
@abstractmethod @abstractmethod
def decrypt(self, text: bytes) -> str | None: def decrypt(self, text: str) -> str | None:
pass pass
@abstractmethod @abstractmethod
def _encrypt(self, text: bytes) -> str: def _encrypt(self, text: str) -> str:
""" """
This is needed because self.decrypt might need This is needed because self.decrypt might need
to perform actions (e.g. prompt for password) to perform actions (e.g. prompt for password)
@ -26,7 +26,7 @@ class BaseEncryption(ABC):
pass pass
@abstractmethod @abstractmethod
def _decrypt(self, text: bytes) -> str: def _decrypt(self, text: str) -> str:
""" """
This is needed because self.decrypt might need This is needed because self.decrypt might need
to perform actions (e.g. prompt for password) to perform actions (e.g. prompt for password)

View file

@ -7,6 +7,7 @@ from jrnl.messages import Message
from jrnl.messages import MsgStyle from jrnl.messages import MsgStyle
from jrnl.messages import MsgText from jrnl.messages import MsgText
from jrnl.output import print_msg from jrnl.output import print_msg
from jrnl.prompt import create_password
class BasePasswordEncryption(BaseEncryption): class BasePasswordEncryption(BaseEncryption):
@ -14,26 +15,37 @@ class BasePasswordEncryption(BaseEncryption):
_journal_name: str _journal_name: str
_max_attempts: int _max_attempts: int
_password: str | None _password: str | None
_encoding: str
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._attempts = 0 self._attempts = 0
self._max_attempts = 3 self._max_attempts = 3
self._password = None self._password = None
self._encoding = "utf-8"
# Check keyring first to be ready for decryption # Check keyring first to be ready for decryption
get_keyring_password(self._config["journal"]) get_keyring_password(self._config["journal"])
# Prompt for password if keyring didn't work # Prompt for password if keyring didn't work
if self._password is None: if self.password is None:
self._prompt_password() 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) return self._encrypt(text)
def decrypt(self, text: bytes) -> str: def decrypt(self, text):
encoded_text = text.encode(self._encoding) while (result := self._decrypt(text)) is None:
while (result := self._decrypt(encoded_text)) is None:
self._prompt_password() self._prompt_password()
return result return result
@ -48,7 +60,7 @@ class BasePasswordEncryption(BaseEncryption):
print_msg(Message(MsgText.WrongPasswordTryAgain, MsgStyle.WARNING)) print_msg(Message(MsgText.WrongPasswordTryAgain, MsgStyle.WARNING))
self._attempts += 1 self._attempts += 1
self._password = print_msg( self.password = print_msg(
Message(MsgText.Password, MsgStyle.PROMPT), Message(MsgText.Password, MsgStyle.PROMPT),
get_input=True, get_input=True,
hide_input=True, hide_input=True,

View file

@ -1,7 +1,38 @@
# Copyright © 2012-2022 jrnl contributors # Copyright © 2012-2022 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html # 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): class Jrnlv1Encryption(BasePasswordEncryption):
pass 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

View file

@ -13,19 +13,25 @@ from .BasePasswordEncryption import BasePasswordEncryption
class Jrnlv2Encryption(BasePasswordEncryption): class Jrnlv2Encryption(BasePasswordEncryption):
_salt: bytes _salt: bytes
_encoding: str
_key: bytes _key: bytes
def __init__(self, *args, **kwargs) -> None: 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) super().__init__(*args, **kwargs)
# Salt is hard-coded @property
self._salt = b"\xf2\xd5q\x0e\xc1\x8d.\xde\xdc\x8e6t\x89\x04\xce\xf8" def password(self):
self._encoding = "utf-8" return self._password
@password.setter
def password(self, value):
self._password = value
self._make_key() self._make_key()
def _make_key(self) -> None: def _make_key(self) -> None:
password = self._password.encode(self._encoding) password = self.password.encode(self._encoding)
kdf = PBKDF2HMAC( kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(), algorithm=hashes.SHA256(),
length=32, length=32,
@ -36,11 +42,19 @@ class Jrnlv2Encryption(BasePasswordEncryption):
key = kdf.derive(password) key = kdf.derive(password)
self._key = base64.urlsafe_b64encode(key) self._key = base64.urlsafe_b64encode(key)
def _encrypt(self, text: str) -> bytes: def _encrypt(self, text: str) -> str:
return Fernet(self._key).encrypt(text.encode(self._encoding)) 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: 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): except (InvalidToken, IndexError):
return None return None

View file

@ -4,7 +4,6 @@
import os import os
from jrnl import Journal from jrnl import Journal
from jrnl import PlainJournal
from jrnl import __version__ from jrnl import __version__
from jrnl.config import is_config_json from jrnl.config import is_config_json
from jrnl.config import load_config from jrnl.config import load_config
@ -132,7 +131,7 @@ def upgrade_jrnl(config_path):
old_journal = Journal.open_journal( old_journal = Journal.open_journal(
journal_name, scope_config(config, journal_name), legacy=True 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(): for journal_name, path in plain_journals.items():
print_msg( print_msg(