Sockets TCP em C

Sockets TCP em C: Comunicação criptografada na Raspberry Pi Deixe um comentário

Com o boom da Internet das Coisas (IoT), algo que muitas vezes passa batido é segurança. Segurança e IoT deveriam sempre andar de mãos dadas, já que uma falha de segurança pode prejudicar muito um projeto ou empresa. Uma das formas de se garantir segurança é utilizar criptografia, ou seja, “embaralhar” os dados de tal forma que somente o remetente consiga restaurar os dados originais, de modo que se alguém conseguir interceptar tais dados não irá conseguir decifrá-los (ao menos, não tão facilmente). Das ferramentas e bibliotecas disponíveis para criptografia, a mais utilizada e confiável do mercado atualmente é a biblioteca OpenSSL, conjunto de ferramentas open-source com vasto conteúdo em criptografia, hashs e algoritmos correlatos, possuindo performance muito boa (é leve) e facilmente compatível com sistemas Linux. E falando em Linux, a melhor forma de validar uma comunicação segura é comunicando dois programas através de sockets seguros e, visando performance, o ideal seria Sockets TCP usando a linguagem C.
O assunto deste post é a utilização do OpenSSL para fazer uma comunicação segura via Sockets TCP em C. Isso será abordado na forma de um projeto contendo dois programas – um socket TCP seguro client e um socket TCP seguro server – onde ambos se comunicarão utilizando criptografia AES 256 CBC.

Material necessário

No projeto deste post, tanto destinatário quanto remetente na comunicação abordada rodarão localmente / na mesma Raspberry, ou seja, você não precisará de um segundo hardware para formar o par destinatário / remetente. Desta forma, tudo que você precisará é de uma Raspberry Pi (modelos: 3B, 3B+, Zero W, enfim, qualquer uma com conectividade de rede) e fonte de alimentação micro-USB adequada (5V / 3A, idealmente).

É importante ressaltar que é necessário que a Raspberry Pi esteja operante, ou seja: com cartão SD com Raspbian instalado, conectividade à Internet e acesso a terminal / console local (com monitor, teclado e mouse) ou remoto (SSH ou VNC).

Princípios básicos de segurança em comunicação

Embora transparente para muitos de nós, a segurança / comunicação segura está presente em basicamente tudo que fazemos via Internet, desde uma simples conversa via aplicativo de mensagens instantâneas até numa transação bancária.

A comunicação com segurança pode ser definida como a troca de mensagens por um canal (Internet, via sockets TCP, como abordado neste post) de forma que as mensagens trocadas estão codificadas em um padrão conhecido SOMENTE por quem estiver envolvido na comunicação (destinatário e remetente). Em suma: quem vai enviar uma mensagem a codifica e quem recebe deve decodificar a mensagem recebida (utilizando o mesmo tipo de criptografia) antes de utilizá-la / interpreta-la. Isto é, de forma simplificada, uma definição de uma transmissão de dados criptografada. Isso quer dizer que criptografar uma mensagem significa codificá-la com base em um algoritmo e uma chave, de forma que somente quem tiver exatamente a chave e seguir exatamente o algoritmo utilizado na codificação conseguirá recuperar a mensagem original. Dessa forma, a comunicação fica protegida / segura.

Sobre a chave, há uma diferenciação muito importante: se a chave a ser usada deve ser idêntica no emissor e no receptor, trata-se de uma criptografia simétrica. Já se a chave a ser utilizada é diferente em ambos os lados (sistema de chave pública e chave privada, por exemplo), trata-se de uma criptografia assimétrica. Por razões de facilitar o aprendizado, este post irá abordar criptografia simétrica.

Observe na figura 1 um diagrama de funcionamento da criptografia simétrica.

Socket TCP server e socket TCP client - criptografia simétrica

O que é o OpenSSL?

O OpenSSL consiste em um conjunto de ferramentas open-source para fins de segurança. Possui suporte a muitos tipos de criptografia e tipos de hashs utilizados no mercado, sendo portanto muito utilizado nos mais variados projetos, desde dispositivos de coleta de dados remotos até soluções de pagamento e transação bancária.

