from . import Journal, util from cryptography.fernet import Fernet, InvalidToken from cryptography.hazmat.primitives import hashes, padding from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes import hashlib from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.backends import default_backend import sys import os import base64 import getpass import logging log = logging.getLogger() def make_key(password): password = util.bytes(password) kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), length=32, # Salt is hard-coded salt=b'\xf2\xd5q\x0e\xc1\x8d.\xde\xdc\x8e6t\x89\x04\xce\xf8', iterations=100000, backend=default_backend() ) key = kdf.derive(password) return base64.urlsafe_b64encode(key) class EncryptedJournal(Journal.Journal): def __init__(self, name='default', **kwargs): super(EncryptedJournal, self).__init__(name, **kwargs) self.config['encrypt'] = True def open(self, filename=None): """Opens the journal file defined in the config and parses it into a list of Entries. Entries have the form (date, title, body).""" filename = filename or self.config['journal'] if not os.path.exists(filename): password = util.getpass("Enter password for new journal: ") if password: if util.yesno("Do you want to store the password in your keychain?", default=True): util.set_keychain(self.name, password) else: util.set_keychain(self.name, None) self.config['password'] = password text = "" self._store(filename, text) util.prompt("[Journal '{0}' created at {1}]".format(self.name, filename)) else: util.prompt("No password supplied for encrypted journal") sys.exit(1) else: text = self._load(filename) self.entries = self._parse(text) self.sort() log.debug("opened %s with %d entries", self.__class__.__name__, len(self)) return self def _load(self, filename, password=None): """Loads an encrypted journal from a file and tries to decrypt it. If password is not provided, will look for password in the keychain and otherwise ask the user to enter a password up to three times. If the password is provided but wrong (or corrupt), this will simply return None.""" with open(filename, 'rb') as f: journal_encrypted = f.read() def validate_password(password): key = make_key(password) try: plain = Fernet(key).decrypt(journal_encrypted).decode('utf-8') self.config['password'] = password return plain except (InvalidToken, IndexError): return None if password: return validate_password(password) return util.get_password(keychain=self.name, validator=validate_password) def _store(self, filename, text): key = make_key(self.config['password']) journal = Fernet(key).encrypt(text.encode('utf-8')) with open(filename, 'wb') as f: f.write(journal) @classmethod def _create(cls, filename, password): key = make_key(password) dummy = Fernet(key).encrypt(b"") with open(filename, 'wb') as f: f.write(dummy) class LegacyEncryptedJournal(Journal.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(LegacyEncryptedJournal, self).__init__(name, **kwargs) self.config['encrypt'] = True def _load(self, filename, password=None): with open(filename, 'rb') as f: journal_encrypted = f.read() iv, cipher = journal_encrypted[:16], journal_encrypted[16:] def validate_password(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.config['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 password: return validate_password(password) return util.get_password(keychain=self.name, validator=validate_password)