Привет, Хабр! Это моя первая статья, так что ладошки потеют. Итак, расскажу про опыт работы с программно-аппаратным модулем Рутокен, Python и многим другим. В первой части расскажу, как реализовывал подпись с помощью обычной флешки, как всё это получилось соединить и зачем я это делал.

Дали задание по программированию — сделать программу, с помощью которой можно будет подписывать ЭЦП и проверять её.
Первое, что пришло в голову, это использование обычной флешки. Однако это было бы слишком просто, и я решил добавить ещё шифрования и расшифрования файлов выбранной директории в зависимости от наличия флешки (чтобы иметь власть над папкой «Анапа 2007»).
Предстоит сделать:
Модуль для проверки флешки в компьютере;
Создание ключей и их запись (SHA-256);
Шифрования и расшифрования директории(AES);
Подписание и проверка подписи(RSA);
Шедевральный GUI.
Начал я, конечно, с выбора библиотек, а именно:
Cryptography-для шифрования и подписи;
Threading-для приоритизации потоков проверки наличия флешки в пк;
Tkinter-для реализации удобнейшего GUI;
Base64-для кодирования подписи;
Psutil-обнаружение USB-устройств;
Другие по мелочи(os, sys, time).
Первое, что написал, это обнаружение флешки и считывания инфы с неё, функция find_usb_key для обнаружения ключа шифрования на флешке, get_usb_drives для обнаружения флешки (также работает на Linux):
def find_usb_key(self):
usb_drives = self.get_usb_drives()
for drive in usb_drives:
key_file = os.path.join(drive, "secret.key")
if os.path.exists(key_file):
return key_file
return None
def get_usb_drives(self):
usb_drives = []
if sys.platform == 'win32':
drives = psutil.disk_partitions()
for drive in drives:
if 'removable' in drive.opts:
usb_drives.append(drive.mountpoint)
else:
drives = ["/media/" + d for d in os.listdir("/media/") if os.path.ismount("/media/" + d)]
usb_drives.extend(drives)
if not usb_drives and sys.platform == 'win32':
for letter in "EFGHIJKLMNOPQRSTUVWXYZ":
if os.path.exists(letter + ":\\"):
usb_drives.append(letter + ":\\")
return usb_drives
Проверка наличия флешки происходит через функцию check_usb_status и вызовом функции для шифрования и расшифрования соответственно:
def check_usb_status(self):
current_status = os.path.exists(self.key_path) if self.key_path else False
if current_status != self.usb_inserted:
self.usb_inserted = current_status
if current_status:
self.usb_status.config(text=f"Флешка с ключом: ОБНАРУЖЕНА ({self.key_path})", fg='green')
self.log("Флешка с ключом подключена")
self.process_directory("decrypt")
else:
self.usb_status.config(text="Флешка с ключом: НЕ ОБНАРУЖЕНА", fg='red')
self.log("Флешка с ключом извлечена!")
self.process_directory("encrypt")
return current_status
База написана, следующим шагом был функционал, а именно запись/считывание ключей шифрования, через функцию load_or_generate_keys, а через check_keys_available проверка ключей для ЭЦП:
def load_or_generate_keys(self):
usb_drives = self.get_usb_drives()
key_found = False
for drive in usb_drives:
private_path = os.path.join(drive, "private_key.pem")
public_path = os.path.join(drive, "public_key.pem")
if os.path.exists(private_path) and os.path.exists(public_path):
try:
with open(private_path, "rb") as f:
self.private_key = serialization.load_pem_private_key(
f.read(),
password=None
)
with open(public_path, "rb") as f:
self.public_key = serialization.load_pem_public_key(
f.read()
)
self.private_key_path = private_path
self.public_key_path = public_path
key_found = True
self.log("Ключи успешно загружены с флешки")
break
except Exception as e:
self.log(f"Ошибка загрузки ключей: {str(e)}")
if not key_found and usb_drives:
drive = usb_drives[0]
self.private_key_path = os.path.join(drive, "private_key.pem")
self.public_key_path = os.path.join(drive, "public_key.pem")
try:
self.private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048
)
self.public_key = self.private_key.public_key()
with open(self.private_key_path, "wb") as f:
f.write(self.private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()
))
with open(self.public_key_path, "wb") as f:
f.write(self.public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
))
self.log("Новые ключи сгенерированы и сохранены на флешку")
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось сохранить ключи на флешку: {str(e)}")
self.private_key = None
self.public_key = None
def check_keys_available(self):
if not self.private_key or not self.public_key:
usb_drives = self.get_usb_drives()
for drive in usb_drives:
private_path = os.path.join(drive, "private_key.pem")
if os.path.exists(private_path):
self.load_or_generate_keys()
return True
return False
return True
Шифрование файлов encrypt_file и расшифрование decrypt_file реализуются несложно, и важно после шифрования удалять изначальные файлы:
def encrypt_file(self, file_path):
if file_path.endswith(self.encrypted_ext):
return
try:
with open(file_path, "rb") as f:
data = f.read()
encrypted = self.fernet.encrypt(data)
encrypted_path = file_path + self.encrypted_ext
with open(encrypted_path, "wb") as f:
f.write(encrypted)
os.remove(file_path)
self.log(f"Шифрование прошло успешно: {os.path.basename(file_path)}")
return True
except Exception as e:
self.log(f"Ошибка при шифровании {os.path.basename(file_path)}: {str(e)}")
return False
def decrypt_file(self, file_path):
if not file_path.endswith(self.encrypted_ext):
return False
try:
with open(file_path, "rb") as f:
encrypted = f.read()
decrypted = self.fernet.decrypt(encrypted)
original_path = file_path[:-len(self.encrypted_ext)]
with open(original_path, "wb") as f:
f.write(decrypted)
os.remove(file_path)
self.log(f"Дешифрование прошло успешно: {os.path.basename(original_path)}")
return True
except Exception as e:
self.log(f"Ошибка при дешифровании {os.path.basename(file_path)}: {str(e)}")
return False
И наконец, последними функцией стала именно подпись sign_file и проверка подписи verify_signature выбранного файла, она, конечно, выполнена довольно просто, но по-другому не уверен, что смогу реализовать:
def sign_file(self, file_path):
if not self.check_keys_available():
messagebox.showerror("Ошибка", "Не найден ключ для подписи на подключенных флешках!")
return False
try:
with open(file_path, "rb") as f:
data = f.read()
signature = self.private_key.sign(
data,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
signature_path = file_path + self.signed_ext
with open(signature_path, "wb") as f:
f.write(base64.b64encode(signature))
self.log(f"Файл подписан: {os.path.basename(file_path)}")
return True
except Exception as e:
self.log(f"Ошибка при подписании файла: {str(e)}")
return False
def verify_signature(self, file_path, signature_path):
if not self.check_keys_available():
messagebox.showerror("Ошибка", "Не найден ключ для проверки на подключенных флешках!")
return False
try:
with open(file_path, "rb") as f:
data = f.read()
with open(signature_path, "rb") as f:
signature = base64.b64decode(f.read())
self.public_key.verify(
signature,
data,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
self.log(f"Подпись верна: {os.path.basename(file_path)}")
return True
except Exception as e:
self.log(f"Ошибка проверки подписи: {str(e)}")
return False
С технической части всё, в итоге имеется программа, которая при запуске создаёт на флешке закрытый и открытый ключ для подписания файлов с помощью ЭЦП и непосредственная их проверка, автоматическое шифрования и расшифрования файлов в директории благодаря наличию или отсутствию флешки с ключом. И напоследок, конечно же, приятный GUI (все главные заголовки намекают на серьезность намерений).
Однако после создания этой программы мне не давал покоя тот факт, что это всё делается через обычную флешку, небезопасно как-то (хотя проблем с безопасностью в этой программе достаточно). Я вспомнил про программно-аппаратные модули, а именно USB-токены. Ну и решил всё снести и реализовать это всё на ЭЦП 3.0 Рутокен. Полный код представлен на моём GitHub.
Продолжение в части 2.
Комментарии (7)
Yashka37
26.05.2025 04:36Довольно информатитная статейка, и интересная реализация подписи и шифрования. Будет интересно почитать во второй части интеграцию функционала рутокена с "Мама не узнает" чтобы под рукой была портативная простая программка для таких ЭЦПшек
Pondesss
26.05.2025 04:36Лет 15 назад это все было как то проще, но подробностей я уже не вспомню. Что то явно связанное с криптопро)))
ISP1973
26.05.2025 04:36Прежде чем писать статьи про криптографию, автору следовало хотя бы поверхностно изучить этот вопрос. Ну например для того, чтобы знать и понимать, что процесс, обратный ЗАшифрованию, называется РАСшифрование, а не ДЕшифрование. И что РАСшифрование и ДЕшифрование -это два совершенно разных процесса, а не одно и тоже, как думает автор. А так же автор бы знал, что уже аж с 2014г термин "ЭЦП" заменён на термин "ЭП". Автор бы ещё назвал ЭП "магическими рунами", а почему нет?
Это всё, что нужно знать об уровне знаний автора, чтобы понимать как относиться и ко всему остальному в статье.
lorz1k Автор
26.05.2025 04:36Спасибо за комментарий, дешифрирование и расшифрования действительно имеют разное значение, эту ошибку я сейчас исправлю, однако, хоть ЭЦП в официальных нормативных документах и не используется, программно-аппаратный модуль Рутокен ЭЦП 3.0, который и был взят мной как последнее улучшение данной программы, использует этот термин.
Также агрессивный тон в котором написано сообщение является неуместным, особенно преувеличение с "магическими рунами", так-как ранее я написал что это моя первая статья и в профиле указано что я стажёр, в следствии этого мои статьи и не претендуют на 100% научную достоверность.
SystemSoft
Могли бы вы сделать свои примеры кода в отдельный блок кода? То есть так:
lorz1k Автор
Спасибо за совет, выглядит и правда лучше