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

View file

@ -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"]},
)
)

View file

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

View file

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

View file

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

View file

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

View file

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