Dear Diary — зашифрованный дневник в консоли на Python

Dear Diary - зашифрованный дневник в консоли на Python
Я уже писал о том, как работать с шифрованными базами данных на Python. И сегодня для закрепления материала мы напишем небольшую программу для хранения текстовых заметок в зашифрованном виде.

Для начала установим необходимые библиотеки. Шифрование возьмёт на себя SQLCipher — библиотека с открытым исходным кодом, которая шифрует базы SQLite алгоритмом AES-256 цепочками шифроблоков. Для этого установим соответствующий пакет:

$ pip install pysqlcipher
Также установим peewee для безопасной работы с записями базы данных:

$ pip install peewee

Работа с БД

Мы создадим подключение к базе данных и модель, представляющую таблицу с записями:

#!/usr/bin/env python

from peewee import *
from playhouse.sqlcipher_ext import SqlCipherDatabase

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(fail_silently=True)

В стандартной библиотеке python есть модуль для чтения паролей из stdin без вывода пароля на экран. Мы воспользуемся этим модулем для безопасного ввода пароля при разблокировке дневника.

В начале работы приложения мы принимаем пароль (как ключ к шифру БД), инициализируем базу данных, а затем выводим главное меню (о нём будет ниже):

#!/usr/bin/env python

from getpass import getpass
import sys

from peewee import *
from playhouse.sqlcipher_ext import SqlCipherDatabase

# ... Здесь код модели из предыдущего блока ...

if __name__ == '__main__':
# безопасно принимаем пароль.
passphrase = getpass('Enter password: ')

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

# инициализируем базу данных.
initialize(passphrase)
menu_loop()

Интерактивное меню

Обычно в мире *nix не принято делать интерактивные меню, это излишне усложняет программу. Однако для дневника, думаю, это отличный подход — так можно будет ввести пароль лишь один раз, а потом выполнять много различных операций в ней уже без пароля.

Для простоты сделаем наше меню выполняющим всего три операции:

  • Добавление новой записи
  • Вывод списка записей, отсортированных по дате создания
  • Поиск записей, основываясь на поиске подстроки в тексте

Вот структура меню и его операции:

#!/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

# ... Здесь код модели из предыдущего блока ...

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

def add_entry():
"""Добавление записи"""

def view_entries(search_query=None):
"""Вывод прошлых записей"""

def search_entries():
"""Поиск записей"""

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

if __name__ == '__main__':
# ... начало программы ...

Меню выводится после инициализации базы данных, предлагая выбор одного варианта из трёх. Программа завершает работу при нажатии q.

Создадим функцию add_entry. Она будет принимать строки, введённые пользователем, считывая их до получения EOF (Ctrl+d на моём компьютере). После ввода пользователем заметки, программа спросит о необходимости сохранить запись и вернётся в главное меню:

def 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.')

Вызов sys.stdin.read() автоматически читает до появления EOF. А так как menu_loop вызвал функцию, то при её завершении мы вернёмся обратно в главное меню.

Теперь создадим функцию view_entries, выводящую на экран сохранённые ранее записи. Мы будем выводить по одной записи на экран, а пользователь может либо запросить следующую, либо прекратить цикл и вернуться в меню.

Для повторного использования этой логики в дальнейшем, я написал функцию view_entries принимающей поисковый запрос:

def view_entries(search_query=None):
"""Вывод прошлых записей"""
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('{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}A {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}B {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}d, {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}Y {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}I:{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}M{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}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

А метод search_entries будет вызывать view_entries с передачей поискового запроса от пользователя. Вот его код:

def search_entries():
"""Поиск записей"""
view_entries(raw_input('Search query: '))

На этом наша программа завершена!

Что можно улучшить

Попробуйте самостоятельно реализовать следующий функционал:

  • Удаление записи
  • Умный постраничный вывод списка записей
  • Цвета в консоли для улучшения внешнего вида. Посмотрите в сторону Blessings для этого.
  • Редактирование записей
  • Календарный вид или поиск записей по дате
  • Веб-фронтенд?

Полный исходный код программы можно скачать здесь.

По материалам «Dear Diary, an Encrypted Command-Line Diary with Python»

Leave a Comment