• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Пишем облако шифрованных файлов на Flask

CognitoInc

(L3) cache
Пользователь
Регистрация
22.01.2024
Сообщения
155
Реакции
346
В данной статье я планирую написать облако файлов с веб-интерфейсом и шифрованием файлов. Это достаточно несложно, так что уровень познания не нужен самый большой, думаю, и новички поймут.Облако будет написано полностью на Python с использованием Flask.Шифрование я планирую осуществлять с помощью AES. Мне кажется, что это нормальный метод, но если есть варианты получше, буду рад прочитать ваши предложения в комментариях.Каков будет функционал?
  1. Страница авторизации.
  2. Веб-панель со списком всех загруженных файлов.
  3. Шифрование файлов с помощью AES.
  4. Загрузка файлов в облако через веб-панель.
Для чего может пригодиться данный опыт написания облака с шифрованием файлов?
  1. Данный опыт можно применить не только для того, чтобы создать облако только для себя, но также можно на его основе сделать полноценный сервис для использования другими людьми.
  2. Также этот софт и полученные знания при его написании можно применить к написанию панели для хранения логов (думаю, такая идея подойдет лучше всего для людей с данного форума).
  3. Просто понимание того, как работать с шифрованием и расшифровкой файлов. Это тоже достаточно полезно. Так как AES шифрование также используется и для расшифровки куки в Chromium браузерах.
  4. Также будет затронута работа с CSS, HTML. Информации на эту тему я расскажу не много, но базовые знания предоставлю.
  5. Работа с базами данных также будет полезна практически во всех проектах, которые вы решите реализовать.
С основной информацией о том, что нам предстоит изучить и написать в данной статье, мы разобрались, теперь приступим к написанию самого облака.Первым делом создадим проект. IDE я, как обычно, буду использовать PyCharm. А версию Python я выбрал 3.11.Показывать, как создавать проект, я не собираюсь, так как уже показывал это в предыдущих статьях. Но все же уточню, что желательно использовать виртуальную среду, созданную прямо в папке проекта.Для этого в правом нижнем углу PyCharm нажимаем туда, куда указано на скриншоте.
1715204087179.png


В появившемся окне выбираем путь, где у вас находится Python, и путь до проекта, где будет создана виртуальная среда разработки (venv).
Screenshot_1.png


После настройки виртуальной среды можно начать писать код. Сначала напишем основу веб-сервера Flask для нашей будущей панели. Для этого нам необходимо скачать и установить библиотеку Flask. Это делается введением команды в терминале PyCharm: pip install flask.

Теперь в основном файле Python проекта вызываем скачанную библиотеку Flask:
Python:
from flask import Flask, render_template

Эта строка отвечает за создание объекта приложения Flask:
Python:
app = Flask(__name__)

Этот код определяет страницу, которая будет открываться при вводе ссылки на ваш сайт без упоминания какой-либо конкретной страницы на нем, то есть это будет страница, открывающаяся по умолчанию.
Python:
@app.route('/')
def index():
    # Возвращаем HTML страницу, используя шаблон lists.html
    return render_template('lists.html')

Этот код отвечает за запуск Flask сервера и также обозначает, что страница Python является основной в нашем приложении. Если у нас будут еще дополнительные файлы Python, то запускать мы будем именно этот код, а остальные файлы будут подтягиваться благодаря вызову функций в основном файле.
Python:
if __name__ == '__main__':
    # Запускаем сервер Flask
    app.run(debug=True)

Теперь нам нужно создать HTML страницу, которая будет открываться. Ее название мы уже указали в Python файле, а именно: lists.html.Для начала нам нужно создать папку templates. Почему нам нужно называть папку именно так и зачем нам вообще папка, я рассказывал в прошлых статьях. Но если кратко - это стандартный путь, который прописан в Flask для поиска HTML файлов.Вот сам код пока что пустой страницы:
HTML:
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Список</title>
</head>
<body>

</body>
</html>

Эта строка обозначает язык страницы, это нужно, как минимум, для того чтобы браузер корректно предлагал автоперевод.
HTML:
<html lang="ru">

