Segunda Entrega: EP de Sockets em Python (PMR3412-V2023)
Nesta segunda entrega, será explorada API Socket, que fornece acesso aos protocolos de camada 3 (Transporte) do modelo TCP/IP. Isto possibilita a criação de aplicações TCP e UDP próprias. Nesta entrega será utilizado o port da API BSD Socket para Python. O módulo sockets já faz parte das bibliotecas padrão do Python; assim, para completar os exercícios, será necessário ter o Python instalado. Outras bibliotecas poderão ser necessárias, siga as instruções dos exercícios para instalá-las.
1. Criando aplicação TCP de geração de estaganografia (5 pontos)
A esteganografia inclui a ocultação de informações em arquivos de computador. Na esteganografia digital, as comunicações eletrônicas podem incluir codificação esteganográfica dentro de uma camada de transporte, como um arquivo de documento, arquivo de imagem, programa ou protocolo. Os arquivos de mídia são ideais para transmissão esteganográfica devido ao seu tamanho grande. Neste exercício, iremos criar uma aplicação TCP que realiza a criação automática de estaganografia e transfere o arquivo gerado para o cliente.
1.1 Testando a biblioteca stegano
Para facilitar o desenvolvimento, vamos utilizar uma implementação pronta de geração de estaganografia. Assim, vamos utilizar uma técnica denominada LSB (Least Significant Bit), que consiste em utilizar o bit menos significativo de uma determinada informação para armazenar um bit de uma nova informação, que é implementado por Cédric Bonhomme. Para testar a biblioteca é necessário seguir as etapas adiante:
- crie uma pasta para sua aplicação;
- instale a biblioteca Stegano com o comando
$ pip install stegano
- faça o download da imagem a ser utilizada e salve na pasta do projeto.
Após esta configuração inicial, crie o arquivo server.py
, que será utilizado para a implementação da parte do servidor.
Ao final, o seu diretório pode ter uma estrutura similar à:
📦mysteganoapp
┣ 📜server.py
┗ 📜img.png
Para usar a biblioteca, consulte as funções hide
e reveal
no arquivo stegano/lsb/lsb.py
.
Um exemplo de utilização é a seguinte:
from stegano import lsb
img = lsb.hide('img.png', 'secret text')
secret = lsb.reveal(img)
print(secret)
img.show()
que deve imprimir a mensagem revelada e abrir uma janela com o esteganograma.
Enunciado da parte 1.1
Esta atividade consiste em apenas configurar a aplicação local para geração de uma esteganografia com o método lsb
do pacote stegano
.
Seguem os requisitos do programa, que deve ser implementado no arquivo server.py
:
- Deve armazenar em variáveis globais os dois argumentos principais: o nome do arquivo da imagem, o texto a ser escondido. Declarar as variáveis, cujos nomes devem ser todos em maiúscula, logo após os imports.
- De acordo com estas duas informações, deve ser gerado o esteganograma com o método
lsb
do pacotestegano
. - Antes de finalizar, o programa deve exibir o esteganograma em uma janela separada ou salvá-la em um arquivo.
No relatório, apresente:
- Listagem do código em Python.
- Printscreen com o esteganograma, a mensagem revelada e os parâmetros fornecidos para criá-lo.
1.2 Criando uma aplicação TCP para transferência de um esteganograma
Vamos transformar a aplicação local em uma aplicação em rede no modelo cliente-servidor. Para tal, utilizaremos o TCP/IP através da API Socket, cuja biblioteca já vem inclusa no Python. Para entender o seu funcionamento, dê uma olhada na sua documentação, em especial na seção de exemplos em https://docs.python.org/3/library/socket.html#example.
Para enviar um objeto Image
da biblioteca Pillow via Sockets, precisamos convertê-la para um array de bytes.
No Python, isto pode ser feito pela função:
def convert_to_byte_arr(image, format):
img_byte_arr = io.BytesIO()
image.save(img_byte_arr, format=format)
return img_byte_arr.getvalue()
onde image
é o objeto do tipo Image
, que é retornado pela função lsb.hide
, e format
é o formato da imagem, p. ex. 'PNG'
.
A lista completa de formatos suportados pelo Pillow pode ser consultada em https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html.
Enunciado da parte 1.2
Esta atividade consiste em converter o programa atual em uma aplicação TCP utilizando a biblioteca Sockets do Python.
Seguem os requisitos do programa:
- Deve ser implementado em dois arquivos:
client.py
eserver.py
para as partes do cliente e servidor, respectivamente. - Ao conectar um cliente, o servidor deve enviar um esteganograma pré-definido, de uma vez só.
- O cliente, após finalizar a transferência do esteganograma, deve exibi-la em uma janela separada ou salvá-la em um arquivo, se desconectar do servidor, exibir a mensagem revelada e finalizar o programa.
- Após a desconexão de um cliente, o servidor deve continuar esperando por novas conexões.
- No servidor, exibir o endereço do socket (endereço IP e porta) do cliente logo após iniciar a conexão.
No relatório, apresente:
- Listagem do código em Python do arquivo do servidor e do cliente.
- Printscreen único mostrando a execução tanto do cliente e do servidor. Nesta imagem, deve aparecer o endereço IP e porta do cliente na saída do servidor.
- Printscreen com o esteganograma exibida/salva e a mensagem revelada no cliente.
- Abra o Wireshark e realize a captura dos pacotes trocados entre o cliente e o servidor, utilizando um filtro de porta. Apresente um printscreen da tela do Wireshark contendo o handshake de três vias.
1.3 Desenvolvendo o protocolo para personalização do esteganograma
A fim de passar argumentos para o servidor, vamos definir um protocolo básico de comunicação. Ao se conectar com o servidor, o cliente deve enviar em uma string uma mensagem de duas linhas:
- a primeira contendo a palavra
STEGANO
seguida de um espaço em branco e o caminho para a imagem; - a segunda incluindo o texto a ser escondio pelo esteganograma.
Por exemplo, podemos reproduzir o exemplo do esteganograma da seção 1.1 com a mensagem:
STEGANO img.png
secret text
Enunciado da parte 1.3
Esta atividade consiste em implementar um protocolo de comunicação para passagens de argumentos para a geração do esteganograma.
Seguem os requisitos do programa:
- O programa cliente deve receber via comando de linha os dois argumentos: o nome do arquivo da imagem e o texto secreto.
- Ao se conectar ao servidor, o cliente deve enviar a mensagem formatada de acordo com o descrito nesta seção e os argumentos recebidos via comando de linha. Imprimir esta mensagem na tela.
- O servidor deve processar a mensagem a fim de definir os argumentos. Uma vez obtidos, eles devem ser usados para gerar o esteganograma a ser enviado ao cliente.
- O resto do programa deve manter o funcionamento da versão anterior, desenvolvido na seção 1.2.
No relatório, apresente:
- Listagem do código em Python do arquivo do servidor e do cliente.
- Printscreen único mostrando a execução tanto do cliente quanto do servidor. Nesta imagem, deve aparecer o endereço IP e porta do cliente na saída do servidor.
- Printscreen com o esteganograma exibida/salva e a mensagem revelada no cliente.
2. Criando aplicação UDP de geração do esteganograma (5 pontos)
A API Socket também permite criar aplicações com o protocolo UDP.
Para tal, ao criar o objeto socket, forneça o valor socket.SOCK_DGRAM
no argumento tipo, que é o segundo argumento posicional.
Note que, devido às diferenças entre os protocolos, os métodos utilizados para a comunicação são diferentes. Outra característica importante de sockets UDP é que não existem conexões. Assim, uma aplicação UDP cliente minimalista poderia ser codificada como:
import socket
HOST = '127.0.0.1'
PORT = 50007
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(b'Hello World', (HOST, PORT))
E a parte do servidor poderia ser implementada com:
import socket
HOST = ''
PORT = 50007
sock.bind((UDP_IP, UDP_PORT))
while True:
data, addr = sock.recvfrom(1024)
print('Received', repr(data))
2.1 Adaptando a aplicação diretamente para uma aplicação UDP
Para converter a aplicação para UDP, precisamos realizar algumas operações extras, que eram tratadas pelo protocolo TCP. A primeira delas é a subdivisão, que reparte a mensagem em segmentos menores, a fim de possibilitar a transferência de arquivos grandes. No Python, uma forma de dividir um array de bytes em segmentos é através da seguinte compreensão de lista:
[byte_arr[i:i + SEG_SIZE] for i in range(0, len(byte_arr), SEG_SIZE)]
onde byte_arr
é o array em questão e SEG_SIZE
é o tamanho de cada segmento.
Enunciado da parte 2.1
Esta atividade consiste em converter a aplicação TCP do exercício anterior para uma aplicação UDP. Note que a aplicação não é confiável, pois removeremos as garantias fornecidas pelo TCP e não reimplementaremos nenhuma delas.
Seguem os requisitos do programa:
- Implementar a comunicação com sockets UDPs. O funcionamento do programa deve ser o mesmo da versão anterior.
- No servidor, a mensagem deve ser segmentada antes de enviada. Em seguida, cada parte deve ser enviada separadamente, na sequência correta. Imprimir uma mensagem para cada segmento enviado.
- No cliente, inicialize um buffer de recebimento da mensagem, que inicialmente é um array de bytes vazio. Cada segmento recebido do servidor deve ser acrescentado ao final do buffer. Imprimir uma mensagem para cada segmento recebido.
- Tanto do lado do cliente como do servidor, exibir os dados do socket (endereço IP e porta) a cada envio ou recebimento de datagrama.
No relatório, apresente:
- Listagem do código em Python do arquivo do servidor e do cliente.
- Printscreen único mostrando a execução tanto do cliente como do servidor. Nesta imagem, devem aparecer os endereços IPs e portas usadas na troca de datagramas, o conteúdo da mensagem do protocolo e as mensagens de envio/recebimento de segmentos.
- Printscreen com o esteganograma exibida/salva e a mensagem revelada no cliente.
- Abra o Wireshark e realize a captura dos pacotes trocados entre o cliente e o servidor, utilizando um filtro de porta. Apresente um printscreen da tela do Wireshark contendo alguns destes pacotes.
2.2 Implementando proteção contra entrega de segmento fora de ordem
Em uma aplicação de transferência de arquivo, não é adequado utilizar diretamente o socket UDP. Isto porque este não é confiável, e é possível que o arquivo seja corrompido e fique inutilizável após a transferência. Deste modo, é usual implementar ao menos a garantia de entrega e a proteção contra recebimentos fora de ordem. Nesta seção, vamos implementar a segunda funcionalidade.
O primeiro passo da aplicação servidor, após processar a mensagem, deve ser enviar o número total de pacotes para o cliente. Uma forma de fazer isso é converter o número em bytes primeiro, e depois enviá-lo. Isto pode ser feito com as instruções
seg_count_bytes = seg_count.to_bytes(2, byteorder='big')
sent = s.sendto(seg_count_bytes, addr)
onde seg_count
é a quantidade de segmentos e 2
é o número de bytes resultantes.
Note que o número de segmentos pode ser calculado através da fórmula
((len(img_byte_arr) + SEG_SIZE - 1) // SEG_SIZE)
No cliente, é possível receber a informação e convertê-la de volta para um inteiro com
data, addr = s.recvfrom(2)
seg_count = int.from_bytes(data, 'big')
Este mesmo procedimento pode ser utilizado para anexar um número a cada segmento.
Enunciado da parte 2.2
Esta atividade consiste em implementar mais confiabilidade à aplicação UDP através da garantia da ordem de entrega dos segmentos.
Seguem os requisitos do programa:
- No servidor, após processar a mensagem, o número de segmentos deve ser calculado e enviado para o cliente, que deve inicializar o buffer com este tamanho.
- No servidor, cada segmento deve consistir do seu número, seguido dos dados da imagem. Imprimir uma mensagem para cada segmento enviado, indicando o seu número de sequência.
- No cliente, ao receber um segmento, o número deve ser extraído e os dados devem ser inseridos no buffer na posição dada por este índice. Imprimir uma mensagem para cada segmento recebido, indicando o seu número de sequência.
- No restante, o funcionamento do programa deve ser o mesmo da versão anterior.
No relatório, apresente:
- Listagem do código em Python do arquivo do servidor e do cliente.
- Printscreen único mostrando a execução tanto do cliente quanto do servidor. Nesta imagem, devem aparecer os endereços IPs e portas usadas na troca de datagramas, o conteúdo da mensagem do protocolo e as mensagens de envio/recebimento de segmentos com seus números.
- Printscreen com o esteganograma exibida/salva e a mensagem revelada no cliente.
- Discorra sobre como você implementaria a garantia de entrega de segmentos e retransmissões.
- Na sua implementação, existiria a possibilidade de ocorrência de Sorcerer's Apprentice Syndrome? Justifique.