Dentre suas vantagens de uso se destacam:

  • Total compatibilidade com Linux;
  • Suporte a uso com diversas linguagens de programação, as quais incluem C e Python;
  • Licença Apache-style, ou seja, você pode usar para fins de estudo ou comerciais sem restrições quanto à licença de software e
  • É muito leve.

Caso desejar mais informações sobre o OpenSSL, basta visitar o site oficial.

Projeto – Socket TCP Server e Socket TCP Client para uma comunicação segura na Raspberry Pi

O projeto deste post consiste em fazer dois programas (um client e um server) em linguagem C para estabelecer uma comunicação segura via socket TCP em C, onde as mensagens enviadas nesta comunicação são todas criptografadas com um tipo de criptografia simétrica. Para facilitar o aprendizado e compreensão, foi escolhida a criptografia AES 256 CBC.

O projeto funcionará da seguinte forma:

  1. O programa com socket client se conecta ao programa com socket server e envia uma mensagem criptografada (criptografia simétrica) usando a criptografia AES 256 CBC.
  2. Esta mensagem é descriptografada no programa com socket server, é levemente modificada, criptografada novamente com AES 256 CBC e, finalmente, é enviada de volta ao socket client.
  3. Ao receber a mensagem, o programa com socket client descriptografa a mensagem e a exibe na tela. Após isso, a conexão entre socket server e client é encerrada e ambos os programas terminam sua execução. É importante ressaltar que cada programa deverá rodar num terminal / sessão diferente na Raspberry Pi.

Observe o diagrama de operação do projeto na figura 2.

Socket TCP server e socket TCP client - Diagrama de funcionamento

No quesito chave, a criptografia simétrica utilizada no projeto possui uma “chave composta“, ou seja, uma chave que contém duas partes distintas:

  • Uma parte chamada Key, com 32 bytes (= 256 bits)
  • Uma parte chamada IV (vetor de inicialização), com 16 bytes (= 128 bits).

Desta maneira, a segurança é aumentada, uma vez que IV e Key podem não possuir vínculo / relação direta. Isto permite fazer várias “táticas” de criptografia e segurança, como por exemplo o uso de números pseudo-aleatórios / pré-definidos para a parte do vetor de inicialização (IV) e um Key fixo. Outra tática utilizada é fazer o IV utilizar a data e hora como referência, modificando assim a chave ao longo do tempo e dificultando a descoberta da lei de formação da chave.
Em resumo, com duas partes distintas, somente quem conhece como a chave é formada consegue descriptografar a mensagem.

Para facilitar o aprendizado e entendimento, este projeto possui Key e IV fixos. Porém, em um projeto comercial / de uso na vida real, isso não é algo recomendável.

Instalação das bibliotecas de desenvolvimento do OpenSSL

Antes de partir para o projeto propriamente dito, é preciso instalar as bibliotecas de desenvolvimento do OpenSSL. Para isso, utilize o comando abaixo:

sudo apt-get install libssl-dev

Considerações sobre as chaves (Key e IV) neste projeto
As seguintes considerações sobre Key e IV devem ser feitas:

  1. A Key e o IV neste projeto estão armazenados em arquivos texto. O ideal é que, para garantir maior segurança, esses arquivos sejam guardados em um local que somente o usuário root tem acesso.  Isso é recomendado pois, se a Key e o IV forem colocados junto com os códigos-fonte de um projeto, isso poderia virar uma brecha de segurança caso alguém de fora da comunicação (um hacker ou cracker, por exemplo) tivesse acesso aos códigos-fonte. Porém, para fins de aprendizado somente, neste post os arquivos com o Key e IV ficarão na mesma pasta do programa.
  2. Os arquivos com Key e IV podem estar armazenados em pastas diferentes, porém nunca podem ter conteúdos diferentes para os dois programas (programa socket server e programa socket client). Se isso ocorrer, a criptografia e descriptografia dos dados não poderá ser feita (afinal, teriam-se chaves diferentes).

Socket TCP server – código-fonte

O código-fonte do programa que funcionará como socket server está abaixo. Salve-o como socket_server.c.

IMPORTANTE: leia atentamente os comentários para maior compreensão do programa.

//includes
#include <openssl/conf.h>
#include <openssl/evp.h>
#include <openssl/err.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

