mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-17 19:48:31 +02:00
Merge afbc1b3f83
into 800a373462
This commit is contained in:
commit
e820bf494e
8 changed files with 162 additions and 111 deletions
|
@ -42,7 +42,7 @@ def open_journal(journal_name="default"):
|
||||||
config.update(journal_conf)
|
config.update(journal_conf)
|
||||||
else: # But also just give them a string to point to the journal file
|
else: # But also just give them a string to point to the journal file
|
||||||
config['journal'] = journal_conf
|
config['journal'] = journal_conf
|
||||||
return Journal.Journal(**config)
|
return Journal.open_journal(journal_name, config)
|
||||||
|
|
||||||
@given('we use the config "{config_file}"')
|
@given('we use the config "{config_file}"')
|
||||||
def set_config(context, config_file):
|
def set_config(context, config_file):
|
||||||
|
|
|
@ -50,6 +50,7 @@ class DayOne(Journal.Journal):
|
||||||
entry.tags = [self.config['tagsymbols'][0] + tag for tag in dict_entry.get("Tags", [])]
|
entry.tags = [self.config['tagsymbols'][0] + tag for tag in dict_entry.get("Tags", [])]
|
||||||
self.entries.append(entry)
|
self.entries.append(entry)
|
||||||
self.sort()
|
self.sort()
|
||||||
|
return self
|
||||||
|
|
||||||
def write(self):
|
def write(self):
|
||||||
"""Writes only the entries that have been modified into plist files."""
|
"""Writes only the entries that have been modified into plist files."""
|
||||||
|
|
85
jrnl/EncryptedJournal.py
Normal file
85
jrnl/EncryptedJournal.py
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
class EncryptedJournal(Journal.Journal):
|
||||||
|
def __init__(self, name='default', **kwargs):
|
||||||
|
super(EncryptedJournal, self).__init__(name, **kwargs)
|
||||||
|
self.config['encrypt'] = True
|
||||||
|
|
||||||
|
def _load(self, filename):
|
||||||
|
with open(filename, "rb") as f:
|
||||||
|
journal_encrypted = f.read()
|
||||||
|
|
||||||
|
def validate_password(password):
|
||||||
|
key = make_key(password)
|
||||||
|
return _decrypt(journal_encrypted, key)
|
||||||
|
|
||||||
|
text = None
|
||||||
|
|
||||||
|
if 'password' in self.config:
|
||||||
|
text = validate_password(self.config['password'])
|
||||||
|
|
||||||
|
if text is None:
|
||||||
|
text = util.get_password(keychain=self.name, validator=validate_password)
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
|
def _store(self, filename, text):
|
||||||
|
key = make_key(self.config['password'])
|
||||||
|
journal = _encrypt(text, key)
|
||||||
|
|
||||||
|
with open(filename, 'wb') as f:
|
||||||
|
f.write(journal)
|
139
jrnl/Journal.py
139
jrnl/Journal.py
|
@ -4,20 +4,16 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from . import Entry
|
from . import Entry
|
||||||
from . import util
|
from . import util
|
||||||
import codecs
|
|
||||||
try: import parsedatetime.parsedatetime_consts as pdt
|
|
||||||
except ImportError: import parsedatetime as pdt
|
|
||||||
import re
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import dateutil
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import codecs
|
||||||
|
import re
|
||||||
|
import dateutil
|
||||||
try:
|
try:
|
||||||
from Crypto.Cipher import AES
|
import parsedatetime.parsedatetime_consts as pdt
|
||||||
from Crypto import Random
|
|
||||||
crypto_installed = True
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
crypto_installed = False
|
import parsedatetime as pdt
|
||||||
import hashlib
|
|
||||||
|
|
||||||
|
|
||||||
class Journal(object):
|
class Journal(object):
|
||||||
|
@ -37,82 +33,33 @@ class Journal(object):
|
||||||
consts = pdt.Constants(usePyICU=False)
|
consts = pdt.Constants(usePyICU=False)
|
||||||
consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday
|
consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday
|
||||||
self.dateparse = pdt.Calendar(consts)
|
self.dateparse = pdt.Calendar(consts)
|
||||||
self.key = None # used to decrypt and encrypt the 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.open()
|
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
"""Returns the number of entries"""
|
"""Returns the number of entries"""
|
||||||
return len(self.entries)
|
return len(self.entries)
|
||||||
|
|
||||||
def _decrypt(self, cipher):
|
|
||||||
"""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(self.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(self, plain):
|
|
||||||
"""Encrypt a plaintext string using self.key as the 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(self.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)
|
|
||||||
|
|
||||||
def make_key(self, password):
|
|
||||||
"""Creates an encryption key from the default password or prompts for a new password."""
|
|
||||||
self.key = hashlib.sha256(password.encode("utf-8")).digest()
|
|
||||||
|
|
||||||
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.
|
||||||
Entries have the form (date, title, body)."""
|
Entries have the form (date, title, body)."""
|
||||||
filename = filename or self.config['journal']
|
filename = filename or self.config['journal']
|
||||||
|
text = self._load(filename)
|
||||||
if self.config['encrypt']:
|
self.entries = self._parse(text)
|
||||||
with open(filename, "rb") as f:
|
|
||||||
journal_encrypted = f.read()
|
|
||||||
|
|
||||||
def validate_password(password):
|
|
||||||
self.make_key(password)
|
|
||||||
return self._decrypt(journal_encrypted)
|
|
||||||
|
|
||||||
# Soft-deprecated:
|
|
||||||
journal = None
|
|
||||||
if 'password' in self.config:
|
|
||||||
journal = validate_password(self.config['password'])
|
|
||||||
if journal is None:
|
|
||||||
journal = util.get_password(keychain=self.name, validator=validate_password)
|
|
||||||
else:
|
|
||||||
with codecs.open(filename, "r", "utf-8") as f:
|
|
||||||
journal = f.read()
|
|
||||||
self.entries = self._parse(journal)
|
|
||||||
self.sort()
|
self.sort()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def write(self, filename=None):
|
||||||
|
"""Dumps the journal into the config file, overwriting it"""
|
||||||
|
filename = filename or self.config['journal']
|
||||||
|
text = u"\n".join([e.__unicode__() for e in self.entries])
|
||||||
|
self._store(filename, text)
|
||||||
|
|
||||||
|
def _load(self, filename):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _store(self, filename, text):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def _parse(self, journal_txt):
|
def _parse(self, journal_txt):
|
||||||
"""Parses a journal that's stored in a string and returns a list of entries"""
|
"""Parses a journal that's stored in a string and returns a list of entries"""
|
||||||
|
@ -178,18 +125,6 @@ class Journal(object):
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Journal with {0} entries>".format(len(self.entries))
|
return "<Journal with {0} entries>".format(len(self.entries))
|
||||||
|
|
||||||
def write(self, filename=None):
|
|
||||||
"""Dumps the journal into the config file, overwriting it"""
|
|
||||||
filename = filename or self.config['journal']
|
|
||||||
journal = u"\n".join([e.__unicode__() for e in self.entries])
|
|
||||||
if self.config['encrypt']:
|
|
||||||
journal = self._encrypt(journal)
|
|
||||||
with open(filename, 'wb') as journal_file:
|
|
||||||
journal_file.write(journal)
|
|
||||||
else:
|
|
||||||
with codecs.open(filename, 'w', "utf-8") as journal_file:
|
|
||||||
journal_file.write(journal)
|
|
||||||
|
|
||||||
def sort(self):
|
def sort(self):
|
||||||
"""Sorts the Journal's entries by date"""
|
"""Sorts the Journal's entries by date"""
|
||||||
self.entries = sorted(self.entries, key=lambda entry: entry.date)
|
self.entries = sorted(self.entries, key=lambda entry: entry.date)
|
||||||
|
@ -321,3 +256,35 @@ class Journal(object):
|
||||||
for entry in mod_entries:
|
for entry in mod_entries:
|
||||||
entry.modified = not any(entry == old_entry for old_entry in self.entries)
|
entry.modified = not any(entry == old_entry for old_entry in self.entries)
|
||||||
self.entries = mod_entries
|
self.entries = mod_entries
|
||||||
|
|
||||||
|
|
||||||
|
class PlainJournal(Journal):
|
||||||
|
def __init__(self, name='default', **kwargs):
|
||||||
|
super(PlainJournal, self).__init__(name, **kwargs)
|
||||||
|
|
||||||
|
def _load(self, filename):
|
||||||
|
with codecs.open(filename, "r", "utf-8") as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
def _store(self, filename, text):
|
||||||
|
with codecs.open(filename, 'w', "utf-8") as f:
|
||||||
|
f.write(text)
|
||||||
|
|
||||||
|
|
||||||
|
def open_journal(name, config):
|
||||||
|
"""
|
||||||
|
Creates a normal, encrypted or DayOne journal based on the passed config.
|
||||||
|
"""
|
||||||
|
if os.path.isdir(config['journal']):
|
||||||
|
if config['journal'].strip("/").endswith(".dayone") or "entries" in os.listdir(config['journal']):
|
||||||
|
from . import DayOneJournal
|
||||||
|
return DayOneJournal.DayOne(**config).open()
|
||||||
|
else:
|
||||||
|
util.prompt(u"[Error: {0} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal']))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not config['encrypt']:
|
||||||
|
return PlainJournal(name, **config).open()
|
||||||
|
else:
|
||||||
|
from . import EncryptedJournal
|
||||||
|
return EncryptedJournal.EncryptedJournal(name, **config).open()
|
||||||
|
|
|
@ -12,7 +12,3 @@ __version__ = '1.8.4'
|
||||||
__author__ = 'Manuel Ebert'
|
__author__ = 'Manuel Ebert'
|
||||||
__license__ = 'MIT License'
|
__license__ = 'MIT License'
|
||||||
__copyright__ = 'Copyright 2013 - 2014 Manuel Ebert'
|
__copyright__ = 'Copyright 2013 - 2014 Manuel Ebert'
|
||||||
|
|
||||||
from . import Journal
|
|
||||||
from . import cli
|
|
||||||
from .cli import run
|
|
||||||
|
|
37
jrnl/cli.py
37
jrnl/cli.py
|
@ -9,7 +9,6 @@
|
||||||
|
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from . import Journal
|
from . import Journal
|
||||||
from . import DayOneJournal
|
|
||||||
from . import util
|
from . import util
|
||||||
from . import exporters
|
from . import exporters
|
||||||
from . import install
|
from . import install
|
||||||
|
@ -67,21 +66,30 @@ def guess_mode(args, config):
|
||||||
|
|
||||||
def encrypt(journal, filename=None):
|
def encrypt(journal, filename=None):
|
||||||
""" Encrypt into new file. If filename is not set, we encrypt the journal file itself. """
|
""" Encrypt into new file. If filename is not set, we encrypt the journal file itself. """
|
||||||
password = util.getpass("Enter new password: ")
|
from . import EncryptedJournal
|
||||||
journal.make_key(password)
|
|
||||||
|
journal.config['password'] = util.getpass("Enter new password: ")
|
||||||
journal.config['encrypt'] = True
|
journal.config['encrypt'] = True
|
||||||
journal.write(filename)
|
|
||||||
|
new_journal = EncryptedJournal.EncryptedJournal(None, **journal.config)
|
||||||
|
new_journal.entries = journal.entries
|
||||||
|
new_journal.write(filename)
|
||||||
|
|
||||||
if util.yesno("Do you want to store the password in your keychain?", default=True):
|
if util.yesno("Do you want to store the password in your keychain?", default=True):
|
||||||
util.set_keychain(journal.name, password)
|
util.set_keychain(journal.name, journal.config['password'])
|
||||||
util.prompt("Journal encrypted to {0}.".format(filename or journal.config['journal']))
|
|
||||||
|
util.prompt("Journal encrypted to {0}.".format(filename or new_journal.config['journal']))
|
||||||
|
|
||||||
|
|
||||||
def decrypt(journal, filename=None):
|
def decrypt(journal, filename=None):
|
||||||
""" 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. """
|
||||||
journal.config['encrypt'] = False
|
journal.config['encrypt'] = False
|
||||||
journal.config['password'] = ""
|
journal.config['password'] = ""
|
||||||
journal.write(filename)
|
|
||||||
util.prompt("Journal decrypted to {0}.".format(filename or journal.config['journal']))
|
new_journal = Journal.PlainJournal(filename, **journal.config)
|
||||||
|
new_journal.entries = journal.entries
|
||||||
|
new_journal.write(filename)
|
||||||
|
util.prompt("Journal decrypted to {0}.".format(filename or new_journal.config['journal']))
|
||||||
|
|
||||||
|
|
||||||
def touch_journal(filename):
|
def touch_journal(filename):
|
||||||
|
@ -111,6 +119,8 @@ def update_config(config, new_config, scope, force_local=False):
|
||||||
config.update(new_config)
|
config.update(new_config)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def run(manual_args=None):
|
def run(manual_args=None):
|
||||||
args = parse_args(manual_args)
|
args = parse_args(manual_args)
|
||||||
args.text = [p.decode('utf-8') if util.PY2 and not isinstance(p, unicode) else p for p in args.text]
|
args.text = [p.decode('utf-8') if util.PY2 and not isinstance(p, unicode) else p for p in args.text]
|
||||||
|
@ -172,16 +182,7 @@ def run(manual_args=None):
|
||||||
else:
|
else:
|
||||||
mode_compose = False
|
mode_compose = False
|
||||||
|
|
||||||
# open journal file or folder
|
journal = Journal.open_journal(journal_name, config)
|
||||||
if os.path.isdir(config['journal']):
|
|
||||||
if config['journal'].strip("/").endswith(".dayone") or \
|
|
||||||
"entries" in os.listdir(config['journal']):
|
|
||||||
journal = DayOneJournal.DayOne(**config)
|
|
||||||
else:
|
|
||||||
util.prompt(u"[Error: {0} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal']))
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
|
||||||
journal = Journal.Journal(journal_name, **config)
|
|
||||||
|
|
||||||
# Writing mode
|
# Writing mode
|
||||||
if mode_compose:
|
if mode_compose:
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import getpass as gp
|
import getpass as gp
|
||||||
import keyring
|
|
||||||
import json
|
import json
|
||||||
if "win32" in sys.platform:
|
if "win32" in sys.platform:
|
||||||
import colorama
|
import colorama
|
||||||
|
@ -51,10 +50,12 @@ def get_password(validator, keychain=None, max_attempts=3):
|
||||||
|
|
||||||
|
|
||||||
def get_keychain(journal_name):
|
def get_keychain(journal_name):
|
||||||
|
import keyring
|
||||||
return keyring.get_password('jrnl', journal_name)
|
return keyring.get_password('jrnl', journal_name)
|
||||||
|
|
||||||
|
|
||||||
def set_keychain(journal_name, password):
|
def set_keychain(journal_name, password):
|
||||||
|
import keyring
|
||||||
if password is None:
|
if password is None:
|
||||||
try:
|
try:
|
||||||
keyring.delete_password('jrnl', journal_name)
|
keyring.delete_password('jrnl', journal_name)
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -85,7 +85,7 @@ setup(
|
||||||
long_description=__doc__,
|
long_description=__doc__,
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
'console_scripts': [
|
||||||
'jrnl = jrnl:run',
|
'jrnl = jrnl.cli:run',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
classifiers=[
|
classifiers=[
|
||||||
|
|
Loading…
Add table
Reference in a new issue