Предисловие

Это не документация или гайд, я просто делюсь своим опытом и продвигаю свои проекты с открытым исходным кодом.

Проект некоммерческий

Начало начал

В 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
Импорт из csv

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

Экспорт в 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 и заходите на мой сайт, если конечно этого хотите.

Если есть что-то что вам интересно, но я про это не рассказал, то задавайте вопросы.