Пишем сервис одноразовых записок на Python

КДПВ


У некоторых людей возникает необходимость передать небольшие сообщения. Но как это сделать, если вы пользуетесь различными социальными сетями и мессенджерами, в безопасности передачи данных через которые вы сомневаетесь.


Некоторые люди для этого используют сервисы самоуничтожающихся шифрованных записок. Но тут встает вопрос можно ли доверять этим сервисам и действительно ли они уничтожают записки после прочтения.


Для решения этой проблемы мы напишем свой сервис самоуничтожающихся шифрованных записок на языке Python с использованием модуля cryptography и фреймворка Flask и развернем его на облачном сервисе Heroku.


Все файлы для приложения можно скачать на github
Приложение в работе можно посмотреть на Heroku


Установка и настройка


Писать будем в виртуальном окружении Virtualenv.


Устанавливаем модуль virtualenv


pip install virtualenv

Создаем папку нашего проекта и в нем активируем виртуальное окружение


mkdir encnotes
cd encnotes
virtualenv venv
source venv/bin/activate (Для nix систем)
venv\Scripts\activate (Для Windows систем)

В папке проекта создаем текстовый файл requirements.txt в который записываем необходимые для установки модули:


cryptography
Flask
Flask-Migrate
Flask-SQLAlchemy
Flask-WTF
Flask-Bootstrap
Flask-SSLify

И с помощью pip устанавливаем модули, перечисленные в файле requirements.txt


pip install -r requirements.txt

Приложение


Создаем файл encnotes.py. Весь код приложения будет в этом файле. В самом начале импортируем все необходимые модули. Постепенно расскажу для чего они нужны.


import os
import random
from cryptography.fernet import Fernet
from flask import Flask, render_template, url_for
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_wtf import FlaskForm
from flask_bootstrap import Bootstrap
from flask_sslify import SSLify
from wtforms import TextAreaField, SubmitField
from wtforms.validators import DataRequired, Length

Один из удобных способов задания настроек программы, это объявить их в классе


class Config():
    SECRET_KEY = os.environ.get('SECRET_KEY')
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
    SITE_URL = 'https://encnote.herokuapp.com'

SECRET_KEY — ключ для создания безопасных форм. Для того чтобы не хранить этот ключ в скрипте, что является плохой практикой, мы сохраним его в переменной среды и будем брать оттуда. Расширение Flask-WTF использует этот ключ для защиты от атак Cross-Site Request Forgery сокращенно CSRF.


SQLALCHEMY_TRACK_MODIFICATIONS — отключает ненужную нам функцию, сигнализирующую
приложению каждый раз, когда в базе должно быть внесено изменение.


SITE_URL — его укажете позднее, когда создадите приложение на Heroku.


Создаем объект приложения как экземпляр класса Flask и загружаем в него настройки из созданного нами ранее класса Config и добавляем объекты импортированных модулей


app = Flask(__name__)
app.config.from_object(Config)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
bootstrap = Bootstrap(app)
sslify = SSLify(app)

Модель базы данных


Создаем модель для базы данных, в которой будем хранить зашифрованные записки. У нашей модели будет 3 поля:


  • id — идентификатор записи, будет создаваться автоматически
  • number — номер записки, по которому мы будем находить нашу записку в базе данных.
    Этот номер будет генерироваться рандомно в диапазоне от 1000000 до 9999999
  • ciptext — сам зашифрованный текст

По этой модели будет создана миграция для базы данных.


class Note(db.Model):
    __tablename__ = 'notes'
    id = db.Column(db.Integer, primary_key=True)
    number = db.Column(db.Integer, unique=True, nullable=False)
    ciptext = db.Column(db.Text, nullable=False)

    def __repr__(self):
        return f'<Note number: {self.number}'

Веб-форма


Для создания веб-формы ввода сообщение, которое будет зашифровано используем расширение Flask-WTF


class TextForm(FlaskForm):
    text = TextAreaField('Введите текст (максимум 1000 символов)',
                         validators=[DataRequired(), Length(1, 1000)])
    submit = SubmitField('Создать')

  • text — поле для ввода сообщения. Я применил 2 валидатора. Один для проверки введено ли сообщение, чтобы форма не разрешала отправить пустое сообщение. Второй валидатор для ограничения максимальной длинны строки в 1000 символов. Можете задать свое ограничение на длину строки.

Регистрация функции в контексте оболочки


Для того чтобы было удобнее проверить нашу модель для базы данных я зарегистрировал функцию как функцию в контексте оболочки. Это дает возможность не импортировать каждый раз модель базы данных и саму базу данных в оболочке.


@app.shell_context_processor
def make_shell_context():
    return {'db': db, 'Note': Note}

Обработчики маршрутов


Маршрут для главной страницы, на которой будет форма ввода сообщения


@app.route('/', methods=['GET', 'POST'])
def index():
    form = TextForm()
    if form.validate_on_submit():
        key = Fernet.generate_key()
        str_key = key.decode('ascii')
        f = Fernet(key)
        bin_string = form.text.data.encode('utf-8')
        cipher_text = f.encrypt(bin_string)
        str_cipher_text = cipher_text.decode('ascii')
        rnumber = random.randint(1000000, 9999999)
        while True:
            n = Note.query.filter_by(number=rnumber).first()
            if n:
                rnumber = random.randint(1000000, 9999999)
                continue
            break
        cipher_note = Note(number=rnumber, ciptext=str_cipher_text)
        link = f'{app.config["SITE_URL"]}/{rnumber}/{str_key}'
        db.session.add(cipher_note)
        db.session.commit()
        return render_template('complete.html', link=link)
    return render_template('index.html', form=form)

Создаем форму из класса, который мы написали ранее. Далее обрабатываем запрос POST, если пользователь ввел текст и нажал на кнопку "Зашифровать". Генерируем ключ и сохраняем его в базе данных. Генерируется ключ URL-safe base64, что значит используются только символы кодировки "Ascii" и без слеша и мы можем его использовать в качестве URL. Сообщение шифруется алгоритмом AES-128.


После создания ключа и шифрования нашего сообщения, которое также будет в формате URL-safe base64, нам нужно сгенерировать номер, который будет идентификатором для нашего зашифрованного сообщения. Чтобы проверить нет ли в базе данных сообщения с таким же номером, в цикле while мы выполняем поиск и генерируем новый номер, до тех пор, пока не найдем уникальный номер. После сохранения сообщения в базу данных, рендерим html страницу из шаблона complete.html в которой будет указана ссылка-ключ для расшифровки сообщения. Чтобы не уделять много времени html верстке я воспользовался расширением Flask-Bootstrap, которое использует шаблоны bootstrap.


Маршрут для страницы с вопросом


Этот маршрут открывается, при переходе по сгенерированной ссылке с ключом. Его можно было бы избежать, но многие сервисы обмена сообщений или социальные сети при передаче ссылки, делают ее пред просмотр. Нам такой вариант не подходит т.к. при пред просмотре будет расшифровываться текст и сообщение удаляться из базы.


@app.route('/<rnumber>/<str_key>')
def question(rnumber, str_key):
    link = f'{app.config["SITE_URL"]}/decrypt/{rnumber}/{str_key}'
    return render_template('question.html', link=link)

После нажатия на кнопку "Расшифровать" мы переходим на третий маршрут, с текстом нашего сообщения


@app.route('/decrypt/<int:rnumber>/<str_key>')
def decrypt(rnumber, str_key):
    cipher_note = Note.query.filter_by(number=rnumber).first_or_404()
    cipher_text = cipher_note.ciptext.encode('ascii')
    key = str_key.encode('ascii')
    try:
        f = Fernet(key)
        text = f.decrypt(cipher_text)
    except (ValueError, InvalidToken):
        return render_template('error.html')
    text = text.decode('utf-8')
    db.session.delete(cipher_note)
    db.session.commit()
    return render_template('decrypt.html', text=text)

Находим сообщение в базе и с помощью ключа из ссылки расшифровываем его. На случай введения неправильного ключа сделал обработчик ошибок в блоке try except.


Развертывание на Heroku


Для развертывания нашего приложения воспользуемся облачным хостинг-провайдером Heroku. Для демонстрации нашего приложения подойдет и бесплатный тариф. Один из минус которого, если в течении 30 минут не будет обращений на сайт, то он засыпает. После этого при заходе на него, нужно немного подождать.


Создайте аккаунт на Heroku и установите командную строку Heroku CLI. Развертывание приложения производиться с помощью средств управления версиями git
После установки Heroku CLI входим в систему


$ heroku login

Добавляем наш проект в git


$ git init

Создаем приложение в Heroku. Вам нужно придумать уникальное имя для вашего приложения, вместо моего encnote


heroku apps:create encnote
Creating ⬢ encnote... done
https://encnote.herokuapp.com/ | https://git.heroku.com/encnote.git

Для нашего приложения нам понадобиться база данных, сделаем ее в Heroku


heroku addons:add heroku-postgresql:hobby-dev
Creating heroku-postgresql:hobby-dev on ⬢ encnote... free
Database has been created and is available
 ! This database is empty. If upgrading, you can transfer
 ! data from another database with pg:copy
Created postgresql-graceful-12123 as DATABASE_URL
Use heroku addons:docs heroku-postgresql to view documentation

Heroku поместил адрес базы данных в переменную среды DATABASE_URL, и нам не придется делать это самостоятельно. Также нам нужно создать миграции для нашей базы данных, для этого сначала укажем переменную среды в нашей системе, указывающую на наше приложение Flask


Для nix систем export FLASK_APP=encnotes.py
Для Windows set FLASK_APP=encnotes.py


Также укажем расположение для временной базы данных, подставье свой адрес


$ export DATABASE_URL=sqlite:////home/user/Python/encnote/app.db (Для nix систем)
$ set DATABASE_URL=sqlite:///D:\Python\encnotes\app.db (Для Windows систем)

После этого выполняем инициализацию базы данных и создание миграций


flask db init
flask db migrate

В папке нашего приложения создастся папка migrate, с необходимыми миграциями. Файл app.db можно удалить.


Создадим файл .gitignore и пропишем в нем файлы, которые git должен игнорировать при загрузке на сервис Heroku


venv
__pycache__

Поскольку веб-сервер flask не достаточно надежен для использования в развертывании, мы будем использовать gunicorn. Чтобы наше приложение могло подключаться к базе данных Postgres добавим psycopg2. Для автоматического редиректа с http на https, чтобы данные передавались безопасным способом мы добавили и активировали расширение Flask-SSLify.


Добавьте следующие модули в файл requirements.txt


gunicorn
psycopg2

Для того чтобы Heroku знал как управлять приложением, нужно создать файл Procfile в каталоге приложения. В этот файл запишем:


web: flask db upgrade; gunicorn encnotes:app

С помощью этой строки мы даем команды Heroku. Сначала запустить апгрейд базы данных, затем запустить веб сервер.


Нам нужно задать переменные среды, секретный ключ для форм и имя файла с нашим приложением. В SECRET_KEY укажите свое значение.


heroku config:set SECRET_KEY=super-secret-work232
heroku config:set FLASK_APP=encnotes.py

Переменную DATABASE_URL с адресом базы данных создал Heroku, когда мы ввели команду


heroku addons:add heroku-postgresql:hobby-dev

Теперь мы все подготовили для развертывания. Добавляем наши файлы в git, делаем коммит и отправляем на heroku


git add .
git commit -m "Heroku deploy"
git push heroku master

Все, приложение готово! В данной статье я показал как легко можно делать небольшие
веб приложения использую фреймворк Flask и простой в использовании python модуль
для шифрования cryptography.

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 9

    +1

    Если уж заговорили о безопасности, то шифрование нужно было реализовывать не на сервере, а на клиенте.

      0
      Согласен с вами. Цель статьи показать практическое применение некоторых модулей и базовые операции в развертывании на Heroku
      0
      Шифрование конечно надо только E2E!
        0
        Как-то мы тестировали похожий готовый сервис и хотели проверить, какие мессенджеры и соц. сети переходят по ссылкам, пробовали даже в смс отправлять. В итоге, только vk.com переходил по ссылкам, а Fb, SMS, Whatsapp, Telegram не лезли не в свое дело.
          0
          Забыл, еще по email отправляли, mail.ru и ya.ru не переходили по ссылкам, что удивительно, т.к. среди спамеров бытует мнение, что почтовики переходят по ссылкам и смотрят, есть ли там редирект.
          Еще не понятно, как FB делает превью ссылки в сообщениях, если он не активирует срабатывание ссылки.
            0
            среди спамеров бытует мнение, что почтовики переходят по ссылкам

            Письма, приходящие на ящики-ловушки (mailtrap) вполне себе могут исследоваться, не затрагивая обычных пользователей.
            0
            WhatsApp/Telegram тоже ходят, но это зависит от настроек на клиенте (link preview или что-то типа).

            В любом случае при отправке линка через мессенжер нельзя расчитывать на то что он не пойдёт по ссылке, при отправке по почте нужно расчитывать на то что все ссылки будут просмотрены (хотя бы антивирусом).

            Как уже выше заметили, имеет смысл только E2E шифрование, и только с паролем или как минимум капчей (хотя бы примитивной), причём с удалением ссылки после первого прочтения.

            Впрочем, если речь по E2E то есть готовые решения типа PrivateBin.
            0
            Писать будем в виртуальном окружении Virtualenv.
            Устанавливаем модуль virtualenv
            pip install virtualenv
            Для Python 3 версии рекомендуется использовать встроенное средство виртуального окружения venv.
            Работает по-сути также как и virtualenv, но не требует установки через pip.

            Примечание, в моем случае для ubuntu/bionic64 (Ubuntu 18.04 LTS) требовалась доп. установка доп. пакетов Python:
            sudo apt-get install python3-venv
            		# The following additional packages will be installed:
            		# python-pip-whl python3-distutils python3-lib2to3 python3.6-venv

              0
              Спасибо за информацию, пока все еще в сдатии изучения Python-а нахожусь, приму к сведению

            Only users with full accounts can post comments. Log in, please.