Привет, Хабр! Это моя первая статья, так что ладошки потеют. Итак, расскажу про опыт работы с программно-аппаратным модулем Рутокен, 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)


  1. SystemSoft
    26.05.2025 04:36

    Могли бы вы сделать свои примеры кода в отдельный блок кода? То есть так:

    def my_func(x):
        return x + 2
      
    print(my_func(6))
    
    #output: 8


    1. lorz1k Автор
      26.05.2025 04:36

      Спасибо за совет, выглядит и правда лучше


  1. Yashka37
    26.05.2025 04:36

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


    1. lorz1k Автор
      26.05.2025 04:36

      Спасибо большое, думаю дня через 2 доделаю программу и сразу выложу


  1. Pondesss
    26.05.2025 04:36

    Лет 15 назад это все было как то проще, но подробностей я уже не вспомню. Что то явно связанное с криптопро)))


  1. ISP1973
    26.05.2025 04:36

    Прежде чем писать статьи про криптографию, автору следовало хотя бы поверхностно изучить этот вопрос. Ну например для того, чтобы знать и понимать, что процесс, обратный ЗАшифрованию, называется РАСшифрование, а не ДЕшифрование. И что РАСшифрование и ДЕшифрование -это два совершенно разных процесса, а не одно и тоже, как думает автор. А так же автор бы знал, что уже аж с 2014г термин "ЭЦП" заменён на термин "ЭП". Автор бы ещё назвал ЭП "магическими рунами", а почему нет?

    Это всё, что нужно знать об уровне знаний автора, чтобы понимать как относиться и ко всему остальному в статье.


    1. lorz1k Автор
      26.05.2025 04:36

      Спасибо за комментарий, дешифрирование и расшифрования действительно имеют разное значение, эту ошибку я сейчас исправлю, однако, хоть ЭЦП в официальных нормативных документах и не используется, программно-аппаратный модуль Рутокен ЭЦП 3.0, который и был взят мной как последнее улучшение данной программы, использует этот термин.

      Также агрессивный тон в котором написано сообщение является неуместным, особенно преувеличение с "магическими рунами", так-как ранее я написал что это моя первая статья и в профиле указано что я стажёр, в следствии этого мои статьи и не претендуют на 100% научную достоверность.