Предисловие
Это не документация или гайд, я просто делюсь своим опытом и продвигаю свои проекты с открытым исходным кодом.
Проект некоммерческий
Начало начал
В 2024 году я начал разрабатывать свой мессенджер на QT, но через примерно год я встал в тупик и решил на примере не сложного проекта разобраться в QT и попробовать нормально построить архитектуру, но это не суть.
Технологии
В качестве языка я выбрал Python, так как на нем пишу уже 7 лет и считаю что почти все можно написать на нем, надо только уметь.
В качестве интерфейса был выбран QT через библиотеку pyside6. С помощью QT можно сделать красивый интерфейс и пример этом - телеграм.
Шифрование происходит следующим образом: создается синхронный ключ и шифруется с помощью ключа асинхронного шифрования (RSA)
#Метод генерации синхронного ключа
def gen_sync_key(self):
with open(self.sync_key_path, 'wb') as key:
key.write(get_random_bytes(32))
#Метод генерации асинхронного ключа
def gen_async_key(self):
keys = RSA.generate(1024)
with open(self.async_public_key_path, 'wb') as key_pub:
key_pub.write(keys.public_key().export_key())
with open(self.async_private_key_path, 'wb') as key_private:
key_private.write(keys.export_key(format='PEM', passphrase=self.password, protection='PBKDF2WithHMAC-SHA512AndAES256-CBC', prot_params={'iteration_count':131072}))
#Метод шифрования синхронного ключа с помощью асинхронного шифрования
def synchronous_key_encryption(self):
with open(self.sync_key_path, 'rb') as sync_key:
sync_key = sync_key.read()
with open(self.async_public_key_path, 'rb') as async_key:
async_key = RSA.import_key(async_key.read(), self.password)
encrypt = PKCS1_OAEP.new(async_key)
encrypt = encrypt.encrypt(sync_key)
with open(self.sync_key_path, 'wb') as sync_key:
sync_key.write(encrypt)Все остальное шифрование строится так:
открыть зашифрованный синхронный ключ --> расшифровать приватным ключом RSA --> зашифровать данные.
Так как все данные хранятся локально, в качестве СУБД был использован sqlite3. Все данные для входа пользователя хранятся в виде хэшэй с солью. Записи паролей состоят из несколько полей: имя записи, имя сайта/сервиса, логин, почта, пароль. Шифруются только почта и пароль, остальные данные считаются бесполезными (но если кто-то попросит шифровать все данные, то я могу это сделать).
#Генерация соли и добавление соли к данным пользователя
def sault_func(self, user_name:str, user_password:str, sault:bytes=get_random_bytes(32)):
user_name = bytes(user_name, encoding="utf-8")
user_password = bytes(user_password, encoding="utf-8")
sault_user_name = sault[:len(sault)//2]+user_name+sault[len(sault)//2:]
sault_user_password = sault[:len(sault)//2]+user_password+sault[len(sault)//2:]
return {"sault": sault, "sault_user_name": sault_user_name, "sault_user_password": sault_user_password}
Помимо этого ключи RSA имеют пароль который пользователь задает при входе в систему. Так как пароль у нас в БД хранится в виде хэша, нужно где-то сохранять паролю в чистом виде, поэтому при входе в систему пароль сохраняется в кэшэ на время работы программы (когда-то я сделаю это временным).
class CachePassword:
def __init__(self, password=None, name_service=None, time_del_pwd=0):
self.PASSWORD = password
self.NAME_SERVICE = name_service
self.time_del_pwd = time_del_pwd
self.user_name = socket.gethostname()
#Метод помещения данных в кэш
def set_password(self):
keyring.set_password(self.NAME_SERVICE, self.user_name, self.PASSWORD)
#Метод выдачи данных из кэша
def get_password(self):
return keyring.get_password(self.NAME_SERVICE, self.user_name)
#Метод таймера удаления данных из кэша
def del_timer(self):
timer = threading.Timer(self.time_del_pwd, self.del_password)
timer.daemon = True
timer.start()
#Метод удаления данных из кэша
def del_password(self):
keyring.delete_password(self.NAME_SERVICE, self.user_name)Раздел про технологии - конец (может быть про что-то забыл)
Функционал
Из базовых функций уже понятно что: шифрование, реализация регистрации и входа в систему, удобное расположение записей

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

#Класс генирации паролей
class GenerationPassword:
def __init__(self, len_password:int):
self.len_password = len_password
self.chars_number = ''.join(string.printable.split())
#Метод валидации пароля
def validation(self, user_password):
if not bool(re.search("[a-z]", user_password)):
return False
if not bool(re.search("[A-Z]", user_password)):
return False
if not bool(re.search("[0-9]", user_password)):
return False
if not bool(re.search("["+ re.escape(string.punctuation)+ "]", user_password)):
return False
return True
#Метод генирации пароля
def generation_password(self):
while True:
password = ''.join([secrets.choice(self.chars_number) for _ in range(self.len_password)])
if self.validation(user_password=password):
return passwordПод стрелкой направленной вниз - импорт паролей из csv файла. Можно импортировать пароли из браузера

Соответственно стрелка вверх - экспорт в csv. Можно использовать, если хотите перенести все в браузер или хотите перенести пароли на новую систему.

Также есть поиск по ключевым словам и функция удаления записи.
Есть также функция ответственная за проверку вводимого пароля при регистрации. Она легкая, но от нее больше и не требуется. Была мысль освободить пользователя от обязательности ввода "безопасного" пароля, но было решено добавить. Если кто-то решит что это лишнее для него, может смело вырезать.
#Проверка валидации данных
def validation(self):
if not self.user_data or self.user_name in self.user_password:
return False
if not len(self.user_password) >= 8:
return False
if bool(re.search("[ ]", self.user_password)):
return False
if bool(re.search("[а-яА-ёЁ]", self.user_data)):
return False
if not bool(re.search("[a-z]", self.user_password)):
return False
if not bool(re.search("[A-Z]", self.user_password)):
return False
if not bool(re.search("[0-9]", self.user_password)):
return False
if not bool(re.search("[!-_#@%$*?/|&)}{<>+='(,)^:;№%*.]", self.user_password)):
return False
return TrueТакже есть функция удаления записи)
Сложности с которыми столкнулся
Большинство сложностей были касаемо работы с интерфейсом, но это все мелочи которые никак не влияют на функционал.
Самая большая проблема была - лагает интерфейс, если слишком много записей. Выглядит сложно, но решилась просто. Проблема была в том что каждый раз когда рендерился интерфес (каждое прокручивание или расширение окна), все данные расшифровывались заново. Решил я это тем, что просто сохранял таблицу в переменную на время работы программы.
Заключение
Все это начиналось как обкадка интерфейса для переписывания мессенджера на новой архитектуре и с уверенной работой с QT, но с этого проекта у меня появилась идея написания ПО которое будет безопасным и открытым.
В планах еще много что написать, так как многие сервисы в россии нормально не работают, а их альтернативами пользоваться никто не хочет. Очень буду рад поддержке. Пока деньги не собираю, поэтому просто подписывайтесь на тгк, github и заходите на мой сайт, если конечно этого хотите.
Если есть что-то что вам интересно, но я про это не рассказал, то задавайте вопросы.