//defines
#define TAM_BUFFER_CRIPTO 1024
#define TAM_BUFFER_DECRIPTO 1024
#define TAM_BUFFER_KEY 32 //32 bytes = 256 bits
#define TAM_BUFFER_IV 16 //16 bytes = 128 bits
#define TAM_MAX_MENSAGEM_BOAS_VINDAS 300
#define TAM_MAX_MENSAGEM_CLIENT 2000
#define NUM_MAX_CONEXAO_CLIENTS 1
#define PORTA_SOCKET_SERVER 8888 //Nota: o valor da porta pode ser qualquer um entre 2000 e 65535.

//IMPORTANTE: como exemplo / aprendizado, o key.txt e iv.txt serão salvos na mesma pasta do programa.
//Porém, "na vida real", os arquivos devem estar em local seguro, o que significa dizer que devem estar em pastas somente acessiveis pelo root
#define CAMINHO_ARQUIVO_KEY "key.txt" //contem o caminho para o arquivo que contem a key (32 bytes)
#define CAMINHO_ARQUIVO_IV "iv.txt" //contem o caminho para o arquivo que contem o iv (16 bytes)

//Variaveis globais
char buffer_key[TAM_BUFFER_KEY];
char buffer_iv[TAM_BUFFER_IV];
unsigned char ciphertext[TAM_BUFFER_CRIPTO]; //Buffer para o dado criptografado. Declare-o com um tamanho grande, para n$
int decryptedtext_len, ciphertext_len;

//prototypes locais
void carrega_key_e_iv(void);
void handle_errors(void);
int encrypt(unsigned char *plaintext, int plaintext_len, unsigned char *key,unsigned char *iv, unsigned char *ciphertext);
int decrypt(unsigned char *ciphertext, int ciphertext_len, unsigned char *key,unsigned char *iv, unsigned char *plaintext);

//Funcao: handle_errors - usada quando ocorre erro na encritptacao ou decriptacao, servindo para colocar na tela o erro acusado pelo OpenSSL
//Parametros: nenhum
//Retorno: nenhum
void handle_errors(void)
{
ERR_print_errors_fp(stderr);
abort();
}

//Funcao: encrypt - faz a encriptacao de uma mensagem de texto (usando OpenSSL, em AES 256 CBC)
//Parametros:
// - Ponteiro para mensagem de texto a ser criptografada
// - Tamanho da mensagem de texto a ser criptografada
// - Ponteiro para a key
// - Ponteiro para a iv
// - Ponteiro para a variavel que ira conter o dado criptografado / resultado da criptografia
//Retorno: tamanho do dado criptografado
int encrypt(unsigned char *plaintext, int plaintext_len, unsigned char *key, unsigned char *iv, unsigned char *ciphertext)
{
EVP_CIPHER_CTX *ctx; //objeto de context utilizado na encriptacao. Por razões de seguranca, deve ser limpo ao final do processo.
int len; //Guarda o tamanho da mensagem criptografada durante o processo
int ciphertext_len; //Contem o tamanho final da mensagem criptografada

//Cria/inicializa contexto
//Em caso de erro, mostra o erro acusado pelo OpenSSL na tela.
if(!(ctx = EVP_CIPHER_CTX_new()))
handle_errors();

/* Inicializa a operacao de encriptacao
IMPORTANTE
- Como o algoritmo de criptografia usado e o AES 256 CBC, tenha certeza que
a key e o iv tem o tamanho correto / esperado. Neste caso, significa dizer que
a key deve ter 32 bytes (=256 bits) e o iv deve ter 16 bytes (=128 bits)

Em caso de erro, mostra o erro acusado pelo OpenSSL na tela.
*/
if(1 != EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv))
handle_errors();

// Aqui, a mensagem de texto e efetivamente encriptada.
//Em caso de erro, mostra o erro acusado pelo OpenSSL na tela.
if(1 != EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, plaintext_len))
handle_errors();

ciphertext_len = len;

//Finalizacao da encriptacao. No processo de finalizacao de encriptacao e feito o padding.
if(1 != EVP_EncryptFinal_ex(ctx, ciphertext + len, &len)) handle_errors();
ciphertext_len += len;

//Limpa o objeto de contexto
EVP_CIPHER_CTX_free(ctx);

return ciphertext_len;
}

