Dear Diary, an Encrypted Command-Line Diary with Python

photos/p1415049480.87.png

In my last post, I wrote about how to work with encrypted SQLite databases with Python. As an example application of these libraries, I showed some code fragments for a fictional diary program. Because I was thinking the examples directory of the peewee repo was looking a little thin, I decided to flesh out the diary program and include it as an example.

In this post, I'll go over the diary code in the hopes that you may find it interesting or useful. The code shows how to use the peewee SQLCipher extension. I've also implemented a simple command-line menu loop. All told, the code is less than 100 lines!

Getting started

To follow along at home, you'll need to install a few libraries. The encryption is handled by SQLCipher, an open-source library that securely encrypts SQLite databases using AES-256 with cipher-block chaining. There are also python bindings which expose a db-api 2.0 compatible API. For detailed instructions on installation, refer to my previous post, but if you want things to just work, you can run the following:

$ pip install sqlcipher3

You will also need to install peewee:

$ pip install peewee

The database layer

We'll be using peewee to securely store and manage the database of diary entries. To get started, we will define our database connection and a model class representing the table of entries.

#!/usr/bin/env python

from peewee import *
from playhouse.sqlcipher_ext import SqlCipherDatabase

# Defer initialization of the database until the script is executed from the
# command-line.
db = SqlCipherDatabase(None)

class Entry(Model):
    content = TextField()
    timestamp = DateTimeField(default=datetime.datetime.now)

    class Meta:
        database = db

def initialize(passphrase):
    db.init('diary.db', passphrase=passphrase, kdf_iter=64000)
    Entry.create_table()

The python standard library includes a module for reading passwords from stdin without echoing the characters. We will use that module to securely accept the passphrase for unlocking the diary.

In the application's entry-point, we'll collect the passphrase, initialize the database, and enter the main menu loop (defined later in the post).

#!/usr/bin/env python

from getpass import getpass
import sys

from peewee import *
from playhouse.sqlcipher_ext import SqlCipherDatabase

# ... Database and model code from previous code snippet ...

if __name__ == '__main__':
    # Collect the passphrase using a secure method.
    passphrase = getpass('Enter password: ')

    if not passphrase:
        sys.stderr.write('Passphrase required to access diary.\n')
        sys.stderr.flush()
        sys.exit(1)

    # Initialize the database.
    initialize(passphrase)
    menu_loop()

Interactive menu loop

The diary will feature an interactive menu loop. Having interactive menus in your scripts is typically frowned-upon, because it hurts composability and is not unix-like. For the diary, though, it seemed like a good idea to me, as it allows you to enter your password once and then have many interactions with the app.

For simplicity, the menu will allow us to perform three operations:

Here is the structure for the menu loop and the functions the menu will delegate to:

#!/usr/bin/env python

from collections import OrderedDict
import datetime
from getpass import getpass
import sys

from peewee import *
from playhouse.sqlcipher_ext import SqlCipherDatabase

# ... Database definition and model code ...

def menu_loop():
    choice = None
    while choice != 'q':
        for key, value in menu.items():
            print('%s) %s' % (key, value.__doc__))
        choice = raw_input('Action: ').lower().strip()
        if choice in menu:
            menu[choice]()

def add_entry():
    """Add entry"""

def view_entries(search_query=None):
    """View previous entries"""

def search_entries():
    """Search entries"""

menu = OrderedDict([
    ('a', add_entry),
    ('v', view_entries),
    ('s', search_entries),
])

if __name__ == '__main__':
    # ... Application entry-point code ...

The menu loop is called after the database is initialized, then runs in a loop, displaying the menu and delegating to one of the three menu functions. The program ends when the user types q.

Let's start by defining the add_entry function. This function will accept multiple lines of input from the user, reading until an EOF is received (Ctrl+d on my computer). After the user has entered their text, the program will prompt the user whether they wish to save the entry and automatically return to the menu loop.

def add_entry():
    """Add entry"""
    print('Enter your entry. Press ctrl+d when finished.')
    data = sys.stdin.read().strip()
    if data and raw_input('Save entry? [Yn] ') != 'n':
        Entry.create(content=data)
        print('Saved successfully.')

Impressively, that's all the code it takes! The call to sys.stdin.read() will automatically read up to an EOF. Since the menu_loop delegated to the function, when the function exits we will be back in the loop, so no additional code is required.

Now let's define the view_entries function which will display previously-written entries. We have a couple options here -- we could get fancy and implement a less-style paging system, or we could go simple and just print every single entry in a massive wall-of-text. I chose to take the middle road -- display one entry at a time, and let the user continue to the next one or break out of the loop.

To accomplish this, view_entries will query entries ordered newest-to-oldest, then in a loop, each entry will be displayed and the user will then be prompted to continue to the next, or to quit. As with the add_entry function, as soon as the function exits, we will be back in the menu loop.

In order to re-use this logic for the search action, I've written view_entries to accept an optional search query.

def view_entries(search_query=None):
    """View previous entries"""
    query = Entry.select().order_by(Entry.timestamp.desc())
    if search_query:
        query = query.where(Entry.content.contains(search_query))

    for entry in query:
        timestamp = entry.timestamp.strftime('%A %B %d, %Y %I:%M%p')
        print(timestamp)
        print('=' * len(timestamp))
        print(entry.content)
        print('n) next entry')
        print('q) return to main menu')
        if raw_input('Choice? (Nq) ') == 'q':
            break

As eluded to in the previous paragraph, search_entries will simply delegate to the view_entries function after collecting the search query from the user. Here is the code:

def search_entries():
    """Search entries"""
    view_entries(raw_input('Search query: '))

And with that, our secret diary program is complete!

Ideas for improving the diary

As an exercise, here are some features that might be cool to add:

Thanks for reading

If you'd like to browse the complete code, you can find it here.

I hope you enjoyed reading this post! Feel free to leave any comments or questions using the form below.

Here are some blog posts on related topics you might be interested in:

Comments (2)

kmonsoor | nov 05 2014, at 05:19am

way cool, bro. i wish there would a VCS-integrated version; e.g. that would store those entries to a remote repo, so that can be accessible from any internet-connected device.

Joshua | nov 04 2014, at 09:35am

These are great, keep them coming!


Commenting has been closed.