mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-10 16:48:31 +02:00
Merge pull request #99 from maebert/96-keychain
Saves passwords to keyring
This commit is contained in:
commit
11a6d84819
12 changed files with 134 additions and 75 deletions
|
@ -1,6 +1,10 @@
|
||||||
Changelog
|
Changelog
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
#### 1.6.0
|
||||||
|
|
||||||
|
* [Improved] Passwords are now saved in the key-chain. The `password` field in `.jrnl_config` is soft-deprecated.
|
||||||
|
|
||||||
#### 1.5.7
|
#### 1.5.7
|
||||||
|
|
||||||
* [Improved] The `~` in journal config paths will now expand properly to e.g. `/Users/maebert`
|
* [Improved] The `~` in journal config paths will now expand properly to e.g. `/Users/maebert`
|
||||||
|
|
|
@ -185,7 +185,6 @@ The configuration file is a simple JSON file with the following options.
|
||||||
- `journals`: paths to your journal files
|
- `journals`: paths to your journal files
|
||||||
- `editor`: if set, executes this command to launch an external editor for writing your entries, e.g. `vim` or `subl -w` (note the `-w` flag to make sure _jrnl_ waits for Sublime Text to close the file before writing into the journal).
|
- `editor`: if set, executes this command to launch an external editor for writing your entries, e.g. `vim` or `subl -w` (note the `-w` flag to make sure _jrnl_ waits for Sublime Text to close the file before writing into the journal).
|
||||||
- `encrypt`: if `true`, encrypts your journal using AES.
|
- `encrypt`: if `true`, encrypts your journal using AES.
|
||||||
- `password`: you may store the password you used to encrypt your journal in plaintext here. This is useful if your journal file lives in an unsecure space (ie. your Dropbox), but the config file itself is more or less safe.
|
|
||||||
- `tagsymbols`: Symbols to be interpreted as tags. (__See note below__)
|
- `tagsymbols`: Symbols to be interpreted as tags. (__See note below__)
|
||||||
- `default_hour` and `default_minute`: if you supply a date, such as `last thursday`, but no specific time, the entry will be created at this time
|
- `default_hour` and `default_minute`: if you supply a date, such as `last thursday`, but no specific time, the entry will be created at this time
|
||||||
- `timeformat`: how to format the timestamps in your journal, see the [python docs](http://docs.python.org/library/time.html#time.strftime) for reference
|
- `timeformat`: how to format the timestamps in your journal, see the [python docs](http://docs.python.org/library/time.html#time.strftime) for reference
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
"password": "",
|
"password": "",
|
||||||
"journals": {
|
"journals": {
|
||||||
"default": "features/journals/simple.journal",
|
"default": "features/journals/simple.journal",
|
||||||
|
"simple": "features/journals/simple.journal",
|
||||||
"work": "features/journals/work.journal",
|
"work": "features/journals/work.journal",
|
||||||
"ideas": "features/journals/nothing.journal"
|
"ideas": "features/journals/nothing.journal"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
Feature: Multiple journals
|
Feature: Encrypted journals
|
||||||
|
|
||||||
Scenario: Loading an encrypted journal
|
Scenario: Loading an encrypted journal
|
||||||
Given we use the config "encrypted.json"
|
Given we use the config "encrypted.json"
|
||||||
|
@ -6,24 +6,32 @@
|
||||||
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: Decrypting a journal
|
||||||
|
Given we use the config "encrypted.json"
|
||||||
|
When we run "jrnl --decrypt" and enter "bad doggie no biscuit"
|
||||||
|
Then the config for journal "default" should have "encrypt" set to "bool:False"
|
||||||
|
Then we should see the message "Journal decrypted"
|
||||||
|
and the journal should have 2 entries
|
||||||
|
|
||||||
|
Scenario: Encrypting a journal
|
||||||
|
Given we use the config "basic.json"
|
||||||
|
When we run "jrnl --encrypt" and enter "swordfish"
|
||||||
|
Then we should see the message "Journal encrypted"
|
||||||
|
and the config for journal "default" should have "encrypt" set to "bool:True"
|
||||||
|
When we run "jrnl -n 1" and enter "swordfish"
|
||||||
|
Then we should see the message "Password"
|
||||||
|
and the output should contain "2013-06-10 15:40 Life is good"
|
||||||
|
|
||||||
Scenario: Loading an encrypted journal with password in config
|
Scenario: Loading an encrypted journal with password in config
|
||||||
Given we use the config "encrypted_with_pw.json"
|
Given we use the config "encrypted_with_pw.json"
|
||||||
When we run "jrnl -n 1"
|
When we run "jrnl -n 1"
|
||||||
Then the output should contain "2013-06-10 15:40 Life is good"
|
Then the output should contain "2013-06-10 15:40 Life is good"
|
||||||
|
|
||||||
Scenario: Decrypting a journal
|
Scenario: Storing a password in Keychain
|
||||||
Given we use the config "encrypted.json"
|
Given we use the config "multiple.json"
|
||||||
When we run "jrnl --decrypt" and enter "bad doggie no biscuit"
|
When we run "jrnl simple --encrypt" and enter "sabertooth"
|
||||||
Then we should see the message "Journal decrypted"
|
When we set the keychain password of "simple" to "sabertooth"
|
||||||
and the journal should have 2 entries
|
Then the config for journal "simple" should have "encrypt" set to "bool:True"
|
||||||
and the config should have "encrypt" set to "bool:False"
|
When we run "jrnl simple -n 1"
|
||||||
|
Then we should not see the message "Password"
|
||||||
Scenario: Encrypting a journal
|
|
||||||
Given we use the config "basic.json"
|
|
||||||
When we run "jrnl --encrypt" and enter "swordfish"
|
|
||||||
Then we should see the message "Journal encrypted"
|
|
||||||
and the config should have "encrypt" set to "bool:True"
|
|
||||||
When we run "jrnl -n 1" and enter "swordfish"
|
|
||||||
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"
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,8 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
import pytz
|
import pytz
|
||||||
|
import keyring
|
||||||
|
keyring.set_keyring(keyring.backends.file.PlaintextKeyring())
|
||||||
try:
|
try:
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -34,11 +36,11 @@ def read_journal(journal_name="default"):
|
||||||
def open_journal(journal_name="default"):
|
def open_journal(journal_name="default"):
|
||||||
with open(jrnl.CONFIG_PATH) as config_file:
|
with open(jrnl.CONFIG_PATH) as config_file:
|
||||||
config = json.load(config_file)
|
config = json.load(config_file)
|
||||||
journals = config['journals']
|
journal_conf = config['journals'][journal_name]
|
||||||
if type(journals) is dict: # We can override the default config on a by-journal basis
|
if type(journal_conf) is dict: # We can override the default config on a by-journal basis
|
||||||
config['journal'] = journals.get(journal_name)
|
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
|
config['journal'] = journal_conf
|
||||||
return Journal.Journal(**config)
|
return Journal.Journal(**config)
|
||||||
|
|
||||||
@given('we use the config "{config_file}"')
|
@given('we use the config "{config_file}"')
|
||||||
|
@ -68,6 +70,10 @@ def run(context, command):
|
||||||
except SystemExit as e:
|
except SystemExit as e:
|
||||||
context.exit_status = e.code
|
context.exit_status = e.code
|
||||||
|
|
||||||
|
@when('we set the keychain password of "{journal}" to "{password}"')
|
||||||
|
def set_keychain(context, journal, password):
|
||||||
|
keyring.set_password('jrnl', journal, password)
|
||||||
|
|
||||||
@then('we should get an error')
|
@then('we should get an error')
|
||||||
def has_error(context):
|
def has_error(context):
|
||||||
assert context.exit_status != 0, context.exit_status
|
assert context.exit_status != 0, context.exit_status
|
||||||
|
@ -134,6 +140,11 @@ def check_message(context, text):
|
||||||
out = context.messages.getvalue()
|
out = context.messages.getvalue()
|
||||||
assert text in out, [text, out]
|
assert text in out, [text, out]
|
||||||
|
|
||||||
|
@then('we should not see the message "{text}"')
|
||||||
|
def check_not_message(context, text):
|
||||||
|
out = context.messages.getvalue()
|
||||||
|
assert text not in out, [text, out]
|
||||||
|
|
||||||
@then('the journal should contain "{text}"')
|
@then('the journal should contain "{text}"')
|
||||||
@then('journal "{journal_name}" should contain "{text}"')
|
@then('journal "{journal_name}" should contain "{text}"')
|
||||||
def check_journal_content(context, text, journal_name="default"):
|
def check_journal_content(context, text, journal_name="default"):
|
||||||
|
@ -148,7 +159,8 @@ def journal_doesnt_exist(context, journal_name="default"):
|
||||||
assert not os.path.exists(journal_path)
|
assert not os.path.exists(journal_path)
|
||||||
|
|
||||||
@then('the config should have "{key}" set to "{value}"')
|
@then('the config should have "{key}" set to "{value}"')
|
||||||
def config_var(context, key, value):
|
@then('the config for journal "{journal}" should have "{key}" set to "{value}"')
|
||||||
|
def config_var(context, key, value, journal=None):
|
||||||
t, value = value.split(":")
|
t, value = value.split(":")
|
||||||
value = {
|
value = {
|
||||||
"bool": lambda v: v.lower() == "true",
|
"bool": lambda v: v.lower() == "true",
|
||||||
|
@ -157,6 +169,8 @@ def config_var(context, key, value):
|
||||||
}[t](value)
|
}[t](value)
|
||||||
with open(jrnl.CONFIG_PATH) as config_file:
|
with open(jrnl.CONFIG_PATH) as config_file:
|
||||||
config = json.load(config_file)
|
config = json.load(config_file)
|
||||||
|
if journal:
|
||||||
|
config = config["journals"][journal]
|
||||||
assert key in config
|
assert key in config
|
||||||
assert config[key] == value
|
assert config[key] == value
|
||||||
|
|
||||||
|
@ -171,4 +185,3 @@ def check_journal_content(context, number, journal_name="default"):
|
||||||
@then('fail')
|
@then('fail')
|
||||||
def debug_fail(context):
|
def debug_fail(context):
|
||||||
assert False
|
assert False
|
||||||
|
|
||||||
|
|
|
@ -12,18 +12,13 @@ except ImportError: import parsedatetime.parsedatetime as pdt
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import time
|
import time
|
||||||
try: import simplejson as json
|
|
||||||
except ImportError: import json
|
|
||||||
import sys
|
import sys
|
||||||
import glob
|
|
||||||
try:
|
try:
|
||||||
from Crypto.Cipher import AES
|
from Crypto.Cipher import AES
|
||||||
from Crypto import Random
|
from Crypto import Random
|
||||||
crypto_installed = True
|
crypto_installed = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
crypto_installed = False
|
crypto_installed = False
|
||||||
if "win32" in sys.platform: import pyreadline as readline
|
|
||||||
else: import readline
|
|
||||||
import hashlib
|
import hashlib
|
||||||
try:
|
try:
|
||||||
import colorama
|
import colorama
|
||||||
|
@ -33,14 +28,13 @@ except ImportError:
|
||||||
import plistlib
|
import plistlib
|
||||||
import pytz
|
import pytz
|
||||||
import uuid
|
import uuid
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
class Journal(object):
|
class Journal(object):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, name='default', **kwargs):
|
||||||
self.config = {
|
self.config = {
|
||||||
'journal': "journal.txt",
|
'journal': "journal.txt",
|
||||||
'encrypt': False,
|
'encrypt': False,
|
||||||
'password': "",
|
|
||||||
'default_hour': 9,
|
'default_hour': 9,
|
||||||
'default_minute': 0,
|
'default_minute': 0,
|
||||||
'timeformat': "%Y-%m-%d %H:%M",
|
'timeformat': "%Y-%m-%d %H:%M",
|
||||||
|
@ -55,6 +49,7 @@ class Journal(object):
|
||||||
self.dateparse = pdt.Calendar(consts)
|
self.dateparse = pdt.Calendar(consts)
|
||||||
self.key = None # used to decrypt and encrypt the journal
|
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
|
||||||
|
|
||||||
journal_txt = self.open()
|
journal_txt = self.open()
|
||||||
self.entries = self.parse(journal_txt)
|
self.entries = self.parse(journal_txt)
|
||||||
|
@ -77,7 +72,7 @@ class Journal(object):
|
||||||
plain = crypto.decrypt(cipher[16:])
|
plain = crypto.decrypt(cipher[16:])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
util.prompt("ERROR: Your journal file seems to be corrupted. You do have a backup, don't you?")
|
util.prompt("ERROR: Your journal file seems to be corrupted. You do have a backup, don't you?")
|
||||||
sys.exit(-1)
|
sys.exit(1)
|
||||||
padding = " ".encode("utf-8")
|
padding = " ".encode("utf-8")
|
||||||
if not plain.endswith(padding): # Journals are always padded
|
if not plain.endswith(padding): # Journals are always padded
|
||||||
return None
|
return None
|
||||||
|
@ -95,33 +90,30 @@ class Journal(object):
|
||||||
plain += b" " * (AES.block_size - len(plain) % AES.block_size)
|
plain += b" " * (AES.block_size - len(plain) % AES.block_size)
|
||||||
return iv + crypto.encrypt(plain)
|
return iv + crypto.encrypt(plain)
|
||||||
|
|
||||||
def make_key(self, prompt="Password: "):
|
def make_key(self, password):
|
||||||
"""Creates an encryption key from the default password or prompts for a new password."""
|
"""Creates an encryption key from the default password or prompts for a new password."""
|
||||||
password = self.config['password'] or util.getpass(prompt)
|
|
||||||
self.key = hashlib.sha256(password.encode("utf-8")).digest()
|
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']
|
||||||
journal = None
|
|
||||||
|
|
||||||
if self.config['encrypt']:
|
if self.config['encrypt']:
|
||||||
with open(filename, "rb") as f:
|
with open(filename, "rb") as f:
|
||||||
journal = f.read()
|
journal_encrypted = f.read()
|
||||||
decrypted = None
|
|
||||||
attempts = 0
|
def validate_password(password):
|
||||||
while decrypted is None:
|
self.make_key(password)
|
||||||
self.make_key()
|
return self._decrypt(journal_encrypted)
|
||||||
decrypted = self._decrypt(journal)
|
|
||||||
if decrypted is None:
|
# Soft-deprecated:
|
||||||
attempts += 1
|
journal = None
|
||||||
self.config['password'] = None # This password doesn't work.
|
if 'password' in self.config:
|
||||||
if attempts < 3:
|
journal = validate_password(self.config['password'])
|
||||||
util.prompt("Wrong password, try again.")
|
if not journal:
|
||||||
else:
|
journal = util.get_password(keychain=self.name, validator=validate_password)
|
||||||
util.prompt("Extremely wrong password.")
|
|
||||||
sys.exit(-1)
|
|
||||||
journal = decrypted
|
|
||||||
else:
|
else:
|
||||||
with codecs.open(filename, "r", "utf-8") as f:
|
with codecs.open(filename, "r", "utf-8") as f:
|
||||||
journal = f.read()
|
journal = f.read()
|
||||||
|
|
|
@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__title__ = 'jrnl'
|
__title__ = 'jrnl'
|
||||||
__version__ = '1.5.7'
|
__version__ = '1.6.0-dev'
|
||||||
__author__ = 'Manuel Ebert'
|
__author__ = 'Manuel Ebert'
|
||||||
__license__ = 'MIT License'
|
__license__ = 'MIT License'
|
||||||
__copyright__ = 'Copyright 2013 Manuel Ebert'
|
__copyright__ = 'Copyright 2013 Manuel Ebert'
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
import readline, glob
|
import readline
|
||||||
|
import glob
|
||||||
import getpass
|
import getpass
|
||||||
try: import simplejson as json
|
try: import simplejson as json
|
||||||
except ImportError: import json
|
except ImportError: import json
|
||||||
|
@ -25,7 +26,6 @@ default_config = {
|
||||||
},
|
},
|
||||||
'editor': "",
|
'editor': "",
|
||||||
'encrypt': False,
|
'encrypt': False,
|
||||||
'password': "",
|
|
||||||
'default_hour': 9,
|
'default_hour': 9,
|
||||||
'default_minute': 0,
|
'default_minute': 0,
|
||||||
'timeformat': "%Y-%m-%d %H:%M",
|
'timeformat': "%Y-%m-%d %H:%M",
|
||||||
|
@ -35,7 +35,7 @@ default_config = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def update_config(config, config_path=os.path.expanduser("~/.jrnl_conf")):
|
def upgrade_config(config, config_path=os.path.expanduser("~/.jrnl_conf")):
|
||||||
"""Checks if there are keys missing in a given config dict, and if so, updates the config file accordingly.
|
"""Checks if there are keys missing in a given config dict, and if so, updates the config file accordingly.
|
||||||
This essentially automatically ports jrnl installations if new config parameters are introduced in later
|
This essentially automatically ports jrnl installations if new config parameters are introduced in later
|
||||||
versions."""
|
versions."""
|
||||||
|
@ -73,11 +73,12 @@ def install_jrnl(config_path='~/.jrnl_config'):
|
||||||
password = getpass.getpass("Enter password for journal (leave blank for no encryption): ")
|
password = getpass.getpass("Enter password for journal (leave blank for no encryption): ")
|
||||||
if password:
|
if password:
|
||||||
default_config['encrypt'] = True
|
default_config['encrypt'] = True
|
||||||
|
if util.yesno("Do you want to store the password in your keychain?", default=True):
|
||||||
|
util.set_keychain("default", password)
|
||||||
print("Journal will be encrypted.")
|
print("Journal will be encrypted.")
|
||||||
print("If you want to, you can store your password in .jrnl_config and will never be bothered about it again.")
|
|
||||||
else:
|
else:
|
||||||
password = None
|
password = None
|
||||||
print("PyCrypto not found. To encrypt your journal, install the PyCrypto package from http://www.pycrypto.org and run 'jrnl --encrypt'. For now, your journal will be stored in plain text.")
|
print("PyCrypto not found. To encrypt your journal, install the PyCrypto package from http://www.pycrypto.org or with 'pip install pycrypto' and run 'jrnl --encrypt'. For now, your journal will be stored in plain text.")
|
||||||
|
|
||||||
# Use highlighting:
|
# Use highlighting:
|
||||||
if not module_exists("colorama"):
|
if not module_exists("colorama"):
|
||||||
|
|
25
jrnl/jrnl.py
25
jrnl/jrnl.py
|
@ -86,10 +86,12 @@ def get_text_from_editor(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. """
|
||||||
journal.config['password'] = ""
|
password = util.getpass("Enter new password: ")
|
||||||
journal.make_key(prompt="Enter new password:")
|
journal.make_key(password)
|
||||||
journal.config['encrypt'] = True
|
journal.config['encrypt'] = True
|
||||||
journal.write(filename)
|
journal.write(filename)
|
||||||
|
if util.yesno("Do you want to store the password in your keychain?", default=True):
|
||||||
|
util.set_keychain(journal.name, password)
|
||||||
util.prompt("Journal encrypted to {0}.".format(filename or journal.config['journal']))
|
util.prompt("Journal encrypted to {0}.".format(filename or journal.config['journal']))
|
||||||
|
|
||||||
def decrypt(journal, filename=None):
|
def decrypt(journal, filename=None):
|
||||||
|
@ -105,16 +107,18 @@ def touch_journal(filename):
|
||||||
util.prompt("[Journal created at {0}]".format(filename))
|
util.prompt("[Journal created at {0}]".format(filename))
|
||||||
open(filename, 'a').close()
|
open(filename, 'a').close()
|
||||||
|
|
||||||
def update_config(config, new_config, scope):
|
def update_config(config, new_config, scope, force_local=False):
|
||||||
"""Updates a config dict with new values - either global if scope is None
|
"""Updates a config dict with new values - either global if scope is None
|
||||||
of config['journals'][scope] is just a string pointing to a journal file,
|
or config['journals'][scope] is just a string pointing to a journal file,
|
||||||
or within the scope"""
|
or within the scope"""
|
||||||
if scope and type(config['journals'][scope]) is dict: # Update to journal specific
|
if scope and type(config['journals'][scope]) is dict: # Update to journal specific
|
||||||
config['journals'][scope].update(new_config)
|
config['journals'][scope].update(new_config)
|
||||||
|
elif scope and force_local: # Convert to dict
|
||||||
|
config['journals'][scope] = {"journal": config['journals'][scope]}
|
||||||
|
config['journals'][scope].update(new_config)
|
||||||
else:
|
else:
|
||||||
config.update(new_config)
|
config.update(new_config)
|
||||||
|
|
||||||
|
|
||||||
def cli(manual_args=None):
|
def cli(manual_args=None):
|
||||||
if not os.path.exists(CONFIG_PATH):
|
if not os.path.exists(CONFIG_PATH):
|
||||||
config = install.install_jrnl(CONFIG_PATH)
|
config = install.install_jrnl(CONFIG_PATH)
|
||||||
|
@ -126,7 +130,7 @@ def cli(manual_args=None):
|
||||||
util.prompt("[There seems to be something wrong with your jrnl config at {0}: {1}]".format(CONFIG_PATH, e.message))
|
util.prompt("[There seems to be something wrong with your jrnl config at {0}: {1}]".format(CONFIG_PATH, e.message))
|
||||||
util.prompt("[Entry was NOT added to your journal]")
|
util.prompt("[Entry was NOT added to your journal]")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
install.update_config(config, config_path=CONFIG_PATH)
|
install.upgrade_config(config, config_path=CONFIG_PATH)
|
||||||
|
|
||||||
original_config = config.copy()
|
original_config = config.copy()
|
||||||
# check if the configuration is supported by available modules
|
# check if the configuration is supported by available modules
|
||||||
|
@ -159,7 +163,7 @@ def cli(manual_args=None):
|
||||||
util.prompt("[Error: {0} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal']))
|
util.prompt("[Error: {0} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal']))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
journal = Journal.Journal(**config)
|
journal = Journal.Journal(journal_name, **config)
|
||||||
|
|
||||||
if mode_compose and not args.text:
|
if mode_compose and not args.text:
|
||||||
if config['editor']:
|
if config['editor']:
|
||||||
|
@ -205,22 +209,21 @@ def cli(manual_args=None):
|
||||||
encrypt(journal, filename=args.encrypt)
|
encrypt(journal, filename=args.encrypt)
|
||||||
# Not encrypting to a separate file: update config!
|
# Not encrypting to a separate file: update config!
|
||||||
if not args.encrypt:
|
if not args.encrypt:
|
||||||
update_config(original_config, {"encrypt": True, "password": ""}, journal_name)
|
update_config(original_config, {"encrypt": True}, journal_name, force_local=True)
|
||||||
install.save_config(original_config, config_path=CONFIG_PATH)
|
install.save_config(original_config, config_path=CONFIG_PATH)
|
||||||
|
|
||||||
elif args.decrypt is not False:
|
elif args.decrypt is not False:
|
||||||
decrypt(journal, filename=args.decrypt)
|
decrypt(journal, filename=args.decrypt)
|
||||||
# Not decrypting to a separate file: update config!
|
# Not decrypting to a separate file: update config!
|
||||||
if not args.decrypt:
|
if not args.decrypt:
|
||||||
update_config(original_config, {"encrypt": False, "password": ""}, journal_name)
|
update_config(original_config, {"encrypt": False}, journal_name, force_local=True)
|
||||||
install.save_config(original_config, config_path=CONFIG_PATH)
|
install.save_config(original_config, config_path=CONFIG_PATH)
|
||||||
|
|
||||||
elif args.delete_last:
|
elif args.delete_last:
|
||||||
last_entry = journal.entries.pop()
|
last_entry = journal.entries.pop()
|
||||||
util.prompt("[Deleted Entry:]")
|
util.prompt("[Deleted Entry:]")
|
||||||
print(last_entry)
|
print(last_entry.pprint())
|
||||||
journal.write()
|
journal.write()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
cli()
|
cli()
|
||||||
|
|
||||||
|
|
38
jrnl/util.py
38
jrnl/util.py
|
@ -4,6 +4,7 @@ import sys
|
||||||
import os
|
import os
|
||||||
from tzlocal import get_localzone
|
from tzlocal import get_localzone
|
||||||
import getpass as gp
|
import getpass as gp
|
||||||
|
import keyring
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
PY3 = sys.version_info[0] == 3
|
PY3 = sys.version_info[0] == 3
|
||||||
|
@ -14,12 +15,42 @@ STDOUT = sys.stdout
|
||||||
TEST = False
|
TEST = False
|
||||||
__cached_tz = None
|
__cached_tz = None
|
||||||
|
|
||||||
def getpass(prompt):
|
def getpass(prompt="Password: "):
|
||||||
if not TEST:
|
if not TEST:
|
||||||
return gp.getpass(prompt)
|
return gp.getpass(prompt)
|
||||||
else:
|
else:
|
||||||
return py23_input(prompt)
|
return py23_input(prompt)
|
||||||
|
|
||||||
|
def get_password(validator, keychain=None, max_attempts=3):
|
||||||
|
pwd_from_keychain = keychain and get_keychain(keychain)
|
||||||
|
password = pwd_from_keychain or getpass()
|
||||||
|
result = validator(password)
|
||||||
|
# Password is bad:
|
||||||
|
if not result and pwd_from_keychain:
|
||||||
|
set_keychain(keychain, None)
|
||||||
|
attempt = 1
|
||||||
|
while not result and attempt < max_attempts:
|
||||||
|
prompt("Wrong password, try again.")
|
||||||
|
password = getpass()
|
||||||
|
result = validator(password)
|
||||||
|
attempt += 1
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
prompt("Extremely wrong password.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def get_keychain(journal_name):
|
||||||
|
return keyring.get_password('jrnl', journal_name)
|
||||||
|
|
||||||
|
def set_keychain(journal_name, password):
|
||||||
|
if password is None:
|
||||||
|
try:
|
||||||
|
keyring.delete_password('jrnl', journal_name)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
elif not TEST:
|
||||||
|
keyring.set_password('jrnl', journal_name, password)
|
||||||
|
|
||||||
def u(s):
|
def u(s):
|
||||||
"""Mock unicode function for python 2 and 3 compatibility."""
|
"""Mock unicode function for python 2 and 3 compatibility."""
|
||||||
|
@ -35,6 +66,11 @@ def py23_input(msg):
|
||||||
STDERR.write(u(msg))
|
STDERR.write(u(msg))
|
||||||
return STDIN.readline().strip()
|
return STDIN.readline().strip()
|
||||||
|
|
||||||
|
def yesno(prompt, default=True):
|
||||||
|
prompt = prompt.strip() + (" [Yn]" if default else "[yN]")
|
||||||
|
raw = py23_input(prompt)
|
||||||
|
return {'y': True, 'n': False}.get(raw.lower(), default)
|
||||||
|
|
||||||
def get_local_timezone():
|
def get_local_timezone():
|
||||||
"""Returns the Olson identifier of the local timezone.
|
"""Returns the Olson identifier of the local timezone.
|
||||||
In a happy world, tzlocal.get_localzone would do this, but there's a bug on OS X
|
In a happy world, tzlocal.get_localzone would do this, but there's a bug on OS X
|
||||||
|
|
|
@ -5,3 +5,4 @@ pycrypto >= 2.6
|
||||||
argparse==1.2.1
|
argparse==1.2.1
|
||||||
tzlocal == 1.0
|
tzlocal == 1.0
|
||||||
slugify==0.0.1
|
slugify==0.0.1
|
||||||
|
keyring==3.0.5
|
||||||
|
|
3
setup.py
3
setup.py
|
@ -73,7 +73,8 @@ setup(
|
||||||
"pytz>=2013b",
|
"pytz>=2013b",
|
||||||
"tzlocal==1.0",
|
"tzlocal==1.0",
|
||||||
"slugify>=0.0.1",
|
"slugify>=0.0.1",
|
||||||
"colorama>=0.2.5"
|
"colorama>=0.2.5",
|
||||||
|
"keyring>=3.0.5"
|
||||||
] + [p for p, cond in conditional_dependencies.items() if cond],
|
] + [p for p, cond in conditional_dependencies.items() if cond],
|
||||||
extras_require = {
|
extras_require = {
|
||||||
"encrypted": "pycrypto>=2.6"
|
"encrypted": "pycrypto>=2.6"
|
||||||
|
|
Loading…
Add table
Reference in a new issue