//Funcao: decrypt - faz a decriptacao de uma mensagem de texto (usando OpenSSL,$
//Parametros:
// - Ponteiro a mensagem criptografada
// - Tamanho da mensagem criptografada
// - Ponteiro para a key
// - Ponteiro para a iv
// - Ponteiro para a variavel que ira conter o dado decriptografado / resultado $
//Retorno: tamanho do dado decriptografado
int decrypt(unsigned char *ciphertext, int ciphertext_len, unsigned char *key, unsigned char *iv, unsigned char *plaintext)
{
EVP_CIPHER_CTX *ctx; //objeto context utilizado na decriptografia. Por razoes de seguranca, ele deve ser limpo ao final do processo
int len; //tamanho do dado decriptografado ao longo do processo.
int plaintext_len; //tamanho final do dado decriptografado

//Cria / inicializa o context
//Em caso de erro, mostra o erro acusado pelo OpenSSL na tela.
if(!(ctx = EVP_CIPHER_CTX_new()))
handle_errors();

/* Inicializa a operacao de decriptacao
IMPORTANTE
- Como o algoritmo de criptografia usado e o AES 256 CBC, tenha certeza que
a key e o iv tem o tamanho correto / esperado. Neste caso, significa dizer $
a key deve ter 32 bytes (=256 bits) e o iv deve ter 16 bytes (=128 bits)

Em caso de erro, mostra o erro acusado pelo OpenSSL na tela.
*/
if(1 != EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv))
handle_errors();

//Aqui ocorre a decriptacao de fato.
//Em caso de erro, mostra o erro acusado pelo OpenSSL na tela.
if(1 != EVP_DecryptUpdate(ctx, plaintext, &len, ciphertext, ciphertext_len))
handle_errors();

plaintext_len = len;

//Finalizacao da decriptacao.
//Em caso de erro, mostra o erro acusado pelo OpenSSL na tela.
if(1 != EVP_DecryptFinal_ex(ctx, plaintext + len, &len)) handle_errors();
plaintext_len += len;

//Limpa objeto de context
EVP_CIPHER_CTX_free(ctx);

return plaintext_len;
}

//Funcao: carrega Key e IV da criptografia
//Paramentros: nenhum
//Retorno: nennhum
void carrega_key_e_iv(void)
{
FILE *arq;
char * ptKey;
char * ptIV;
int contador_bytes;

//carrega a key (32 bytes = 256 bits)
arq = fopen(CAMINHO_ARQUIVO_KEY, "r");
if(arq == NULL)
printf("Erro: impossivel carregar a KEY\n");
else
{
contador_bytes=0;
ptKey = &buffer_key[0];
while(contador_bytes < TAM_BUFFER_KEY)
{
*ptKey = fgetc(arq);
ptKey++;
contador_bytes++;
}
}
fclose(arq);
printf("[KEY] Carregada com sucesso.\n\n");

//Carrega o IV (16 bytes = 128 bits)
arq = fopen(CAMINHO_ARQUIVO_IV, "r");
if(arq == NULL)
printf("Erro: impossivel carregar o IV\n");
else
{
contador_bytes=0;
ptIV = &buffer_iv[0];
while(contador_bytes < TAM_BUFFER_IV)
{
*ptIV = fgetc(arq);
ptIV++;
contador_bytes++;
}
}
fclose(arq);
printf("[IV] Carregado com sucesso.\n\n");
}