В этой строке указывается название страницы, которое будет отображаться не вкладке в браузере.
HTML:
<title>Список</title>

А внутри этого кода будут написаны все основные объекты на странице. В принципе, весь HTML код нужно писать именно внутри этого кода.
HTML:
<body>

</body>

Что ж, базу всех баз мы разобрали. Теперь нужно запустить сервер Flask, а именно наш Python файл, я назвал его main.py. Если все правильно и без ошибок, то вы должны будете увидеть такую вот картину в консоли:
1715204754575.jpeg


Перейдя по указанной в консоли ссылке, вы попадете на нашу страницу. Она будет пустой и без каких-либо стилей. Теперь я бы хотел написать создание базы данных при запуске нашего Flask сервера.
Что такое база данных?
Реляционная база данных (SQL) это база, которая будет хранить конкретные данные, которые мы назначим в виде структурированной таблицы. База данных будет использовать SQLite.

В таблице будут такие столбцы:
  1. Уникальный ID файла
  2. Имя файла
  3. Сам файл в виде шифрованных байтов
Мы выяснили, что такое база данных, и разобрали ее будущую структуру. Теперь приступим к реализации.
Для начала нам потребуется установить библиотеку Flask-SQLAlchemy и вызвать ее в нашем Python файле.
Python:
from flask_sqlalchemy import SQLAlchemy

Теперь укажем путь к базе данных:
Python:
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///data.db'

Создадим объект базы данных (точно так же мы делали с объектом Flask):
Python:
db = SQLAlchemy(app)

