Статья Шифрование на Python. Часть 1

Доброго времени суток друзья !

main.jpg


Программа для защиты информации на языке питон на основе Tkinter GUI и библиотеки pyAesCrypt. Я не знаю для чего изобретать велосипед, но можно надежно шифровать свои данные (не ТераБайтные :D например) своими силами. Скорость шифрования устраивает но проблема скорости есть, над этим не работал пока. На директории до 5 - 10 ГБ уйдет от 3 до 7 минут в зависимости от системы (но это не точно). Но тут вопрос видимо в алгоритме шифрования и мультипроцессинге имхо. В общем, всегда есть над чем работать)

На выходе должен быть работающий код (при наличии поставленных библиотек), который можно доработать или изменить например алгоритм шифрования. В общем используется документация, питон и иногда гуглнет (куда без него). Если будет кому то интересно или полезно, буду рад получить обратную связь. По информативности и написанию тоже, хотя в основном будет оптимизировано документационный код)


Часть замечаний будет непосредственно в комментариях внутри кода, постарался сделать подробно (надеюсь не слишком)
Тут оставлю ссылку на
вторую часть.

Короче, погнали.



Подключаем библиотеки :

Python:
#!/usr/bin/env python
import os
import threading

import pyperclip
import pyAesCrypt
from tkinter import *
import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext
Проверяем, устанавливаем через pip.


Создаем главное окно :
Далее создаем класс приложения и вызываем метод инит, инициализируем нужные переменные и виджеты. Виджеты расположены по осям х, у для геометрии окна 500х500 (вроде оптимально))

Python:
# инициализацию можно делать по разному, тут наследуемся от Frame.tk
class MainWindow(tk.Frame):
    def __init__(self, root):
        super().__init__(root)
        self.init_main()
    # создаем нужные переменные и виджеты главного окна
    def init_main(self):
        self.img_cache = [] # храним изображения
        self.thread_count = 0 # счетчик потоков
        self.path_error = False # отловить ошибку
        # тут стандартные виджеты tkinter
        lb_dir = Label(text=u"Директория шифрования : ")
        lb_dir.pack()
        self.ent_value_dir = Entry(width=40)
        self.ent_value_dir.pack()
        lb_pass = Label(text=u"Пароль : ")
        lb_pass.pack()
        ent_value_pass = Entry(width=40)
        ent_value_pass.pack()
        #base_img = "iVBORw0KGgoAAAAN_youR_very_long_code"
        crypt_img = PhotoImage(file='1.png')
        self.img_cache.append(crypt_img)
        btn_crypt = ttk.Button(text='  CRYPT', image=self.img_cache[0], compound='left', command=lambda: self.crypting(self.ent_value_dir.get(), ent_value_pass.get()))
        btn_crypt.place(x=5, y=20)
        decrypt_img = PhotoImage(file='2.png')
        self.img_cache.append(decrypt_img)
        btn_decrypt = ttk.Button(text='  DECRYPT', image=self.img_cache[1], compound='left', command=lambda: self.decrypting(self.ent_value_dir.get(), ent_value_pass.get()))
        btn_decrypt.place(x=5, y=60)
        paste_img = PhotoImage(file='3.png')
        self.img_cache.append(paste_img)
        btn_stop = ttk.Button(text='  PASTE', image=self.img_cache[2], compound='left', command=lambda: self.paste_dir_to_entry())
        btn_stop.place(x=390, y=20)
        stop_img = PhotoImage(file='4.png')
        self.img_cache.append(stop_img)
        btn_stop = ttk.Button(text='  STOP', image=self.img_cache[3], compound='left', command=lambda: self.close_crypter())
        btn_stop.place(x=390, y=60)
        self.console = scrolledtext.ScrolledText(fg="red", bg="black", state='disable')
        self.console.pack(pady=20)
Для кнопок лучше вставлять изображения - закодированные в base64, или придется держать файлы изображений в рабочей директории, если заходите скомпилировать в один exe. файл - это неудобно.


Немного криптографии :
Немного отступлю от кода в сторону криптографии :

Основные функции расшифровка / дешифровка работают принимая на вход директорию и пароль и используя модуль pyAesCrypt. В документации указан алгоритм шифрования AES256-CBC, СВС (Цепочка блоков шифров) - это режим шифрования, поменять его нельзя, но этот режим является предпочтительным для современных алгоритмов (насколько мне известно из источников) Сам алгоритм шифрования здесь затрагивать не буду, но хочется заметить, что тут используется входной ключ длиной 256 бит и я нашел хорошую цитату из книги «Прикладная криптография» Брюса Шнайера ( она достаточно длинная, но точно достойна к прочтению :)) о сложности дешифровки 256 битного ключа в специфике нашей физики материи и энергии)
Одним из следствий закона второго термодинамики является то, что для представления информации необходимо некоторое количество энергии. Запись одиночного бита, изменяющая состояние системы, требует количества энергии не меньше чем kT; где Т — абсолютная температура системы и k — постоянная Больцмана. (Не волнуйтесь, урок физики уже почти закончен.)

Приняв, что k = 1,38*10-16 эрг/K, и что температура окружающей вселенной 3,2K, идеальный компьютер, работая при 3,2K, потреблял бы 4,4*10-16 эрга всякий раз, когда он устанавливает или сбрасывает бит. Работа компьютера при температуре более низкой, чем температура космического пространства, потребовала бы дополнительных расходов энергии для отвода тепла.

Далее, энергия, излучаемая нашим Солнцем за год, составляет около 1,21*1041 эргов. Это достаточно для выполнения 2*1056 перемен бита в нашем идеальном компьютере, а этого, в свою очередь, хватит для того, что бы 187-битовый счетчик пробежал все свои значения. Если мы построим вокруг Солнца сферу Дайсона и перехватим без всяких потерь всю его энергию за 32 года, мы сможем получить компьютер для вычисления 2192 чисел. Конечно, энергии для проведения каких-нибудь полезных вычислений с этим счетчиком уже не останется.

Но это только одна жалкая звезда. При взрыве типичной сверхновой выделяется около 1051 эргов. (В сто раз больше энергии выделяется в виде нейтрино, но пусть они пока летают). Если всю эту энергию удастся бросить на одну вычислительную оргию, то все свои значения сможет принять 219-битовый счетчик.

Эти числа не имеют ничего общего с самой аппаратурой, они просто показывают максимальные значения, обусловленные термодинамикой. Кроме того, эти числа наглядно демонстрируют, что вскрытие грубой силой 256-битового ключа будет невозможно, пока компьютеры построены из обычной материи и располагаются в обычном пространстве.


Основные функции :

Расшифровка / дешифровка директории:

Python:
    def crypt_file(self, file, password):
        bufferSize = 512 * 1024 # размер буфера по умолчанию - больше не надо
        try:
            # шифруем с расширением .your_version&
            pyAesCrypt.encryptFile(str(file), str(file) + ".your_version&",
                                   password, bufferSize)
            # выводим в консоль tkinter
            self.insert_to_console('ENCRYPTED >>> ' + str(file) + ".your_version&" + '\n')
            os.remove(file) # удаляем файл
        except Exception as e:
            self.insert_to_console('Ошибка шифрования <Unknown_Error>')
            pass

    def crypt_disk(self, dir, password):
        try:
            for file in os.listdir(dir):
                if os.path.isdir(dir + '\\' + file): # если это директория - продолжаем рекурсивно
                    self.crypt_disk(dir + '\\' + file, password)
                if os.path.isfile(dir + '\\' + file): # если файл - шифруем
                    try:
                        self.crypt_file(dir + '\\' + file, password)
                    except Exception as ex:
                        self.insert_to_console(ex)
                        pass
        except OSError:
            self.path_error = True # поймали ошибку
            return

    def decrypt_file(self, file, password): # расшифровка практически идентична, только меняем метод на decryptFile
        bufferSize = 512 * 1024
        try:
            pyAesCrypt.decryptFile(str(file), str(os.path.splitext(file)[0]),
                                   password, bufferSize)
            self.insert_to_console('DECRYPTED >>> ' + str(os.path.splitext(file)[0]) + '\n')
            os.remove(file)
        except Exception as e:
            print('Ошибка расшифровки, файлы не зашифрованы,'
                  'неверный пароль или файл поврежден')
            self.insert_to_console(e)
            pass

    def decrypt_disk(self, dir, password):
        try:
            for file in os.listdir(dir):
                if os.path.isdir(dir + '\\' + file):
                    self.decrypt_disk(dir + '\\' + file, password)
                if os.path.isfile(dir + '\\' + file):
                    try:
                        self.decrypt_file(dir + '\\' + file, password)
                    except Exception as ex:
                        self.insert_to_console(ex)
                        pass
        except OSError:
            self.path_error = True
            pass

Тут используется функция self.insert_to_console для вывода в tk виджет scrolledtext.

Напишем ее :
Python:
    def insert_to_console(self, text):
        self.console.configure(state='normal')  # включаем инсерт
        self.console.insert(END, text) # вставить текст
        self.console.yview(END)  # автоскролл
        self.console.configure(state='disabled')

Основные криптографические функции есть, но нам необходимо использовать потоки, чтобы окно не зависало. Во время работы криптографические функции нам надо получать динамический вывод в консоль программы, но без использования многопоточности, поток главного окна проги будет перекрывать поток выполнения, окно попросту будет висеть пока программа не закончит работу. Чтобы при повторном нажатии кнопки у нас потоки не накладывались друг на друга, разрешим запускаться только одному потоку.

Реализовал так:
Python:
    def crypting(self, dir, password):
        # проверяем нет ли ошибки пути или пустой пароль
        if self.path_error or password == '':
            self.path_error = False # сбрасываем ошибку
            self.thread_count = 0 # обнуляем счетчик процессов
            self.insert_to_console('Ошибка : Неправильный путь или нет пароля !\n')
            return
        else:
            self.thread_count += 1
            if self.thread_count > 1:
                # print(threading.enumerate())
                self.insert_to_console('Ограничение потока, запущено : 1\n')
                return
        pycrypt = threading.Thread(target=self.crypt_disk, args=(dir, password))
        pycrypt.start()

    def decrypting(self, dir, password):
        if self.path_error or password == '':
            self.path_error = False
            self.thread_count = 0
            self.insert_to_console('Ошибка : Неправильный путь или нет пароля !\n')
            return
        else:
            self.thread_count += 1
            if self.thread_count > 1:
                self.insert_to_console('Ограничение потока, запущено : 1\n')
                return
        pycrypt = threading.Thread(target=self.decrypt_disk, args=(dir, password))
        pycrypt.start()
Эти функции запускаются по нажатии кнопок CRYPT / DECRYPT соответственно.

Напишем функции для кнопки PASTE и STOP:
Python:
    def close_crypter(self): # закрыть окно
        quit()

    def paste_dir_to_entry(self): # вставить текст из буфера обмена в поле директория
        self.ent_value_dir.insert(tk.END, pyperclip.paste())

И наконец инициализируем запуск:
Python:
    def run_app():
        root = tk.Tk()
        root.resizable(width=False, height=False) # не изменять размер окна
        MainWindow(root)
        root.title("crypter")
        root.geometry("500x500")
        root.mainloop()


if __name__ == '__main__':
    crypter = MainWindow
    crypter.run_app()

1632415455326.png


Вот так это выглядит.

Чтобы скомпилировать в .exe поменяйте расширение на your_file.pyw и выполните:
pyinstaller --onefile your_file.pyw
или
pyinstaller --onefile --icon=skeleton.ico zvepb_crypter.pyw
для эстетики.

Для компиляции необходима библиотека pyinstaller:
pip install pyinstaller

Файлы иконок для запуска:
1.png
2.png

3.png
4.png



Исходный код:

Python:
#!/usr/bin/env python
import os
import threading

import pyperclip
import pyAesCrypt
from tkinter import *
import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext


class MainWindow(tk.Frame):
    def __init__(self, root):
        super().__init__(root)
        self.init_main()

    def init_main(self):
        self.img_cache = []
        self.thread_count = 0
        self.path_error = False
        lb_dir = Label(text=u"Директория шифрования : ")
        lb_dir.pack()
        self.ent_value_dir = Entry(width=40)
        self.ent_value_dir.pack()
        lb_pass = Label(text=u"Пароль : ")
        lb_pass.pack()
        ent_value_pass = Entry(width=40)
        ent_value_pass.pack()
        crypt_img = PhotoImage(file='1.png')
        self.img_cache.append(crypt_img)
        btn_crypt = ttk.Button(text='  CRYPT', image=self.img_cache[0], compound='left', command=lambda: self.crypting(self.ent_value_dir.get(), ent_value_pass.get()))
        btn_crypt.place(x=5, y=20)
        decrypt_img = PhotoImage(file='2.png')
        self.img_cache.append(decrypt_img)
        btn_decrypt = ttk.Button(text='  DECRYPT', image=self.img_cache[1], compound='left', command=lambda: self.decrypting(self.ent_value_dir.get(), ent_value_pass.get()))
        btn_decrypt.place(x=5, y=60)
        paste_img = PhotoImage(file='3.png')
        self.img_cache.append(paste_img)
        btn_stop = ttk.Button(text='  PASTE', image=self.img_cache[2], compound='left', command=lambda: self.paste_dir_to_entry())
        btn_stop.place(x=390, y=20)
        stop_img = PhotoImage(file='4.png')
        self.img_cache.append(stop_img)
        btn_stop = ttk.Button(text='  STOP', image=self.img_cache[3], compound='left', command=lambda: self.close_crypter())
        btn_stop.place(x=390, y=60)
        self.console = scrolledtext.ScrolledText(fg="red", bg="black", state='disable')
        self.console.pack(pady=20)

    def close_crypter(self):
        quit()

    def insert_to_console(self, text):
        self.console.configure(state='normal')  # enable insert
        self.console.insert(END, text)
        self.console.yview(END)  # autoscroll
        self.console.configure(state='disabled')

    def paste_dir_to_entry(self):
        self.ent_value_dir.insert(tk.END, pyperclip.paste())

    def crypt_file(self, file, password):
        bufferSize = 512 * 1024
        try:
            pyAesCrypt.encryptFile(str(file), str(file) + ".zvp",
                                   password, bufferSize)
            self.insert_to_console('ENCRYPTED >>> ' + str(file) + ".zvp" + '\n')
            os.remove(file)
        except Exception as e:
            self.insert_to_console('Ошибка шифрования <Unknown_Error>')
            pass

    def crypt_disk(self, dir, password):
        try:
            for file in os.listdir(dir):
                if os.path.isdir(dir + '\\' + file):
                    self.crypt_disk(dir + '\\' + file, password)
                if os.path.isfile(dir + '\\' + file):
                    try:
                        self.crypt_file(dir + '\\' + file, password)
                    except Exception as ex:
                        self.insert_to_console(ex)
                        pass
        except OSError:
            self.path_error = True
            return

    def decrypt_file(self, file, password):
        bufferSize = 512 * 1024
        try:
            pyAesCrypt.decryptFile(str(file), str(os.path.splitext(file)[0]),
                                   password, bufferSize)
            self.insert_to_console('DECRYPTED >>> ' + str(os.path.splitext(file)[0]) + '\n')
            os.remove(file)
        except Exception as e:
            print('Ошибка расшифровки, файлы не зашифрованы,'
                  'неверный пароль или файл поврежден')
            self.insert_to_console(e)
            pass

    def decrypt_disk(self, dir, password):
        try:
            for file in os.listdir(dir):
                if os.path.isdir(dir + '\\' + file):
                    self.decrypt_disk(dir + '\\' + file, password)
                if os.path.isfile(dir + '\\' + file):
                    try:
                        self.decrypt_file(dir + '\\' + file, password)
                    except Exception as ex:
                        self.insert_to_console(ex)
                        pass
        except OSError:
            self.path_error = True
            pass
 
# проблемы - ускорение процесса шифрования и создающиеся потоки при повторном нажатии кнопки
    # (исправлено с помощью path_error и thread_count
    def crypting(self, dir, password):
        if self.path_error or password == '':
            self.path_error = False
            self.thread_count = 0
            self.insert_to_console('Ошибка : Неправильный путь или нет пароля !\n')
            return
        else:
            self.thread_count += 1
            if self.thread_count > 1:
                # print(threading.enumerate())
                self.insert_to_console('Ограничение потока, запущено : 1\n')
                return
        pycrypt = threading.Thread(target=self.crypt_disk, args=(dir, password))
        pycrypt.start()

    def decrypting(self, dir, password):
        if self.path_error or password == '':
            self.path_error = False
            self.thread_count = 0
            self.insert_to_console('Ошибка : Неправильный путь или нет пароля !\n')
            return
        else:
            self.thread_count += 1
            if self.thread_count > 1:
                self.insert_to_console('Ограничение потока, запущено : 1\n')
                return
        pycrypt = threading.Thread(target=self.decrypt_disk, args=(dir, password))
        pycrypt.start()

    def run_app():
        root = tk.Tk()
        root.resizable(width=False, height=False)
        MainWindow(root)
        root.title("zvepb_crypter")
        root.geometry("500x500")
        root.mainloop()


if __name__ == '__main__':
    zv = MainWindow
    zv.run_app()


Может будет полезно / интересно тем кто начал или изучает питон, ну или для общего развития ^_^. Если есть какие то замечания - выслушаю конструктивную критику, помощь / недочеты по коду. Тут оставлю ссылку на вторую часть.

Спасибо за внимание)
 
Последнее редактирование модератором:

TR1X

Green Team
04.04.2020
220
156
BIT
134
А зачем тебе импортивать много раз, когда можно за раз написать:
from tkinter import *

А так статья топ, однозначно лайкосик!
Да, действительно есть лишний импорт. Дело в том, что from tkinter import * не импортирует ttk и scrolledtext, поэтому они импортятся отдельно. А из tkinter у меня лишний раз импортируется import tkinter as tk и потом я наследуюсь от tk.Frame.

Можно оставить только
Python:
from tkinter import *
from tkinter import ttk
from tkinter import scrolledtext
и наследоваться от Frame и поменять root = Tk().

Спасибо за замечание)

Почему threading, а не multiprocessing? Тут, как-бы, лучше все ядра использовать.
Изначально задача стояла - создать многопоточность и получать динамический вывод ( кстати мультипроцессинг тоже использовал). Но далее я просто оставил без внимания этот факт и тут согласен что тут надо юзать multiprocessing.

Спасибо за замечание.
 

Muxtar

Green Team
02.06.2021
132
65
BIT
0
Да, действительно есть лишний импорт. Дело в том, что from tkinter import * не импортирует ttk и scrolledtext, поэтому они импортятся отдельно. А из tkinter у меня лишний раз импортируется import tkinter as tk и потом я наследуюсь от tk.Frame.

Можно оставить только
Python:
from tkinter import *
from tkinter import ttk
from tkinter import scrolledtext
и наследоваться от Frame и поменять root = Tk().

Спасибо за замечание)
А ясно. Продолжай том же духе, твои статьи просто идеальны
 

TR1X

Green Team
04.04.2020
220
156
BIT
134
Пора внутренний crypto CTF/конкурс на кодбае устраивать, чей криптор быстрее, меньше итд :)
Среди пайтона то да) Но с С-ями змейке не тягаться)

А так, планирую небольшую доработку во второй части.
 
  • Нравится
Реакции: mrYODA

ooolledj

One Level
26.03.2021
6
1
BIT
0
Счетчик потоков обнуляется только если попытаться cделать CRYPT / DECRYPT с пустым полем пароля или если сработало исключение OSError в функциях decrypt_disk / crypt_disk.

Если пробовать поработать с другими файлами или директориями или запустить CRYPT / DECRYPT этой же директории - будет ошибка, что уже есть запущенный поток.

Решается путем добавление блока finally в функциях decrypt_disk / crypt_disk:
Python:
...
except OSError:
    self.path_error = True
    return
finally:
    self.thread_count = 0
...

А для функций crypting / decrypting нужно изменить блок условий:
Python:
#Аналогично для функции crypting
def decrypting(self, dir, password):
    if self.path_error or password == '':
        # обнуление потоков убираем отсюда
        self.path_error = False  # сбрасываем ошибку
        self.insert_to_console('ERROR : wrong path or no password!\n')
        return
    # проверка наличия потока
    if self.thread_count > 1:
        self.insert_to_console('Thread limit, running : > 1\n')
        return
    else:
        pycrypt = threading.Thread(target=self.decrypt_disk, args=(dir, password))
        pycrypt.start()
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!