7 ноября 2025 г.

Шифровальщик

Есть алгоритм шифрования на Rust, который стало интересно использовать в приложениях на Python. Что из этого получилось?

Программировать может каждый. Ранее у меня возникла идея создания программы на Python, которая с помощью библиотеки написанной на Rust предоставляет функционал для шифрования с помощью российского алгоритма «Кузнечик».

Kuznetchik

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

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

code_python

Задержим дыхание и продолжим изучение. Вот мы уже видим, что ядро программы это всего несколько модулей (файлов). В бэкенде и системном программировании, к которому относится Rust, вообще принято производить как можно меньше кода.

Анализ кода Rust

Как это работает? Есть две функции (encrypt и decrypt), которые отвечают за шифрацию и дешифрацию, соответственно. Хорошей практикой считается предоставлять пользователям удобный интерфейс, убирая «под капот» операции, которые можно автоматизировать без ущерба для эффективности. Дзен, когда код легко понимать и читать, а к нему прилагается в разумных пределах подробная и качественная документация (можно сказать, инструкция по использованию).

Разберёмся на примере функции encrypt, отвечающей за шифрацию. Опять немного «кошмариков». Так выглядит наша функция в Python.

def encrypt(plaintext: str | bytes, 
            *, 
            code: str, 
            mode: EncryptMode = EncryptMode.ECB) -> bytes:
    """Зашифровать предоставленные данные.

    :param plaintext: Данные для шифрования, текст или bytes-объект.
    :param code: Код шифрования.
    :param mode: Режим шифрования.
    :returns:
        Зашифрованный текст bytes-строкой в формате ASCII.
    :raises UnicodeEncodeErrors: При ошибках декодирования строковых значений
                                 в байт-строки.
    :raises ValueError: При предоставлении неверных аргументов.
    """

    def validate_inputs_data() -> None:
        """Проверка основных входных параметров.

        :raises ValueError: При обнаружении ошибок в данных.
        """
        if not isinstance(plaintext, (str, bytes)) or not plaintext:
            raise ValueError(
                'plaintext must be str, bytes and cannot be empty')
        if not isinstance(code, str) or not code:
            raise ValueError('code must be str and cannot be empty')
        if not isinstance(mode, EncryptMode):
            raise ValueError('mode must be an instance of EncryptMode')

    validate_inputs_data()

    hash_code, salt = get_hash_blake2b(code)
    plaintext_type = type(plaintext)
    if isinstance(plaintext, str):
        plaintext = plaintext.encode('utf-8')
    encoded_data = encrypting_rust(plaintext, code=hash_code, mode=mode)
    meta = make_meta(plaintext_type=plaintext_type, salt=salt, mode=mode)

    return base64.b64encode(meta + encoded_data)

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

girlsjpg

Начнём двигаться построчно. Станет проще, а со временем понятнее. Это всегда так. Во-первых, если код документирован, то часть информации содержится в docstrings, это текст заключенный в кавычки под названием функции. Как правило, там уже есть часть важной информации: функционал, принимаемые на вход аргументы и какой возвращается результат.

Некоторые IDE (от английского integrated development environment, интегрированная среда разработки), дополнительно обрабатывают эти блоки и выводят их содержание при наведении курсора на изучаемую функцию. Например, вот так выглядит наша encrypt:

ide_pycharm_encrypt

Теперь заглянем внутрь функции.

Первый блок: проверка переданных аргументов. Мы должны быть уверены, что нам передали нужный вид и тип данных (например, это точно текст), что вообще-то что передали при вызове.

if not isinstance(plaintext, (str, bytes)) or not plaintext:
            raise ValueError(
                'plaintext must be str, bytes and cannot be empty')
        if not isinstance(code, str) or not code:
            raise ValueError('code must be str and cannot be empty')
        if not isinstance(mode, EncryptMode):
            raise ValueError('mode must be an instance of EncryptMode')

В переводе на русский язык:

1) если переданный текст (plaintext) не является строкой или байтовым массивом, ну или вообще там пусто, возбуди исключение (вызови ошибку);

2) если код шифрования (code) не текстовая строка или пустая текстовая строка, тоже должна возникнуть ошибка;

3) если режим шифрования (mode) отличается от предустановленных вариантов, то и это ошибка.

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

Данные проверены. С ними можно работать. Идём дальше.

hash_code, salt = get_hash_blake2b(code)
    plaintext_type = type(plaintext)
    if isinstance(plaintext, str):
        plaintext = plaintext.encode('utf-8')