int main(int argc , char *argv[])
{
int socket_desc , client_sock , c , read_size; //socket_desc: descriptor do socket servidor
     //client_sock: descriptor da conexao com o client
     //read_size: contem o tamanho da estrutura que contem os dados do socket
struct sockaddr_in server , client; //server: estrutura com informações do socket (lado do servidor)
     //client: estrutura com informações do socket (lado do client)
char client_message[TAM_MAX_MENSAGEM_CLIENT]; //array utilizado como buffer dos bytes enviados pelo client
char msg_boas_vindas[TAM_MAX_MENSAGEM_BOAS_VINDAS]; //array que contem a mensagem de boas vindas (enviada no momento que a conexao e estabelecida)
char msg_client[TAM_MAX_MENSAGEM_CLIENT]; //array que contem mensagem enviada ao client enquanto a conexao estiver estabelecida

//Buffer para mensagem decriptada
unsigned char decryptedtext[TAM_BUFFER_DECRIPTO];
int i;

//Inicializacoes do OpenSSL
ERR_load_crypto_strings();
OpenSSL_add_all_algorithms();

//carrega a key e o iv salvos em local adequado
carrega_key_e_iv();

//Tenta criar socket
socket_desc = socket(AF_INET , SOCK_STREAM , 0);
if (socket_desc == -1)
{
perror("Impossivel criar socket");
    return 1;
}
puts("Socket criado com sucesso!");

//Prepara a estrutura de socket do servidor (contendo configurações do socket, como protocolo IPv4, porta de comunicacao e filtro de ips que podem se conectar)
server.sin_family = AF_INET;
server.sin_addr.s_addr = INADDR_ANY;
server.sin_port = htons( PORTA_SOCKET_SERVER );

//Tenta fazer Bind (informa que o referido socket operara na porta definida por PORTA_SOCKET_SERVER)
if( bind(socket_desc,(struct sockaddr *)&server , sizeof(server)) < 0)
{
perror("Erro ao fazer bind");
return 1;
}
puts("Bind feito com sucesso!");

//Faz o Listen. É permitido apenas uma conexao no socket
listen(socket_desc , NUM_MAX_CONEXAO_CLIENTS);

//Aguarda uma conexao
puts("Aguardando conexao...");
c = sizeof(struct sockaddr_in);

client_sock = accept(socket_desc, (struct sockaddr *)&client, (socklen_t*)&c);
//foi recebido um pedido de conexao. Verifica se o pedido foi bem sucedido
if (client_sock < 0)
{
   perror("Falha ao aceitar conexao");
    return 1;
}
puts("Conexao aceita!");

//Aguarda receber bytes do client
while( (read_size = recv(client_sock , client_message, 2000, 0)) > 0 )
{

//Descriptografa a mensagem
    ciphertext_len = read_size;
    memcpy(ciphertext, client_message,read_size);
decryptedtext_len = decrypt(ciphertext, ciphertext_len, buffer_key, buffer_iv,decryptedtext);

//Mostra a mensagem recebida (decriptografada) na tela:
printf("\n\nMensagem decriptografada: %s\n\n",decryptedtext);

//Constroi a mensagem descriptografada a ser enviada de volta ao client
memset(msg_client,0,TAM_MAX_MENSAGEM_CLIENT);
memcpy(msg_client, decryptedtext, strlen(decryptedtext));
sprintf(msg_client,"%s - modificado",msg_client);
printf("\n\nMensagem a ser enviada de volta ao client: %s.\n\n", msg_client);

//Criptografa a mensagem construida e a envia ao client
ciphertext_len = encrypt (msg_client, strlen ((char *)msg_client), buffer_key, buffer_iv, ciphertext);

write(client_sock , ciphertext , ciphertext_len);
memset(msg_client,0,TAM_MAX_MENSAGEM_CLIENT);
memset(client_message,0,TAM_MAX_MENSAGEM_CLIENT);
}

//client se desconectou. O programa sera encerrado.
if(read_size == 0)
{
puts("Client desconectado. A aplicacao sera encerrada.");
fflush(stdout);
close(client_sock); //fecha o socket utilizado, disponibilizando a porta para outras aplicacoes
}
else if(read_size == -1) //caso haja falha na recepção, o programa sera encerrado
perror("recv failed");

return 0;
}

Conforme dito anteriormente, neste projeto a chave da criptografia é composta de duas partes (Key e IV) e ambas são salvas em arquivos texto. Sendo assim, coloque na mesma pasta que você salvou o programa os arquivos texto (com a Key e IV). O conteúdo de ambos está abaixo, portanto basta copiar o conteúdo de cada um e salvar conforme indicado.

Arquivo Key (key.txt):

01234567890123456789012345678901

Arquivo IV (iv.txt):

0123456789012345

Para compilar, utilize o seguinte comando:

gcc socket_server.c -o socket_server_seguro -lcrypto

Socket TCP client – código-fonte

O código-fonte do programa que funcionará como socket client está abaixo. Salve-o como socket_client.c. Lembre-se de salvá-lo na mesma pasta do programa socket server.

IMPORTANTE: leia atentamente os comentários para maior compreensão do programa.

