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

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

Задержим дыхание и продолжим изучение. Вот мы уже видим, что ядро программы это всего несколько модулей (файлов). В бэкенде и системном программировании, к которому относится 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)
Любой из нас, кто впервые сталкивается с программированием, а разработчики с чужим кодом, может испытывать эмоции отторжения и уныния.

Начнём двигаться построчно. Станет проще, а со временем понятнее. Это всегда так. Во-первых, если код документирован, то часть информации содержится в docstrings, это текст заключенный в кавычки под названием функции. Как правило, там уже есть часть важной информации: функционал, принимаемые на вход аргументы и какой возвращается результат.
Некоторые IDE (от английского integrated development environment, интегрированная среда разработки), дополнительно обрабатывают эти блоки и выводят их содержание при наведении курсора на изучаемую функцию. Например, вот так выглядит наша 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)
'Здесь должен быть ваш незашифрованный текст'