Хранить пароли в чистом виде сегодня — моветон. Любую, даже самую защищённую базу, могут увести. Поэтому пароли держат в виде «хэшей» — своеобразных цифровых отпечатков, которые создаёт математический алгоритм.

Взгляните. Для фразы Vlad Barmichev алгоритм SHA-256 выдаёт хэш такой:
B/0H0LLkjTJUkOk/seRCVPOmwHLnL8FSkWkhGt7DidU=

А для просто Vlad — уже другой:
QcyZ5hXNJiaxA4FOM64So8JHzg74NvRl0kPBKOXjnss=

Длина исходного текста роли не играет — хэш всегда одной длины. Детали могут разниться, но суть вот в чём: по паролю можно построить хэш, а вот восстановить пароль по хэшу — невозможно.

Это главное. Даже если злоумышленник получит базу хэшей, ему это мало что даст. Узнать исходные пароли он не сможет. При входе в систему она сама хэширует введённый пароль и сравнивает результат с хранящимся в базе. Подсунуть хэш вместо пароля не получится — на вход алгоритму нужно подать именно исходные данные.

Теперь — к нашему коду.

hash_code, salt = get_hash_blake2b(code)
Перевод: Получаем хэш пароля и "соль" алгоритмом BLAKE2b. Соль — это случайная строка, которая усложняет взлом, делая одинаковые пароли разными.

plaintext_type = type(plaintext)
Перевод: Запоминаем тип исходных данных (строка или байты), чтобы потом вернуть результат в том же формате.

if isinstance(plaintext, str):
plaintext = plaintext.encode('utf-8')
Перевод: Если исходный текст — это строка, преобразуем его в байты. Алгоритмы шифрования работают именно с байтами, а не с текстом.

encoded_data = encrypting_rust(plaintext, code=hash_code, mode=mode)
    meta = make_meta(plaintext_type=plaintext_type, salt=salt, mode=mode)

    return base64.b64encode(meta + encoded_data)

encoded_data = encrypting_rust(plaintext, code=hash_code, mode=mode)
Перевод: Передаём подготовленные данные и хэш-пароля в функцию шифрования. Режим mode указывает, какой именно алгоритм использовать. Функция encrypting_rust это «обёртка», через которую Python взаимодействует с шифровальщиком, написанным на Rust.

meta = make_meta(plaintext_type=plaintext_type, salt=salt, mode=mode)
Перевод: Формируем служебный заголовок (meta). В нём хранится вся информация, которая потом понадобится для расшифровки: тип исходных данных, соль и режим шифрования.

return base64.b64encode(meta + encoded_data)
Перевод: Функция возвращает итог — склеенные заголовок и зашифрованные данные, преобразованные в формат base64. Это удобно для хранения и передачи, так как результат представляет собой чистый текст.

Так выглядит процесс взаимодействия с шифратором со стороны Python. На секундочку заглянем в Rust, и увидим, как там встречают эту задачу.

pub fn encrypting(
    plaintext: Vec<u8>,
    key: Vec<u8>,
    encrypt_mode: &str,
) -> Result<Vec<u8>, CipherError> {
    let encryptor = get_encryptor(&key, encrypt_mode)?;
    encryptor.encrypt(&plaintext)
}

Если быть внимательным, то заметим, что аргументы функции аналогичные тем, что были в нашей Python-функции encrypt. Это вовсе не обязательное условие, но в данном случае это совпадает. Только Rust иначе их воспринимает как тип данных, ну и по-другому обрабатывает.

encryptor.encrypt(&plaintext)

Вот эта же строка, собственно, и скрывает магию. Она вызывает алгоритм шифрования и получив результат возвращает его как результат работы функции encrypting (одна из особенностей Rust, что оператор return там принято упускать). Далее через «обёртку» они попадают в функцию encrypt, которую мы разбирали на стороне Python.

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

Попробуем запустить программу в командной строке.

>>> from grass_crypt.interfaces import encrypt, decrypt
>>> text = 'Здесь должен быть ваш незашифрованный текст'
>>> code = 'password12345
>>> enc = encrypt(text, code=code)
>>> enc
b'U1RSRUNCIbVTvD3xhoFPTFWAhGLTAeMYR3rJqvICkgJo4eAAsz3agtNh43q5wl2hM056VTmmJzo63W57Y2Ed14Kn7iIQBPZtgdwGhj7/HAlvcHgrGzw7aOKLwkZ3iL9H83W/C6tMBIpcvzKZaKWR4ZxyoOkfyg=='
>>> decrypt(enc, code=code)
'Здесь должен быть ваш незашифрованный текст'
smiles_cat
← Назад ко всем записям