Work on cryptography

This commit is contained in:
Manuel Ebert 2014-09-27 13:15:46 -07:00
parent e75e3d73a0
commit a1b5a4099e
11 changed files with 69 additions and 17 deletions

View file

@ -0,0 +1,13 @@
{
"default_hour": 9,
"default_minute": 0,
"editor": "",
"encrypt": true,
"highlight": true,
"journals": {
"default": "features/journals/encrypted_jrnl-1-9-5.journal"
},
"linewrap": 80,
"tagsymbols": "@",
"timeformat": "%Y-%m-%d %H:%M"
}

View file

@ -4,8 +4,7 @@ editor: ''
encrypt: true encrypt: true
highlight: true highlight: true
journals: journals:
default: features/journals/encrypted.journal default: features/journals/encrypted_jrnl1-9-5.journal
linewrap: 80 linewrap: 80
password: bad doggie no biscuit
tagsymbols: '@' tagsymbols: '@'
timeformat: '%Y-%m-%d %H:%M' timeformat: '%Y-%m-%d %H:%M'

View file

@ -1,11 +1,16 @@
Feature: Encrypted journals Feature: Encrypted journals
Scenario: Loading an encrypted journal Scenario: Loading an encrypted journal
Given we use the config "encrypted.yaml" Given we use the config "encrypted.yaml"
When we run "jrnl -n 1" and enter "bad doggie no biscuit" When we run "jrnl -n 1" and enter "bad doggie no biscuit"
Then we should see the message "Password" Then we should see the message "Password"
and the output should contain "2013-06-10 15:40 Life is good" and the output should contain "2013-06-10 15:40 Life is good"
Scenario: Upgrading a journal encrypted with jrnl 1.x
Given we use the config "encrypted_old.json"
When we run "jrnl -n 1" and enter "Y"
Then we should see the message "Password"
and the output should contain "2013-06-10 15:40 Life is good"
Scenario: Decrypting a journal Scenario: Decrypting a journal
Given we use the config "encrypted.yaml" Given we use the config "encrypted.yaml"
When we run "jrnl --decrypt" and enter "bad doggie no biscuit" When we run "jrnl --decrypt" and enter "bad doggie no biscuit"

View file

@ -50,7 +50,8 @@ def open_journal(journal_name="default"):
def set_config(context, config_file): def set_config(context, config_file):
full_path = os.path.join("features/configs", config_file) full_path = os.path.join("features/configs", config_file)
install.CONFIG_FILE_PATH = os.path.abspath(full_path) install.CONFIG_FILE_PATH = os.path.abspath(full_path)
# Add jrnl version to file if config_file.endswith("yaml"):
# Add jrnl version to file for 2.x journals
with open(install.CONFIG_FILE_PATH, 'a') as cf: with open(install.CONFIG_FILE_PATH, 'a') as cf:
cf.write("version: {}".format(__version__)) cf.write("version: {}".format(__version__))

View file

@ -1,12 +1,22 @@
from . import Journal, util from . import Journal, util
from cryptography.fernet import Fernet, InvalidToken from cryptography.fernet import Fernet, InvalidToken
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.backends import default_backend
import base64 import base64
from passlib.hash import pbkdf2_sha256 import os
def make_key(password): def make_key(password):
derived_key = pbkdf2_sha256.encrypt(password.encode("utf-8"), rounds=10000, salt_size=32) kdf = PBKDF2HMAC(
return base64.urlsafe_b64encode(derived_key) algorithm=hashes.SHA256(),
length=32,
salt=os.urandom(16),
iterations=100000,
backend=default_backend()
)
key = kdf.derive(password)
return base64.urlsafe_b64encode(key)
class EncryptedJournal(Journal.Journal): class EncryptedJournal(Journal.Journal):
@ -21,14 +31,15 @@ class EncryptedJournal(Journal.Journal):
def validate_password(password): def validate_password(password):
key = make_key(password) key = make_key(password)
try: try:
return Fernet(key).decrypt(journal_encrypted) return Fernet(key).decrypt(journal_encrypted).decode('utf-8')
except InvalidToken: except (InvalidToken, IndexError):
print base64.urlsafe_b64decode(journal_encrypted)
return None return None
return util.get_password(keychain=self.name, validator=validate_password) return util.get_password(keychain=self.name, validator=validate_password)
def _store(self, filename, text): def _store(self, filename, text):
key = make_key(self.config['password']) key = make_key(self.config['password'])
journal = Fernet(key).encrypt(text) journal = Fernet(key).encrypt(text.encode('utf-8'))
with open(filename, 'w') as f: with open(filename, 'w') as f:
f.write(journal) f.write(journal)

View file

@ -45,7 +45,7 @@ class Journal(object):
def write(self, filename=None): def write(self, filename=None):
"""Dumps the journal into the config file, overwriting it""" """Dumps the journal into the config file, overwriting it"""
filename = filename or self.config['journal'] filename = filename or self.config['journal']
text = u"\n".join([e.__unicode__() for e in self.entries]) text = "\n".join([e.__unicode__() for e in self.entries])
self._store(filename, text) self._store(filename, text)
def _load(self, filename): def _load(self, filename):