Теперь определим ее структуру, а именно столбцы id, filename, data, iv.
Python:
class File(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    filename = db.Column(db.String(100), nullable=False)
    data = db.Column(db.LargeBinary, nullable=False)
    iv = db.Column(db.LargeBinary, nullable=False)

А теперь создаем базу данных:
Python:
db.create_all()

Я специально показал путь создания базы данных по пунктам, чтобы вы могли легко применить это в других проектах, но если вам не хочется вникать, то вот полный код нашего получившегося файла main.py:
Python:
from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///data.db'

db = SQLAlchemy(app)


class File(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    filename = db.Column(db.String(100), nullable=False)
    data = db.Column(db.LargeBinary, nullable=False)
    iv = db.Column(db.LargeBinary, nullable=False)

@app.route('/')
def index():
    return render_template('lists.html')


if __name__ == '__main__':
    with app.app_context():
        db.create_all()
    app.run(debug=True)

При запуске приложения у вас создастся папка instance (дефолтный путь к базе данных), а в ней уже будет наша база данных.
1715205409022.png


Теперь приступим к написанию функционала для загрузки файла и его шифрования через AES перед записью в базу данных. Для начала обновим наш python файл:
Python:
import io
from flask import Flask, render_template, request, send_file, make_response, jsonify
from flask_sqlalchemy import SQLAlchemy
from werkzeug.utils import secure_filename
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
import os

app = Flask(__name__)

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///data.db'

db = SQLAlchemy(app)


class File(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    filename = db.Column(db.String(100), nullable=False)
    data = db.Column(db.LargeBinary, nullable=False)
    iv = db.Column(db.LargeBinary, nullable=False)


def encrypt_file(input_file, key):
    cipher = AES.new(key, AES.MODE_CBC)
    plaintext = input_file.read()
    ciphertext = cipher.encrypt(plaintext)
    return cipher.iv, ciphertext


def decrypt_file(data, key, iv):
    cipher = AES.new(key, AES.MODE_CBC, iv)
    return cipher.decrypt(data)


def allowed_file(filename):
    allowed_formats = {'exe', 'rar', 'zip'}
    _, file_extension = os.path.splitext(filename)
    if file_extension[1:].lower() in allowed_formats:
        return True
    return False


@app.route('/', methods=['GET', 'POST'])
def index():
    key = None
    files = File.query.all()  # Получаем все записи из базы данных
    if request.method == 'POST':
        file = request.files['file']
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)

            key = get_random_bytes(32)  # Генерация случайного ключа
            iv, encrypted_data = encrypt_file(file, key)

            new_file = File(filename=filename, data=encrypted_data, iv=iv)
            db.session.add(new_file)
            db.session.commit()

            key = key.hex()

        else:
            return "Файл не загружен. Недопустимый формат файла."

    return render_template('lists.html', key=key, files=files)


@app.route('/download/<int:file_id>', methods=['POST'])
def download(file_id):
    key_hex = request.form.get('key')
    if key_hex:
        key = bytes.fromhex(key_hex)  # Преобразуем строку ключа обратно в байты
        file = File.query.get(file_id)
        if file:
            decrypted_data = decrypt_file(file.data, key, file.iv)

            response = make_response(decrypted_data)
            response.headers['Content-Type'] = 'application/octet-stream'
            response.headers['Content-Disposition'] = f'attachment; filename="{file.filename}"'
            return response

    return jsonify({'error': 'Invalid key or file ID'}), 400


if __name__ == '__main__':
    with app.app_context():
        db.create_all()
    app.run(debug=True)

Очень важно! PyCharm предложит вам установить недостающую библиотеку Crypto. Но это не то, что нам нужно, если вы ее скачали, то удалите ее, а на ее место нужно установить библиотеку pycryptodome. Есть также библиотека pycryptodomex, по сути это та же самая библиотека, но по какой-то причине с ней ничего не работает.
В функции index происходит следующее:
  1. Генерируется случайный ключ для шифрования и расшифровки файла.
  2. Полученные данные (файл) шифруются с помощью сгенерированного ключа через AES. (Шифруется файл с помощью другой функции, а здесь эта функция просто вызывается.)
  3. Далее происходит запись получившихся данных в базу данных.
  4. Также имеется проверка, если файл имеет неразрешенный формат, то выводится уведомление об этом и файл не шифруется и не загружается.
Функция allowed_file нужна для того, чтобы проверять, разрешен ли выбранный файл для загрузки на сервер. А именно, происходит проверка формата файла.

Функция encrypt_file отвечает за шифрование полученных данных.
  1. Для начала создается объект шифрования AES с ключом, который генерируется в функции index.
  2. Затем читаются данные полученного файла.
  3. После получения данных они шифруются с помощью объекта шифрования, который создался ранее.
  4. Получение шифрованных данных, а именно: само приложение и вектор инициализации.
Функция decrypt_file отвечает за расшифровку данных, используя данные из столбца data, который находится в таблице (зашифрованные данные), а также данные из столбца iv (вектор инициализации) и полученного ключа для расшифровки.
Для расшифровки вызывается метод decrypt(data) из библиотеки pycryptodome.

Функция download, как ни странно, отвечает за скачивание файла.
  1. Получает запрос с ID файла и его ключом, введенным на сайте.
  2. Если ключ был передан, то пытается его конвертировать обратно в байты.
  3. Используя полученный ID, пытается найти зашифрованные данные в базе данных и вектор инициализации.
  4. Если данные найдены, то передает их вместе с ключом и вектором инициализации в функцию decrypt_file. Полученные результаты сохраняются в переменной decrypted_data.
  5. Если данные расшифрованы, отправляет HTTP ответ с полученными данными для последующего скачивания.
Что такое этот ваш вектор инициализации?
Если просто, то это набор случайных байтов, они добавляются к еще не зашифрованным данным перед их шифрованием. Это необходимо для обеспечения уникальности полученных данных.

Теперь приступим к дописыванию HTML страницы. Мы будем дописывать её для того, чтобы добавить возможность выбора файла с компьютера и вывода ключа для расшифровки.

В тело (body) добавляем форму, в которой будет находиться 2 кнопки:
HTML:
<form method="post" enctype="multipart/form-data">
    <input type="file" name="file" accept=".exe, .rar, .zip">
    <input type="submit" value="Загрузить">
</form>

Ниже, также в теле (body), добавляем отображение ключа (key берется из функции index):
Python:
{% if key %}
<p>Ключ для расшифровки: {{ key }}</p>
{% endif %}

Тестовая версия готова. Если все правильно, то при заходе на страницу и загрузке файла вы увидите такую картину:
1715206152536.png


Теперь я думаю, нам нужно сделать вывод всех файлов на странице и к каждому файлу добавить кнопку "Скачать" (весь функционал на Python был показан выше, там уже имеется функционал для кнопки "Скачать" и прочее). Для этого нам нужно снова отредактировать HTML.
Добавляем этот код внутрь тела (body):
HTML:
<table border="1">
    <thead>
        <tr>
            <th>ID</th>
            <th>Filename</th>
            <th>Action</th>
        </tr>
    </thead>
    <tbody>
        {% for file in files %}
        <tr>
            <td>{{ file.id }}</td>
            <td>{{ file.filename }}</td>
            <td>
                <form action="/download/{{ file.id }}" method="post">
                    <input type="text" name="key" placeholder="Введите ключ" required>
                    <button type="submit">Скачать</button>
                </form>
            </td>
        </tr>
        {% endfor %}
    </tbody>
</table>

<table> Сама таблица, в которой будут отображаться данные.

Этот код отвечает за заголовки столбцов, в которых будут храниться данные.
HTML:
<tr>
    <th>ID</th>
    <th>Filename</th>
    <th>Action</th>
</tr>


В этом коде и есть данные, которые хранятся в столбцах.
HTML:
{% for file in files %}
<tr>
    <td>{{ file.id }}</td>
    <td>{{ file.filename }}</td>
    <td>
        <form action="/download/{{ file.id }}" method="post">
            <input type="text" name="key" placeholder="Введите ключ" required>
            <button type="submit">Скачать</button>
        </form>
    </td>
</tr>


Вот полный код HTML-файла, если вам не важно, как это работает, а просто хотите скопировать:
HTML:
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Список</title>
</head>
<body>
    <form method="post" enctype="multipart/form-data">
        <input type="file" name="file" accept=".exe, .rar, .zip">
        <input type="submit" value="Загрузить">
    </form>

    {% if key %}
    <p>Ключ для расшифровки: {{ key }}</p>
    {% endif %}

    <table border="1">
        <thead>
            <tr>
                <th>ID</th>
                <th>Filename</th>
                <th>Action</th>
            </tr>
        </thead>
        <tbody>
            {% for file in files %}
            <tr>
                <td>{{ file.id }}</td>
                <td>{{ file.filename }}</td>
                <td>
                    <form action="/download/{{ file.id }}" method="post">
                        <input type="text" name="key" placeholder="Введите ключ" required>
                        <button type="submit">Скачать</button>
                    </form>
                </td>
            </tr>
            {% endfor %}
        </tbody>
    </table>
</body>
</html>

Теперь можно приступить к запуску и проверке текущей версии нашего облака.
Если всё удачно, то страница должна выглядеть так:
1715206492460.png


Выбираем нужный файл и нажимаем кнопку "Загрузить", и мы получим ключ для расшифровки файла.
1715206540061.png


А вся страница скачивания файлов должна будет выглядеть так:
1715206618546.png


Теперь я решил немного задизайнить нашу страницу со списком файлов.
  1. Для начала нам нужно создать папку static, а в ней создать CSS файл, я назвал его lists.css.
  2. Теперь нам нужно вызвать его в нашем HTML файле:
HTML:
<link rel="stylesheet" href="{{ url_for('static', filename='lists.css') }}">

Теперь приступим к самим стилям.
По умолчанию задний фон страницы белый, будем это исправлять.
Для того, чтобы сменить цвет заднего фона, нам понадобится перейти в наш CSS файл и прописать это:
CSS:
body{
background-color: #272932
}

Так как фон у меня темный, то темные буквы совсем не смотрятся, поэтому нужно изменить их цвет. Мы будем менять цвет именно в body. Поскольку все, что находится внутри body (а в нашем случае это все объекты на странице), будет принимать стили, указанные для body (только если эти стили не перекрывают другие стили, которые используются по умолчанию для определенных объектов). Вот как нам нужно изменить CSS, чтобы добиться белого цвета:
CSS:
body {
    background-color: #272932;
    color: white;
}

Если внимательно посмотреть на скриншот ниже, то можно заметить то, о чем я как раз и говорил: цвет текста на кнопках не изменился. Поэтому для кнопок нужно применять стиль именно к ним дополнительно.
1715207676384.png


Чтобы изменить цвет всем объектам определенного типа, нам нужно узнать, какой это тип. У кнопки это тип button. Поэтому создаем для него стиль:
CSS:
button{
color: white;
}

Теперь у нас получается, что цвет букв белый, и задний фон кнопок также белый. Поэтому сменим задний фон у кнопок, добавив в стили такую строку:
CSS:
background: #915bc8;

Также у кнопок абсолютно ужасные бордеры, поэтому отключим их:
CSS:
border: none;


Весь наш интерфейс находится справа в углу. Хотелось бы сделать его по центру. Для этого потребуется снова отредактировать стиль "body".
CSS:
display: flex;
flex-direction: column;
align-items: center;

Также для наглядности покажу, как изменить цвет бордеров на примере таблицы. Для этого нам нужно назначить стиль "table".
CSS:
table{
    border: 1px solid #4867e5;
    background: #272932;
}

Вот результат который мы получаем:
1715207987223.png


Особо заморачиваться над стилями не буду, так что на этом и остановимся, главное, что я показал, как вообще работать со стилями.

Раз основной функционал у нас готов, осталось совсем немного. Нам нужно добавить страницу авторизации. Для этого нам потребуется отредактировать наш Python файл, добавив туда это:
Python:
app.secret_key = 'your_secret_key'

Это нужно для того, чтобы защитить данные сессии пользователя.
Сессия хранит различные данные, такие как статус авторизации.

Теперь добавим логин и пароль для авторизации. Я не буду делать систему регистрации, а только авторизацию. Пользователь у нас будет один, но при желании можно создавать базу данных со списком пользователей и при регистрации вносить туда данные. Пример того, как работать с базой данных и обращаться к ней, я уже описал в этой статье. Но в данном случае я впишу данные для входа прямо в коде:
Python:
USERNAME = '123'
PASSWORD = '123'

В данный момент при переходе по ссылке у нас по умолчанию открывается страница с файлами, поэтому в функции index изменим отвечающую за это строку:
Python:
@app.route('/', methods=['GET', 'POST'])

на эту строку:
Python:
@app.route('/index', methods=['GET', 'POST'])

Теперь допишем код, чтобы по умолчанию открывалась наша будущая страница авторизации:
Python:
@app.route('/', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        if username == USERNAME and password == PASSWORD:
            session['logged_in'] = True
            return redirect(url_for('index'))
        else:
            return render_template('login.html', error='Неверный логин или пароль')
    return render_template('login.html', error=None)

Как видим в app.route, просто прописан слэш, поэтому страница будет открываться по умолчанию.

Что же написано в этом коде?
В данном коде принимаются данные из полей HTML-страницы авторизации и проверяется условие if, в котором прописано, являются ли полученные из формы логин и пароль тем логином и паролем, что прописаны у нас в коде. Если они равны, то открывается index (страница с файлами), если не равны, то выводится текст о том, что пароль или логин неверны.

Теперь приступим к написанию самой страницы авторизации. Для начала создадим в папке templates HTML-файл с названием login.html. В нем пропишем этот код:
HTML:
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Авторизация</title>
</head>
<body>
    <h1>Login</h1>
    {% if error %}
        <p style="color: red;">{{ error }}</p>
    {% endif %}
    <form method="post">
        <label for="username">Username:</label>
        <input type="text" id="username" name="username"><br><br>
        <label for="password">Password:</label>
        <input type="password" id="password" name="password"><br><br>
        <input type="submit" value="Login">
    </form>
</body>
</html>

Как видно из кода, в нём есть input объекты для логина и пароля, именно эти поля предназначены для ввода данных.
При запуске программы и переходе по ссылке откроется страница авторизации, как и задумывалось.
1715208476000.png


Особо не буду заморачиваться над дизайном, просто назначу цвет фона и отцентрирую все объекты, чтобы они не висели в правом верхнем углу экрана.
Для этого создаем CSS файл для страницы авторизации и подключаем его такой строкой:
HTML:
<link rel="stylesheet" href="{{ url_for('static', filename='login.css') }}">

Вставляем эту строку в раздел <head> нашей страницы.

Теперь переходим в CSS файл и применяем стили к нашему телу страницы "welcome to the club":
CSS:
body{
    background-color: #272932;
    color: white;
    display: flex;
    flex-direction: column;
    align-items: center;
}

Вот так теперь выглядит страница авторизации:
1715208623619.png


Заключение.
На этом статья подходит к концу. Мы рассмотрели, как шифровать файлы с помощью AES, как их расшифровывать, основы CSS, основы работы с базами данных, и применили все эти знания на практике через создание облака для хранения зашифрованных файлов.При создании проекта я полагался главным образом на свои знания и интуицию, поэтому не претендую на истинную верность моего подхода. Если у вас есть другие идеи или варианты реализации, буду рад услышать их в комментариях. Если будет интерес и активность по теме, я могу написать вторую статью о создании зеркала в onion для нашего обменника, системе регистрации с записью пользователей в базу данных, а также более подробно рассмотреть HTML и CSS.

Статья написана CognitoInc специально для форума xss.pro
 

Вложения

  • CryptoCloud.zip
    3.7 КБ · Просмотры: 26
Дополню, в изначальном коде авторизация не имеет смысла ибо можно сразу перейти в индекс, исправляем
В любое место
Python:
@app.before_request
def before_request():
    session.permanent = False
    if 'logged_in' not in session:
        session['logged_in'] = False
В самом начале функций index и download
Python:
if session['logged_in'] != True: return make_response('I\'m a teapot', 418)
Также при загрузке архива получил ошибку ValueError: Data must be padded to 16 byte boundary in CBC mode, решается это следующим образом

Python:
from Crypto.Util.Padding import pad, unpad

def encrypt_file(input_file, key):
    cipher = AES.new(key, AES.MODE_CBC)
    plaintext = input_file.read()
    padded_plaintext = pad(plaintext, AES.block_size)
    ciphertext = cipher.encrypt(padded_plaintext)
    return cipher.iv, ciphertext

def decrypt_file(data, key, iv):
    cipher = AES.new(key, AES.MODE_CBC, iv)
    decrypted_data = cipher.decrypt(data)
    return unpad(decrypted_data, AES.block_size)
 
Последнее редактирование:
Дополню, в изначальном коде авторизация не имеет смысла ибо можно сразу перейти в индекс, исправляем
В любое место
Python:
@app.before_request
def before_request():
    session.permanent = False
    if 'logged_in' not in session:
        session['logged_in'] = False
В самом начале функций index и download
Python:
if session['logged_in'] != True: return make_response('I\'m a teapot', 418)
Также при загрузке архива получил ошибку ValueError: Data must be padded to 16 byte boundary in CBC mode, решается это следующим образом

Python:
from Crypto.Util.Padding import pad, unpad

def encrypt_file(input_file, key):
    cipher = AES.new(key, AES.MODE_CBC)
    plaintext = input_file.read()
    padded_plaintext = pad(plaintext, AES.block_size)
    ciphertext = cipher.encrypt(padded_plaintext)
    return cipher.iv, ciphertext

def decrypt_file(data, key, iv):
    cipher = AES.new(key, AES.MODE_CBC, iv)
    decrypted_data = cipher.decrypt(data)
    return unpad(decrypted_data, AES.block_size)
Спасибо за фиксы!
Странно что у тебя на индекс можно было зайти, у меня при попытке сразу редирект на авторизацию был.
Ошибку при скачке архива кстать тоже не ловил. В любом случае, благодарю за помощь!
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх