Python: Criptografia e Compressão de Arquivos via Fluxo de Bytes

Este guia explora como implementar criptografia e compressão de arquivos em Python utilizando fluxos de bytes. Abordaremos a criação de um utilitário para proteger e compactar seus dados de forma eficiente.

Estrutura do Projeto e Dependências

Para este projeto, necessitaremos de algumas bibliotecas Python. A principal é a pycryptodome para as operações de criptografia AES.

Nota sobre tkinterdnd2: Se estiver utilizando tkinterdnd2 e precisar empacotar sua aplicação (por exemplo, com PyInstaller), é crucial incluir o arquivo hook-tkinterdnd2.py no diretório raiz do projeto. Ao executar o PyInstaller, utilize a flag --additional-hooks-dir=..


# hook-tkinterdnd2.py
"""PyInstaller hook file.

You need to use this hook-file if you are packaging a project using tkinterdnd2.
Just put hook-tkinterdnd2.py in the same directory where you call pyinstaller and type:

   pyinstaller myproject/myproject.py --additional-hooks-dir=.
"""

from PyInstaller.utils.hooks import collect_data_files, eval_statement

datas = collect_data_files('tkinterdnd2')
   

Implementação da Criptografia e Compressão (Versão 1.0)

Esta versão foca em criptografar e comprimir um único arquivo selecionado em um único arquivo ZIP.

Classe FileAES (Versão 1.0)

A classe FileAES gerencia a interface gráfica e as operações de arquivo.


import os
import base64
import zipfile
import time
import tkinter.filedialog
from tkinter import Tk, Entry, Text, Button, Label, StringVar
from ttkbootstrap import Style
from Crypto.Cipher import AES
from binascii import b2a_hex, a2b_hex

class FileAES_v1:

   def __init__(self, encryption_key):
       self.key = encryption_key.encode('utf-8')
       self.mode = AES.MODE_ECB
       self.encoding = 'utf-8'
       self.compressed_extension = '.lambert'
       self.setup_ui()

   def _pad_data(self, data_bytes):
       block_size = AES.block_size
       padding_needed = block_size - (len(data_bytes) % block_size)
       if padding_needed == block_size:
           return data_bytes # Already a multiple of block size
       padding = bytes([padding_needed] * padding_needed)
       return data_bytes + padding

   def _unpad_data(self, data_bytes):
       padding_len = data_bytes[-1]
       return data_bytes[:-padding_len]

   def encrypt(self, plain_text):
       cipher = AES.new(self.key, self.mode)
       padded_data = self._pad_data(plain_text.encode(self.encoding))
       encrypted_data = cipher.encrypt(padded_data)
       return b2a_hex(encrypted_data).decode(self.encoding)

   def decrypt(self, encrypted_hex):
       cipher = AES.new(self.key, self.mode)
       encrypted_bytes = a2b_hex(encrypted_hex.encode(self.encoding))
       decrypted_padded_data = cipher.decrypt(encrypted_bytes)
       return self._unpad_data(decrypted_padded_data).decode(self.encoding)

   def create_zip(self, zip_filename, files_to_zip):
       with zipfile.ZipFile(zip_filename, 'w', zipfile.ZIP_DEFLATED) as zipf:
           for file_path in files_to_zip:
               if os.path.isfile(file_path):
                   arcname = os.path.basename(file_path)
                   zipf.write(file_path, arcname)
               elif os.path.isdir(file_path):
                   for root, _, filenames in os.walk(file_path):
                       for filename in filenames:
                           file_full_path = os.path.join(root, filename)
                           arcname = os.path.relpath(file_full_path, os.path.commonpath(files_to_zip))
                           zipf.write(file_full_path, arcname)


   def extract_zip(self, zip_filename, extract_path="."):
       with zipfile.ZipFile(zip_filename, 'r') as zip_f:
           zip_f.extractall(path=extract_path)

   def setup_ui(self):
       style = Style(theme='pulse')
       win = style.master
       win.title("Ferramenta de Criptografia/Descriptografia 1.0")
       win.resizable(False, False)

       self.selected_files = []
       self.output_zip_name = StringVar()
       self.output_zip_name.set("")

       self.file_display = Text(win, width=40, height=7, state='disabled', wrap='word')
       self.output_name_label = Label(win, text="Nome do Arquivo Compactado:")
       self.output_name_entry = Entry(win, textvariable=self.output_zip_name, width=20)
       self.select_button = Button(win, text="Selecionar Arquivos", command=self.select_files)
       self.process_button = Button(win, text="Processar", command=self.handle_files, bootstyle="INFO-OUTLINE")

       self.file_display.grid(row=0, column=0, columnspan=4, padx=5, pady=5)
       self.output_name_label.grid(row=5, column=0, sticky="w", padx=5, pady=2)
       self.output_name_entry.grid(row=5, column=1, columnspan=2, sticky="ew", padx=5, pady=2)
       self.select_button.grid(row=6, column=0, columnspan=2, padx=5, pady=5)
       self.process_button.grid(row=6, column=2, columnspan=2, padx=5, pady=5)

       win.mainloop()

   def select_files(self):
       file_paths = tkinter.filedialog.askopenfilenames(title="Selecione os arquivos para processar")
       self.selected_files = list(file_paths)
       if self.selected_files:
           self.file_display.config(state='normal')
           self.file_display.delete('1.0', END)
           display_text = "Arquivos Selecionados:\n" + "\n".join(self.selected_files)
           self.file_display.insert(END, display_text)
           self.file_display.config(state='disabled')
       else:
           self.file_display.config(state='normal')
           self.file_display.delete('1.0', END)
           self.file_display.insert(END, "Nenhum arquivo selecionado.")
           self.file_display.config(state='disabled')

   def handle_files(self):
       if not self.selected_files:
           self.file_display.config(state='normal')
           self.file_display.delete('1.0', END)
           self.file_display.insert(END, "Erro: Nenhum arquivo selecionado.")
           self.file_display.config(state='disabled')
           return

       if not self.output_zip_name.get():
           self.output_zip_name.set(time.strftime("%Y%m%d_%H%M%S"))

       zip_filename = self.output_zip_name.get() + ".zip"
       temp_encrypted_files = []

       for file_path in self.selected_files:
           base_name = os.path.basename(file_path)
           file_dir = os.path.dirname(file_path)
           encrypted_file_path = os.path.join(file_dir, base_name + self.compressed_extension)

           with open(file_path, 'rb') as f_in:
               file_content = f_in.read()
               # Codifica o conteúdo binário para base64 antes de criptografar
               base64_content = base64.b64encode(file_content).decode(self.encoding)
               encrypted_content = self.encrypt(base64_content)

           with open(encrypted_file_path, 'w') as f_out:
               f_out.write(encrypted_content)
           temp_encrypted_files.append(encrypted_file_path)

       self.create_zip(zip_filename, temp_encrypted_files)

       for temp_file in temp_encrypted_files:
           os.remove(temp_file)

       self.file_display.config(state='normal')
       self.file_display.delete('1.0', END)
       self.file_display.insert(END, f"Arquivos processados e compactados em: {zip_filename}")
       self.file_display.config(state='disabled')

if __name__ == '__main__':
   # Chave de exemplo - use uma chave forte e mantenha-a segura!
   SECRET_KEY = 'uma_chave_muito_segura_123'
   app = FileAES_v1(SECRET_KEY)
   

Implementação Avançada (Versão 1.1)

Esta versão introduz a capacidade de criptografar/descriptografar múltiplos arquivos, dividir arquivos criptografados em partes e gerenciar a chave de criptografia através da interface.

Classe FileAES (Versão 1.1)

Esta versão expande a funcionalidade para lidar com operações mais complexas.


import os
import base64
import zipfile
import time
import tkinter.filedialog
import re
import random
import math
from tkinter import Tk, Entry, Text, Button, Label, StringVar
from ttkbootstrap import Style
from Crypto.Cipher import AES
from binascii import b2a_hex, a2b_hex

class FileAES_v1_1:

   def __init__(self):
       self.encryption_key_str = '' # Chave de criptografia como string
       self.aes_key = b''          # Chave AES formatada
       self.mode = AES.MODE_ECB
       self.encoding = 'utf-8'
       self.compressed_extension = '.skj'
       self.setup_ui()

   def _pad_data(self, data_bytes):
       block_size = AES.block_size
       padding_needed = block_size - (len(data_bytes) % block_size)
       padding = bytes([padding_needed] * padding_needed)
       return data_bytes + padding

   def _unpad_data(self, data_bytes):
       padding_len = data_bytes[-1]
       # Validação simples para evitar erros com dados malformados
       if padding_len > AES.block_size or padding_len == 0:
           return data_bytes # Não parece ser padding válido ou é dado puro
       return data_bytes[:-padding_len]

   def format_key(self, key_str):
       key_bytes = key_str.encode(self.encoding)
       if len(key_bytes) in [16, 24, 32]:
           self.aes_key = key_bytes
       elif len(key_bytes) < 16:
           self.aes_key = key_bytes.ljust(16, b'\0')
       elif len(key_bytes) > 32:
           self.aes_key = key_bytes[:32]
       else: # Entre 17 e 23, ou 25 e 31
           self.aes_key = key_bytes.ljust(24, b'\0') # Escolhe 24 como padrão intermediário

   def encrypt(self, plain_text):
       cipher = AES.new(self.aes_key, self.mode)
       padded_data = self._pad_data(plain_text.encode(self.encoding))
       encrypted_data = cipher.encrypt(padded_data)
       return b2a_hex(encrypted_data).decode(self.encoding)

   def decrypt(self, encrypted_hex):
       cipher = AES.new(self.aes_key, self.mode)
       encrypted_bytes = a2b_hex(encrypted_hex.encode(self.encoding))
       decrypted_padded_data = cipher.decrypt(encrypted_bytes)
       return self._unpad_data(decrypted_padded_data).decode(self.encoding)

   def create_zip(self, zip_filename, files_to_zip):
       with zipfile.ZipFile(zip_filename, 'w', zipfile.ZIP_DEFLATED) as zipf:
           for file_path in files_to_zip:
               if os.path.isfile(file_path):
                   arcname = os.path.basename(file_path)
                   zipf.write(file_path, arcname)

   def extract_zip(self, zip_filename, extract_path):
        # O caminho completo para o arquivo zip é necessário aqui
       full_zip_path = os.path.join(extract_path, zip_filename)
       target_dir = os.path.join(extract_path, os.path.splitext(zip_filename)[0])
       if not os.path.exists(target_dir):
           os.makedirs(target_dir)
       with zipfile.ZipFile(full_zip_path, 'r') as zip_f:
           zip_f.extractall(path=target_dir)
       return target_dir # Retorna o diretório onde os arquivos foram extraídos

   def setup_ui(self):
       style = Style(theme='vapor')
       win = style.master
       win.title("Ferramenta de Criptografia/Descriptografia 1.1")
       win.geometry("450x300")
       win.resizable(False, False)

       self.selected_files = []
       self.output_zip_name = StringVar()
       self.output_zip_name.set("")
       self.encryption_key_str = StringVar()
       self.encryption_key_str.set("chave_padrao_1234567890")

       self.file_display = Text(win, width=50, height=7, state='disabled', wrap='word')
       self.output_name_label = Label(win, text="Nome do Pacote Compactado:")
       self.output_name_entry = Entry(win, textvariable=self.output_zip_name, width=25)
       self.key_label = Label(win, text="Chave de Criptografia:")
       self.key_entry = Entry(win, textvariable=self.encryption_key_str, width=25)
       self.select_button = Button(win, text="Selecionar Arquivos", command=self.select_files)
       self.process_button = Button(win, text="Processar", command=self.handle_files, bootstyle="INFO-OUTLINE")

       self.file_display.grid(row=0, column=0, columnspan=4, padx=5, pady=5)
       self.output_name_label.grid(row=5, column=0, sticky="w", padx=5, pady=2)
       self.output_name_entry.grid(row=5, column=1, columnspan=3, sticky="ew", padx=5, pady=2)
       self.key_label.grid(row=6, column=0, sticky="w", padx=5, pady=2)
       self.key_entry.grid(row=6, column=1, columnspan=3, sticky="ew", padx=5, pady=2)
       self.select_button.grid(row=7, column=0, columnspan=2, padx=5, pady=5)
       self.process_button.grid(row=7, column=2, columnspan=2, padx=5, pady=5)

       win.mainloop()

   def select_files(self):
       file_paths = tkinter.filedialog.askopenfilenames(title="Selecione os arquivos para processar")
       self.selected_files = list(file_paths)
       if self.selected_files:
           self.file_display.config(state='normal')
           self.file_display.delete('1.0', END)
           display_text = "Arquivos Selecionados:\n" + "\n".join(self.selected_files)
           self.file_display.insert(END, display_text)
           self.file_display.config(state='disabled')
       else:
           self.file_display.config(state='normal')
           self.file_display.delete('1.0', END)
           self.file_display.insert(END, "Nenhum arquivo selecionado.")
           self.file_display.config(state='disabled')

   def encrypt_file_in_parts(self, file_path):
       part_files = []
       # Determina aleatoriamente o número de partes (entre 3 e 6)
       num_parts = random.randint(3, 6)
       with open(file_path, 'rb') as f_in:
           content_bytes = f_in.read()
           # Codifica o conteúdo binário para base64 antes de criptografar
           base64_content = base64.b64encode(content_bytes).decode(self.encoding)
           encrypted_content_hex = self.encrypt(base64_content)

           total_len = len(encrypted_content_hex)
           if total_len == 0:
               return []

           part_size = math.ceil(total_len / num_parts)
           start = 0
           for i in range(num_parts):
               end = min(start + part_size, total_len)
               part_data = encrypted_content_hex[start:end]
               if not part_data:
                   break

               part_filename = f"{file_path}{self.compressed_extension}{i+1}"
               with open(part_filename, 'w') as f_part:
                   f_part.write(part_data)
               part_files.append(part_filename)
               start = end
       return part_files

   def decrypt_file_from_parts(self, encrypted_part_files, output_dir):
       # Ordena os arquivos por número na extensão para garantir a ordem correta
       encrypted_part_files.sort(key=lambda x: int(re.search(r'\d+$', x).group()) if re.search(r'\d+$', x) else 0)

       original_base_name = ""
       combined_encrypted_data = ""
       files_to_remove = []

       for part_file in encrypted_part_files:
           if not os.path.exists(part_file): continue
           
           match = re.match(r'^(.*)\.skj(\d+)$', part_file)
           if match:
               base_name_part = match.group(1)
               if not original_base_name:
                   original_base_name = base_name_part
               
               with open(part_file, 'r') as f_part:
                   combined_encrypted_data += f_part.read()
               files_to_remove.append(part_file)
           else: # Arquivo não segue o padrão .skj<numero>
                self.file_display.config(state='normal')
                self.file_display.insert(END, f"\nAviso: Ignorando arquivo com formato inesperado: {part_file}")
                self.file_display.config(state='disabled')


       if not original_base_name or not combined_encrypted_data:
           return

       decrypted_base64 = self.decrypt(combined_encrypted_data)
       original_data_bytes = base64.b64decode(decrypted_base64)
       final_output_path = os.path.join(output_dir, os.path.basename(original_base_name))

       with open(final_output_path, 'wb') as f_out:
           f_out.write(original_data_bytes)

       # Remove os arquivos temporários de partes criptografadas
       for rm_file in files_to_remove:
           try:
               os.remove(rm_file)
           except OSError as e:
               print(f"Erro ao remover arquivo temporário {rm_file}: {e}")
               
       return final_output_path

   def handle_files(self):
       self.encryption_key_str.get()
       self.format_key(self.encryption_key_str.get()) # Formata a chave antes de usar

       if not self.selected_files:
           self.file_display.config(state='normal')
           self.file_display.insert(END, "\nErro: Nenhum arquivo selecionado.")
           self.file_display.config(state='disabled')
           return

       if not self.output_zip_name.get():
           self.output_zip_name.set(time.strftime("%Y%m%d_%H%M%S"))

       zip_filename = self.output_zip_name.get() + ".zip"
       all_files_to_zip = []
       operation_type = '' # 'pack', 'unpack', 'decrypt'

       # Prepara o diretório de destino para descompressão/descriptografia
       base_processing_dir = os.path.dirname(self.selected_files[0]) if self.selected_files else "."
       os.makedirs(base_processing_dir, exist_ok=True)

       files_to_process_count = len(self.selected_files)
       processed_files_count = 0

       # Verifica o tipo de operação com base nas extensões dos arquivos selecionados
       is_zip = any(f.lower().endswith('.zip') for f in self.selected_files)
       is_encrypted_part = any(f.lower().endswith(self.compressed_extension) for f in self.selected_files)

       if is_zip and files_to_process_count == 1:
            operation_type = 'unpack'
       elif is_encrypted_part and files_to_process_count > 1: # Assume que múltiplas partes juntas indicam descriptografia
            operation_type = 'decrypt'
       else: # Assume que se não for .zip ou partes de criptografia, é para criptografar
            operation_type = 'pack'


       if operation_type == 'pack':
           self.file_display.config(state='normal')
           self.file_display.delete('1.0', END)
           self.file_display.insert(END, f"Modo: Criptografar e Compactar\n")

           temp_encrypted_part_files = []
           for file_path in self.selected_files:
               if os.path.splitext(file_path)[1] not in ['.zip', self.compressed_extension]:
                   parts = self.encrypt_file_in_parts(file_path)
                   temp_encrypted_part_files.extend(parts)
                   all_files_to_zip.extend(parts)
                   self.file_display.insert(END, f" - Criptografado: {os.path.basename(file_path)}\n")
                   processed_files_count += 1
               else:
                   self.file_display.insert(END, f" - Ignorado (formato não suportado para criptografia): {os.path.basename(file_path)}\n")

           if all_files_to_zip:
               self.create_zip(os.path.join(base_processing_dir, zip_filename), all_files_to_zip)
               self.file_display.insert(END, f"\nPacote compactado criado: {zip_filename}")
               for part_file in temp_encrypted_part_files:
                   try:
                       os.remove(part_file)
                   except OSError as e:
                       print(f"Erro ao remover arquivo temporário {part_file}: {e}")
           else:
               self.file_display.insert(END, "\nNenhum arquivo válido encontrado para criptografar.")

       elif operation_type == 'unpack':
           self.file_display.config(state='normal')
           self.file_display.delete('1.0', END)
           self.file_display.insert(END, f"Modo: Descompactar\n")

           for zip_file_path in self.selected_files:
               if zip_file_path.lower().endswith('.zip'):
                   extracted_dir = self.extract_zip(os.path.basename(zip_file_path), base_processing_dir)
                   self.file_display.insert(END, f" - Descompactado: {os.path.basename(zip_file_path)} em '{extracted_dir}'\n")
                   processed_files_count += 1
           if processed_files_count == 0:
                self.file_display.insert(END, "\nNenhum arquivo ZIP válido encontrado para descompactar.")


       elif operation_type == 'decrypt':
           self.file_display.config(state='normal')
           self.file_display.delete('1.0', END)
           self.file_display.insert(END, f"Modo: Descriptografar\n")
           
           # Agrupa os arquivos por nome base antes da extensão .skj<numero>
           grouped_files = {}
           for file_path in self.selected_files:
               match = re.match(r'^(.*)\.skj(\d+)$', file_path)
               if match:
                   base_name = match.group(1)
                   if base_name not in grouped_files:
                       grouped_files[base_name] = []
                   grouped_files[base_name].append(file_path)

           decrypted_count = 0
           for base_name, parts in grouped_files.items():
               decrypted_file = self.decrypt_file_from_parts(parts, base_processing_dir)
               if decrypted_file:
                   self.file_display.insert(END, f" - Descriptografado: {os.path.basename(decrypted_file)}\n")
                   decrypted_count += 1
           
           if decrypted_count == 0:
                self.file_display.insert(END, "\nNenhum arquivo .skj válido encontrado para descriptografar.")


       self.file_display.config(state='disabled')


if __name__ == '__main__':
   app = FileAES_v1_1()

   </numero></numero>

Considerações de Segurança e Boas Práticas

  • Chave de Criptografia: Nunca use chaves fixas diretamente no código, especialmente em produção. Utilize métodos seguros para gerenciar chaves, como variáveis de ambiente, arquivos de configuração protegidos ou gerenciadores de segredos. A chave deve ter 16, 24 ou 32 bytes para compatibilidade com AES.
  • Modo de Operação: O modo ECB é simples, mas menos seguro pois criptografa blocos idênticos de texto plano da mesma forma. Para maior segurança, considere modos como CBC, GCM ou CTR, que exigem a gestão de um vetor de inicialização (IV).
  • Tratamento de Erros: Implemente um tratamento de erros mais robusto para lidar com arquivos corrompidos, chaves inválidas ou falhas de permissão.
  • Interface do Usuário: Para aplicações de desktop, o uso de bibliotecas como ttkbootstrap melhora a aparência. Certifique-se de que os elementos da UI sejam intuitivos e forneçam feedback claro ao usuário.

Tags: Python criptografia AES ZIP compressão

Publicado em 8-9 16:51