View file

@ -131,7 +131,10 @@ def run(manual_args=None):
config = install.load_or_install_jrnl() config = install.load_or_install_jrnl()
if args.ls: if args.ls:
print(util.py2encode(list_journals(config))) util.prnt(u"Journals defined in {}".format(install.CONFIG_FILE_PATH))
ml = min(max(len(k) for k in config['journals']), 20)
for journal, cfg in config['journals'].items():
print " * {:{}} -> {}".format(journal, ml, cfg['journal'] if isinstance(cfg, dict) else cfg)
sys.exit(0) sys.exit(0)
original_config = config.copy() original_config = config.copy()

View file

@ -77,8 +77,8 @@ def load_or_install_jrnl():
config_path = CONFIG_FILE_PATH if os.path.exists(CONFIG_FILE_PATH) else CONFIG_FILE_PATH_FALLBACK config_path = CONFIG_FILE_PATH if os.path.exists(CONFIG_FILE_PATH) else CONFIG_FILE_PATH_FALLBACK
if os.path.exists(config_path): if os.path.exists(config_path):
config = util.load_config(CONFIG_FILE_PATH) config = util.load_config(CONFIG_FILE_PATH)
upgrade_config(config)
upgrade.upgrade_jrnl_if_necessary(CONFIG_FILE_PATH) upgrade.upgrade_jrnl_if_necessary(CONFIG_FILE_PATH)
upgrade_config(config)
return config return config
else: else:
install() install()

View file

@ -4,12 +4,15 @@ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
import hashlib import hashlib
import util import util
from . import __version__ from . import __version__
from . import EncryptedJournal
import sys import sys
from cryptography.fernet import Fernet
def upgrade_encrypted_journal(filename, key_plain): def upgrade_encrypted_journal(filename, key_plain):
"""Decrypts a journal in memory using the jrnl 1.x encryption scheme """Decrypts a journal in memory using the jrnl 1.x encryption scheme
and returns it in plain text.""" and returns it in plain text."""
util.prompt( "UPGRADING JOURNAL")
with open(filename) as f: with open(filename) as f:
iv_cipher = f.read() iv_cipher = f.read()
iv, cipher = iv_cipher[:16], iv_cipher[16:] iv, cipher = iv_cipher[:16], iv_cipher[16:]
@ -23,6 +26,11 @@ def upgrade_encrypted_journal(filename, key_plain):
plain += unpadder.finalize() plain += unpadder.finalize()
except ValueError: except ValueError:
return None return None
key = EncryptedJournal.make_key(key_plain)
journal = Fernet(key).encrypt(plain.encode('utf-8'))
with open(filename, 'w') as f:
f.write(journal)
return plain return plain
@ -44,12 +52,15 @@ def upgrade_jrnl_if_necessary(config_path):
plain_journals = {} plain_journals = {}
for journal, journal_conf in config['journals'].items(): for journal, journal_conf in config['journals'].items():
if isinstance(journal_conf, dict): if isinstance(journal_conf, dict):
if journal_conf.get("encrypted"): if journal_conf.get("encrypt"):
encrypted_journals[journal] = journal_conf.get("journal") encrypted_journals[journal] = journal_conf.get("journal")
else: else:
plain_journals[journal] = journal_conf.get("journal") plain_journals[journal] = journal_conf.get("journal")
else: else:
plain_journals[journal] = journal_conf.get("journal") if config.get('encrypt'):
encrypted_journals[journal] = journal_conf
else:
plain_journals[journal] = journal_conf
if encrypted_journals: if encrypted_journals:
util.prompt("Following encrypted journals will be upgraded to jrnl {}:".format(__version__)) util.prompt("Following encrypted journals will be upgraded to jrnl {}:".format(__version__))
for journal, path in encrypted_journals.items(): for journal, path in encrypted_journals.items():
@ -63,3 +74,7 @@ def upgrade_jrnl_if_necessary(config_path):
if not cont: if not cont:
util.prompt("jrnl NOT upgraded, exiting.") util.prompt("jrnl NOT upgraded, exiting.")
sys.exit(1) sys.exit(1)
for journal, path in encrypted_journals.items():
util.prompt("Enter password for {} journal (stored in {}).".format(journal, path))
util.get_password(keychain=journal, validator=lambda pwd: upgrade_encrypted_journal(path, pwd))

View file

@ -79,6 +79,11 @@ def py2encode(s):
return s.encode("utf-8") if PY2 and type(s) is unicode else s return s.encode("utf-8") if PY2 and type(s) is unicode else s
def prnt(s):
"""Encode and print a string"""
STDOUT.write(u(s + "\n"))
def prompt(msg): def prompt(msg):
"""Prints a message to the std err stream defined in util.""" """Prints a message to the std err stream defined in util."""
if not msg.endswith("\n"): if not msg.endswith("\n"):