//Includes
#include <openssl/conf.h>
#include <openssl/evp.h>
#include <openssl/err.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <unistd.h>

//defines
#define TAM_BUFFER_CRIPTO 1024
#define TAM_BUFFER_DECRIPTO 1024
#define TAM_BUFFER_KEY 32 //32 bytes = 256 bits
#define TAM_BUFFER_IV 16 //16 bytes = 128 bits
#define TAM_MAX_MENSAGEM_BOAS_VINDAS 300
#define TAM_MAX_MENSAGEM_CLIENT 2000
#define NUM_MAX_CONEXAO_CLIENTS 1
#define PORTA_SOCKET_SERVER 8888

//IMPORTANTE: como exemplo / aprendizado, o key.txt e iv.txt serão salvos na mesma pasta do programa.
//Porém, "na vida real", os arquivos devem estar em local seguro, o que significa dizer que devem estar em pastas somente acessiveis pelo root
#define CAMINHO_ARQUIVO_KEY "key.txt" //contem o caminho para o arquivo que contem a key (32 bytes)
#define CAMINHO_ARQUIVO_IV "iv.txt" //contem o caminho para o arquivo que contem o iv (16 bytes)

//Variaveis globais
char buffer_key[TAM_BUFFER_KEY];
char buffer_iv[TAM_BUFFER_IV];
unsigned char ciphertext[TAM_BUFFER_CRIPTO];
int decryptedtext_len, ciphertext_len;

//prototypes locais
void carregar_key_e_iv(void);
void error(char *msg);
void handle_errors(void);
int encrypt(unsigned char *plaintext, int plaintext_len, unsigned char *key,unsigned char *iv, unsigned char *ciphertext);
int decrypt(unsigned char *ciphertext, int ciphertext_len, unsigned char *key,unsigned char *iv, unsigned char *plaintext);

//Funcao: handle_errors - usada quando ocorre erro na encritptacao ou decriptacao, servindo para colocar na tela o erro acusado pelo OpenSSL
//Parametros: nenhum
//Retorno: nenhum
void handle_errors(void)
{
ERR_print_errors_fp(stderr);
abort();
}

//Funcao: encrypt - faz a encriptacao de uma mensagem de texto (usando OpenSSL, em AES 256 CBC)
//Parametros:
// - Ponteiro para mensagem de texto a ser criptografada
// - Tamanho da mensagem de texto a ser criptografada
// - Ponteiro para a key
// - Ponteiro para a iv
// - Ponteiro para a variavel que ira conter o dado criptografado / resultado da criptografia
//Retorno: tamanho do dado criptografado
int encrypt(unsigned char *plaintext, int plaintext_len, unsigned char *key, unsigned char *iv, unsigned char *ciphertext)
{
EVP_CIPHER_CTX *ctx; //objeto de context utilizado na encriptacao. Por razões de seguranca, deve ser limpo ao final do processo.
int len; //Guarda o tamanho da mensagem criptografada durante o processo
int ciphertext_len; //Contem o tamanho final da mensagem criptografada

//Cria/inicializa contexto
//Em caso de erro, mostra o erro acusado pelo OpenSSL na tela.
if(!(ctx = EVP_CIPHER_CTX_new()))
handle_errors();

/* Inicializa a operacao de encriptacao
IMPORTANTE
- Como o algoritmo de criptografia usado e o AES 256 CBC, tenha certeza que
a key e o iv tem o tamanho correto / esperado. Neste caso, significa dizer que
a key deve ter 32 bytes (=256 bits) e o iv deve ter 16 bytes (=128 bits)
Em caso de erro, mostra o erro acusado pelo OpenSSL na tela.
*/
if(1 != EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv))
handle_errors();

// Aqui, a mensagem de texto e efetivamente encriptada.
//Em caso de erro, mostra o erro acusado pelo OpenSSL na tela.
if(1 != EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, plaintext_len))
handle_errors();

ciphertext_len = len;

//Finalizacao da encriptacao. No processo de finalizacao de encriptacao e feito o padding.
if(1 != EVP_EncryptFinal_ex(ctx, ciphertext + len, &len)) handle_errors();
ciphertext_len += len;

//Limpa o objeto de contexto
EVP_CIPHER_CTX_free(ctx);

return ciphertext_len;
}

//Funcao: decrypt - faz a decriptacao de uma mensagem de texto (usando OpenSSL,$
//Parametros:
// - Ponteiro a mensagem criptografada
// - Tamanho da mensagem criptografada
// - Ponteiro para a key
// - Ponteiro para a iv
// - Ponteiro para a variavel que ira conter o dado decriptografado / resultado $
//Retorno: tamanho do dado decriptografado
int decrypt(unsigned char *ciphertext, int ciphertext_len, unsigned char *key, unsigned char *iv, unsigned char *plaintext)
{
EVP_CIPHER_CTX *ctx; //objeto context utilizado na decriptografia. Por razoes de seguranca, ele deve ser limpo ao final do processo
int len; //tamanho do dado decriptografado ao longo do processo.
int plaintext_len; //tamanho final do dado decriptografado

//Cria / inicializa o context
//Em caso de erro, mostra o erro acusado pelo OpenSSL na tela.
if(!(ctx = EVP_CIPHER_CTX_new()))
handle_errors();

/* Inicializa a operacao de decriptacao
IMPORTANTE
- Como o algoritmo de criptografia usado e o AES 256 CBC, tenha certeza que
a key e o iv tem o tamanho correto / esperado. Neste caso, significa dizer $
a key deve ter 32 bytes (=256 bits) e o iv deve ter 16 bytes (=128 bits)
Em caso de erro, mostra o erro acusado pelo OpenSSL na tela.
*/
if(1 != EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv))
handle_errors();

//Aqui ocorre a decriptacao de fato.
//Em caso de erro, mostra o erro acusado pelo OpenSSL na tela.
if(1 != EVP_DecryptUpdate(ctx, plaintext, &len, ciphertext, ciphertext_len))
handle_errors();

plaintext_len = len;

//Finalizacao da decriptacao.
//Em caso de erro, mostra o erro acusado pelo OpenSSL na tela.
if(1 != EVP_DecryptFinal_ex(ctx, plaintext + len, &len)) handle_errors();
plaintext_len += len;

//Limpa objeto de context
EVP_CIPHER_CTX_free(ctx);

return plaintext_len;
}

//Funcao: Exibe o erro de socket client na tela e finaliza o programa
//Paramentros: nenhum
//Retorno: nennhum
void error(char *msg)
{
perror(msg);
exit(0);
}

//Funcao: carrega Key e IV da criptografia
//Paramentros: nenhum
//Retorno: nennhum
void carregar_key_e_iv(void)
{
FILE *arq;
char * ptKey;
char * ptIV;
int contador_bytes;

//carrega a key (32 bytes = 256 bits)
arq = fopen(CAMINHO_ARQUIVO_KEY, "r");
if(arq == NULL)
printf("Erro: impossivel carregar a KEY\n");
else
{
contador_bytes=0;
ptKey = &buffer_key[0];
while(contador_bytes < TAM_BUFFER_KEY)
{
*ptKey = fgetc(arq);
ptKey++;
contador_bytes++;
}
}
fclose(arq);
printf("[KEY] Carregada com sucesso.\n\n");

//Carrega o IV (16 bytes = 128 bits)
arq = fopen(CAMINHO_ARQUIVO_IV, "r");
if(arq == NULL)
printf("Erro: impossivel carregar o IV\n");
else
{
contador_bytes=0;
ptIV = &buffer_iv[0];
while(contador_bytes < TAM_BUFFER_IV)
{
*ptIV = fgetc(arq);
ptIV++;
contador_bytes++;
}
}
fclose(arq);
printf("[IV] Carregado com sucesso.\n\n");
}

//Programa principal
int main(int argc, char *argv[])
{
int sockfd, portno, n;
int i;
struct sockaddr_in serv_addr;
struct hostent *server;
char buffer[256];
unsigned char decryptedtext[TAM_BUFFER_DECRIPTO];
//Inicializacoes do OpenSSL
ERR_load_crypto_strings();
OpenSSL_add_all_algorithms();

//carrega a key e o iv salvos em local adequado
carregar_key_e_iv();

//Criacao do socket client
portno = PORTA_SOCKET_SERVER;
sockfd = socket(AF_INET, SOCK_STREAM, 0);

if (sockfd < 0)
error("\nERRO: impossivel abrir socket nesta porta");

//Aqui, o socket client e criado.
//OBS: na funcao gethostbyname(), pode ser passado como
//parametro tanto um DNS quanto um IP. Como neste teste será usado comunicação entre dois sockets localmente
//na Raspbnerry Pi, será utiliado o IP de loopbakc (127.0.0.1)
server = gethostbyname("127.0.0.1");

//verifica se houve falha ao contactar o host
if (server == NULL)
{
fprintf(stderr,"\nERRO: o host informado nao esta ao alcance ou nao existe.\n");
exit(0);
}

//inicializa com zeros a estrutura de socket
bzero((char *) &serv_addr, sizeof(serv_addr));

//preenche a estrutura de socket
serv_addr.sin_family = AF_INET;
bcopy((char *)server->h_addr,
(char *)&serv_addr.sin_addr.s_addr,
server->h_length);
serv_addr.sin_port = htons(portno);

//Tenta se conectar ao socket server
if (connect(sockfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)) < 0)
error("\nERRO: impossivel conectar ao host.");
else
printf("\nConexao ao host bem sucedida!\n\n");

//le a mensagem a ser enviada.
//OBS: aqui foi usado fgets() pois a funcao gets() possui uma falha,
//podendo causar buffer overflow.
printf("Mensagem a ser enviada: ");
memset(buffer,0x00,sizeof(buffer));
fgets(buffer,sizeof(buffer), stdin);

//Criptografa a mensagem construida e a envia ao host
ciphertext_len = encrypt (buffer, strlen ((char *)buffer), buffer_key, buffer_iv, ciphertext);
n = write(sockfd,ciphertext,ciphertext_len);

if (n < 0)
error("ERRO: impossivel enviar mensagem criptografada ao host");

bzero(buffer,256);

//aguarda receber mensagem criptografada do host
n = read(sockfd,ciphertext,255);
if (n < 0)
error("ERRO: falha ao receber dados do host");
//Descriptografa a mensagem e a exibe na tela
ciphertext_len = n;
decryptedtext_len = decrypt(ciphertext, ciphertext_len, buffer_key, buffer_iv,decryptedtext);

printf("\n\n[Mensagem recebida do servidor]\n\n");

for(i=0; i<decryptedtext_len; i++)
    printf("%c",decryptedtext[i]);

printf("\n\n");

//fim de programa
return 0;
}

Conforme dito anteriormente, a chave da criptografia é composta de duas partes (Key e IV) e ambas são salvas em arquivos texto. Sendo assim, coloque na mesma pasta que você salvou o programa socket client os arquivos texto com a Key e IV. O conteúdo de ambos está abaixo, logo basta copiar o conteudo de cada um e salvar conforme indicado.

Arquivo Key (key.txt).
Observe que este deve ter, obrigatoriamente, o mesmo conteúdo do arquivo key.txt do socket server.

01234567890123456789012345678901

Arquivo IV (iv.txt).
Observe que este deve ter, obrigatoriamente, o mesmo conteúdo do arquivo iv.txt do socket server.

0123456789012345

Para compilar, utilize o seguinte comando:

gcc socket_client.c -o socket_client_seguro -lcrypto

Testando ambos os programas

Primeiramente, em uma das telas do terminal da Raspberry Pi, rode o programa socket_server com o comando abaixo:

./socket_server_seguro

Em outra tela de terminal da Raspberry Pi, rode o programa socket_client com o comando abaixo:

./socket_client_seguro

É de fundamental importância que você execute sempre o programa socket TCP server ANTES do programa socket TCP client. Inverter essa ordem fará com que o exemplo / projeto não funcione.

Na tela de terminal com o programa do socket client, digite uma frase ou palavra qualquer e aperte Enter. Você observará na tela de terminal com o programa socket server que a mensagem é recebida, descriptografada, modificada, criptografada novamente e enviada de volta ao socket client. Na tela do terminal com o programa socket client, a mensagem modificada será recebida, descriptografada e exibida, com a modificação inserida no socket server. Observe a figura 3.

Socket TCP server e socket TCP client - Resultado

Gostou do post Sockets TCP em C: Comunicação criptografada na Raspberry Pi? Deixe seu comentário logo abaixo.

Faça seu comentário

Acesse sua conta e participe