Olá, Habr! Neste artigo, vamos implementar a criptografia E2E em um chat simples como exemplo prático. Continuando nossa série sobre desenvolvimento de rede em Go, na edição anterior, construímos um chat TCP básico onde as mensagens trafegavam em texto puro, tornando-as vulneráveis a interceptações. Agora, vamos adicionar criptografia para garantir a segurança na troca de dados.
Um Pouco de Teoria
Este guia foca na compreensão e aplicação prática, sem aprofundar nos complexos aspectos matemáticos da criptografia. Nosso objetivo é a implementação de engenharia. Para começar, precisamos entender três conceitos fundamentais:
- Criptografia Simétrica: Utiliza a mesma chave secreta para criptografar e descriptografar dados. É rápida e ideal para grandes volumes de informação. O principal desafio é como transferir essa chave de forma segura pela internet, evitando interceptações.
- Criptografia Assimétrica: Resolve o problema da transferência de chaves. Cada usuário possui um par de chaves: uma pública (usada para criptografar e compartilhada abertamente) e uma privada (usada para descriptografar e mantida em segredo).
- Criptografia de Curva Elíptica (ECC / ECDH): Um tipo moderno de criptografia assimétrica baseado em matemática complexa de pontos em curvas. Suas vantagens incluem velocidade significativamente maior que o RSA e chaves muito menores para o mesmo nível de segurança (apenas 32 bytes para X25519 contra 4096 bits do RSA). Utilizaremos o protocolo ECDH (Elliptic Curve Diffie-Hellman), que permite que duas partes, conhecendo as chaves públicas uma da outra, calculem um segredo compartilhado comum sem trocá-lo diretamente pela rede.
O Processo Simplificado:
- O cliente gera uma chave AES (para criptografia simétrica) e um par de chaves ECDH (privada e pública).
- As chaves públicas ECDH são trocadas entre os usuários. Cada um, usando sua chave privada e a chave pública do outro, calcula uma chave mestra compartilhada.
- Utilizando a chave mestra, cada usuário criptografa sua chave AES e a envia ao interlocutor.
- Após a troca das chaves simétricas, cada participante descriptografa a chave AES do outro.
- Ao enviar uma mensagem, o usuário a criptografa com sua chave AES, que o destinatário já possui. O destinatário, então, descriptografa a mensagem com a mesma chave.
Implementando o Cliente
Anteriormente, focamos na parte do servidor e usamos telnet como cliente. Agora, desenvolveremos a lógica do cliente:
gotype ClientChat struct { Address string Conn net.Conn } func NewClientChat(Addr string) *ClientChat { return &ClientChat{ Address: Addr, } }
Conectando ao host:
gofunc (c *ClientChat) Start() { var err error c.Conn, err = net.Dial("tcp", c.Address) if err != nil { log.Fatal("Failed connection: %w", err) } defer c.Conn.Close() c.enterToChat() go c.readLoop() scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { text := scanner.Text() if text == "" { continue } // Lógica de processamento de entrada } if err := scanner.Err(); err != nil { log.Printf("Failed read from console: %v", err) } }
Agora, vamos complementar a estrutura do cliente para armazenar suas próprias chaves e as chaves dos interlocutores:
gotype CryptoKeys struct { PublicECDHKey []byte PrivateECDHKey []byte AESKey []byte } type P2PKeys struct { SecretECDH []byte AES []byte } type ClientChat struct { Address string Conn net.Conn Keys cryptography.CryptoKeys TopologyTable map[string]P2PKeys }
Criamos uma tabela hash (TopologyTable) onde a chave é o endereço do usuário e o valor contém a chave mestra ECDH para troca de chaves simétricas e a própria chave simétrica do usuário.
Em seguida, desenvolvemos as funções criptográficas:
Geração da Chave Mestra Comum:
gofunc GenarateECDHSecret(privateKey []byte, publicKey []byte) []byte { curve := ecdh.X25519() myPrivateKey, _ := curve.NewPrivateKey(privateKey) peerPublicKey, _ := curve.NewPublicKey(publicKey) sharedSecret, _ := myPrivateKey.ECDH(peerPublicKey) aesKey := sha256.Sum256(sharedSecret) return aesKey[:] }
Criptografia da Nossa Chave Simétrica:
gofunc EncryptAESKey(secretShared []byte, aesKey []byte) []byte { block, _ := aes.NewCipher(secretShared) aesGCM, _ := cipher.NewGCM(block) nonce := make([]byte, aesGCM.NonceSize()) io.ReadFull(rand.Reader, nonce) ciphertext := aesGCM.Seal(nonce, nonce, aesKey, nil) return ciphertext }
Por que usar GCM?
O modo GCM (Galois/Counter Mode) é um modo de criptografia autenticada (AEAD - Authenticated Encryption with Associated Data). Ele realiza duas tarefas:
- Criptografa a
aesKeyusando a chave compartilhada e umnoncealeatório. - Gera uma tag de autenticidade de 16 bytes combinando o texto cifrado, o
noncee a chave secreta. Isso protege o pacote contra modificações durante a transmissão.
A estrutura resultante é: [Nonce de 12 bytes] + [Chave criptografada] + [Tag de 16 bytes].
Descriptografia da Chave do Remetente:
gofunc DecryptSenderKey(secretShared []byte, ciphertextWithNonce []byte) ([]byte, error) { block, _ := aes.NewCipher(secretShared) aesGCM, _ := cipher.NewGCM(block) nonceSize := aesGCM.NonceSize() if len(ciphertextWithNonce) < nonceSize { return nil, fmt.Errorf("ciphertext too short") } nonce := ciphertextWithNonce[:nonceSize] actualCiphertext := ciphertextWithNonce[nonceSize:] aesKey, err := aesGCM.Open(nil, nonce, actualCiphertext, nil) if err != nil { return nil, fmt.Errorf("failed to decrypt or authenticate: %w", err) } return aesKey, nil }
Criptografia e Descriptografia de Mensagens:
gofunc EncryptMessage(key []byte, plaintext []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { return nil, fmt.Errorf("failed to create cipher: %w", err) } aesGCM, err := cipher.NewGCM(block) if err != nil { return nil, fmt.Errorf("failed to create gcm: %w", err) } nonce := make([]byte, aesGCM.NonceSize()) if _, err = io.ReadFull(rand.Reader, nonce); err != nil { return nil, fmt.Errorf("failed to generate nonce: %w", err) } ciphertext := aesGCM.Seal(nonce, nonce, plaintext, nil) return ciphertext, nil } func DecryptMessage(key []byte, ciphertextWithNonce []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { return nil, fmt.Errorf("failed to create cipher: %w", err) } aesGCM, err := cipher.NewGCM(block) if err != nil { return nil, fmt.Errorf("failed to create gcm: %w", err) } nonceSize := aesGCM.NonceSize() if len(ciphertextWithNonce) < nonceSize { return nil, fmt.Errorf("ciphertext too short") } nonce := ciphertextWithNonce[:nonceSize] actualCiphertext := ciphertextWithNonce[nonceSize:] plaintext, err := aesGCM.Open(nil, nonce, actualCiphertext, nil) if err != nil { return nil, fmt.Errorf("failed to decrypt (fake tag or wrong key): %w", err) } return plaintext, nil }
Para gerenciar a comunicação entre os usuários, precisamos das seguintes estruturas de pacote:
goconst ( TypeAuth = "AUTH" TypePeerList = "PEER_LIST" TypeNewPeer = "NEW_PEER" TypeGetPeerList = "GET_PEER_LIST" TypeSenderKey = "SENDER_KEY" TypeChatMsg = "CHAT_MSG" ) type Message struct { Author string `json:"author"` Text string `json:"text"` } type PeerInfo struct { Nickname string `json:"nickname"` PublicKey []byte `json:"public_key"` } type Packet struct { Type string `json:"type"` YourName string `json:"your_name"` // Campo para filtrar pacotes próprios FromPeer string `json:"from_peer,omitempty"` PublicKey []byte `json:"public_key,omitempty"` TargetPeer string `json:"target_peer,omitempty"` EncAESKey []byte `json:"enc_aes_key,omitempty"` Peers []PeerInfo `json:"peers,omitempty"` Message []byte `json:"message,omitempty"` }
TypeAuth: Enviado pelo cliente ao conectar, contendo sua chave pública ECDH para registro no servidor.TypePeerList: Enviado pelo servidor ao novo cliente em resposta à autenticação, listando todos os participantes ativos e suas chaves públicas.TypeNewPeer: Enviado pelo servidor a todos os usuários existentes para notificar a conexão de um novo usuário e compartilhar sua chave pública.TypeGetPeerList: Requisição do cliente ao servidor para atualizar a lista de participantes (usado como fallback se a tabela de topologia estiver dessincronizada).TypeSenderKey: Enviado entre clientes via servidor, contendo a chave AES do remetente criptografada com o segredo ECDH compartilhado para o destinatário específico.TypeChatMsg: Pacote com a mensagem de texto. O conteúdo (Message) é criptografado com a chave AES do remetente, e o servidor apenas o retransmite.
Funções auxiliares para criação de pacotes:
gofunc NewPeerPacket(publicKey []byte) *message.Packet { return &message.Packet{ Type: message.TypeAuth, PublicKey: publicKey, } } func ProcessedNewPeer(newPeerPacket message.Packet, privateKey []byte, aesKey []byte) *message.Packet { secret := cryptography.GenarateECDHSecret(privateKey, newPeerPacket.PublicKey) cryptAES := cryptography.EncryptAESKey(secret, aesKey) packet := message.Packet{ Type: message.TypeSenderKey, EncAESKey: cryptAES, TargetPeer: newPeerPacket.FromPeer, } return &packet } func CreateSenderKeyPacket(cryptoAES []byte, target string) *message.Packet { return &message.Packet{ Type: message.TypeSenderKey, TargetPeer: target, EncAESKey: cryptoAES, } }
Geração das Chaves do Cliente:
gofunc GenerateClientsCrypto() *CryptoKeys { privateKey, err := ecdh.X25519().GenerateKey(rand.Reader) if err != nil { log.Fatalf("failed generate private key: %v", err) } publicKey := privateKey.PublicKey() myAESKey := make([]byte, 32) _, err = rand.Read(myAESKey) if err != nil { log.Fatalf("failed generate AES key: %v", err) } return &CryptoKeys{ PublicECDHKey: publicKey.Bytes(), PrivateECDHKey: privateKey.Bytes(), AESKey: myAESKey, } }
Lógica de Leitura do Cliente:
gofunc (c *ClientChat) readLoop() { scanner := bufio.NewScanner(c.Conn) for scanner.Scan() { var packet message.Packet if err := json.Unmarshal(scanner.Bytes(), &packet); err != nil { log.Printf("Error parsing packet: %v", err) continue } c.processPacket(packet) } }
O cliente lê o fluxo de pacotes enviados pelo servidor e os processa:
Processamento de Pacotes:
gofunc (c *ClientChat) processPacket(packet message.Packet) error { switch packet.Type { case message.TypeNewPeer: if packet.FromPeer == packet.YourName { c.addAES(packet.FromPeer, c.Keys.AESKey) return nil } fmt.Printf(" [System]: User %s enter to chat.\n", packet.FromPeer) sendKeyPacket := processedpacket.ProcessedNewPeer(packet, c.Keys.PrivateECDHKey, c.Keys.AESKey) secretECDH := cryptography.GenarateECDHSecret(c.Keys.PrivateECDHKey, packet.PublicKey) c.addECDH(packet.FromPeer, secretECDH) c.sendMessage(*sendKeyPacket) case message.TypeSenderKey: peerKeys := c.getP2PKeys(packet.FromPeer) aesKey, err := cryptography.DecryptSenderKey(peerKeys.SecretECDH, packet.EncAESKey) if err != nil { log.Printf("Failed decrypt aes: %v", err) return err } c.addAES(packet.FromPeer, aesKey) case message.TypeChatMsg: keys := c.getP2PKeys(packet.FromPeer) decryptMessage, _ := cryptography.DecryptMessage(keys.AES, packet.Message) var msg message.Message if err := json.Unmarshal(decryptMessage, &msg); err != nil { log.Printf("Failed decode message: %v\n", err) return err } fmt.Printf("[%s]: %s\n", packet.FromPeer, msg.Text) case message.TypePeerList: c.createTopologyTable(packet.Peers) c.BroadcastAES() default: log.Printf("incorrect packet type: %s", packet.Type) return fmt.Errorf("incorrect packet type: %s", packet.Type) } return nil }
TypeNewPeer: Quando um novo usuário entra, o cliente gera a chave mestra ECDH com o novo usuário, criptografa sua chave AES e a envia. A chave mestra é armazenada localmente.TypeSenderKey: Ao receber a chave AES criptografada, o cliente usa a chave mestra ECDH armazenada para descriptografá-la e salvá-la para comunicação futura com aquele usuário.TypeChatMsg: A mensagem recebida é descriptografada usando a chave AES correspondente ao remetente, que já deve estar disponível localmente.TypePeerList: O cliente processa a lista de participantes recebida do servidor, inicializando a tabela de topologia e transmitindo sua chave AES para os outros.
Métodos Auxiliares do Cliente:
enterToChat(): Envia um pacoteAUTHcom a chave pública ECDH para iniciar o handshake.createTopologyTable(peers): Inicializa a tabela de topologia com as chaves públicas recebidas, calculando os segredos ECDH compartilhados.BroadcastAES(): Envia a chave AES do cliente para todos os outros participantes, criptografada com os segredos ECDH correspondentes.getP2PKeys(user): Recupera as chaves ECDH e AES para um determinado usuário, solicitando uma atualização da lista ao servidor se necessário.getTopologyTable(): Solicita ao servidor uma atualização da lista de participantes.sendMessage(packet): Serializa um pacote em JSON e o envia via TCP.addECDH(user, ecdhKey): Adiciona ou atualiza o segredo ECDH para um usuário naTopologyTable.addAES(user, aesKey): Adiciona ou atualiza a chave AES para um usuário naTopologyTable.
Ajustando o Servidor
O servidor precisa armazenar as chaves públicas dos usuários, processar pacotes e gerenciar o handshake:
gotype Peer struct { Conn net.Conn PublicKey []byte ConnectedAt time.Time } type Server struct { Address string Listener net.Listener clients map[string]*Peer deadClients []net.Conn mu sync.RWMutex messagesChan chan message.Packet }
A estrutura Peer agora inclui a chave pública do cliente. O canal messagesChan será usado para transmitir pacotes.
Handshake e Registro do Usuário:
gofunc (s *Server) handleConn(conn net.Conn) { defer conn.Close() err := s.handshake(conn) if err != nil { return } buf := make([]byte, 2048) for { n, err := conn.Read(buf) if err != nil { log.Printf("Connection error: %v", err) return } var packet message.Packet err = json.Unmarshal(buf[:n], &packet) if err != nil { log.Printf("Failed encoding packet: %v", err) continue } packet.FromPeer = conn.RemoteAddr().String() s.processPacket(packet) } } func (s *Server) handshake(conn net.Conn) error { if _, exists := s.clients[conn.RemoteAddr().String()]; exists { conn.Close() return fmt.Errorf("user already connection") } buf := make([]byte, 2048) n, err := conn.Read(buf) if err != nil { log.Printf("Handshake error: %v", err) return fmt.Errorf("failed handshake: %w", err) } var packet message.Packet err = json.Unmarshal(buf[:n], &packet) if err != nil { log.Printf("Handshake error: %v", err) return fmt.Errorf("failed handshake: %v", err) } if packet.Type != message.TypeAuth { log.Printf("Incorrect type packet") return fmt.Errorf("client hello packet need auth type") } s.registerPeer(conn, packet.PublicKey) packet.Type = message.TypeNewPeer packet.FromPeer = conn.RemoteAddr().String() s.processPacket(packet) packetListPeers := s.generatePacketListPeers(conn.RemoteAddr().String()) s.processPacket(*packetListPeers) return nil }
Durante o handshake, o servidor aguarda o pacote AUTH, registra o novo usuário com sua chave pública, envia um pacote NEW_PEER e, em seguida, um PEER_LIST com todos os participantes ativos.
Geração do Pacote de Lista de Participantes:
gofunc (s *Server) generatePacketListPeers(target string) *message.Packet { var packet message.Packet packet.TargetPeer = target packet.Type = message.TypePeerList peersInfo := make([]message.PeerInfo, 0, len(s.clients)) s.mu.Lock() for peer, data := range s.clients { if peer != target { peersInfo = append(peersInfo, message.PeerInfo{ Nickname: peer, PublicKey: data.PublicKey, }) } } s.mu.Unlock() packet.Peers = peersInfo return &packet }
Processamento de Pacotes no Servidor:
gofunc (s *Server) processPacket(packet message.Packet) error { switch packet.Type { case message.TypeNewPeer, message.TypeChatMsg: s.messagesChan <- packet case message.TypePeerList, message.TypeSenderKey: targetConn, exists := s.getPeerConn(packet.TargetPeer) if !exists { log.Printf("not found target connection") return fmt.Errorf("not found target connection") } s.writeInConnection(targetConn, packet) case message.TypeGetPeerList: packetList := s.generatePacketListPeers(packet.FromPeer) targetConn, exists := s.getPeerConn(packet.TargetPeer) // Note: TargetPeer here is likely incorrect, should be FromPeer if !exists { log.Printf("not found target connection") return fmt.Errorf("not found target connection") } s.writeInConnection(targetConn, *packetList) default: return fmt.Errorf("incorrect packet type") } return nil }
- Pacotes
NEW_PEEReCHAT_MSGsão enviados para o canalmessagesChanpara broadcast. - Pacotes
PEER_LISTeSENDER_KEY, direcionados a um usuário específico, são encaminhados diretamente para a conexão do destinatário. GET_PEER_LISTaciona a geração e envio de uma lista de participantes para o remetente.
Testando a Implementação
Ao conectar dois clientes e enviar uma mensagem, o servidor registrará um erro ao tentar decodificar a mensagem de chat, pois ela está criptografada. No entanto, os clientes conseguirão trocar mensagens criptografadas com sucesso.
Exemplo de Saída do Cliente:
─$ go run cmd/client/main.go
Hello world!
[[::1]:35176]: Hello world!
Exemplo de Saída do Servidor (com erro de decodificação):
└─$ go run cmd/server/main.go
2026/06/28 20:35:01 Server started
2026/06/28 20:35:04 Welcome, [::1]:56122
2026/06/28 20:35:08 Welcome, [::1]:56126
2026/06/28 20:35:12 failed decode message on server side: invalid character 'B' looking for beginning of value
2026/06/28 20:35:12 message byte: [66 118 215 94 9 164 135 205 93 184 98 52 117 159 67 10 187 128 100 149 226 36 107 89 162 153 217 94 103 29 239 219 126 9 228 218 119 54 195 208 128 194 140 213 22 216 245 239 45 3 91 2 159 88 91 44 117 195 53 166 138 86 89]
Este exemplo demonstra com sucesso a implementação da criptografia End-to-End (E2E) em um chat, garantindo a confidencialidade das comunicações.
Código Principal do Cliente:
gopackage client import ( "bufio" "encoding/json" "fmt" "log" "net" "os" cryptography "github.com/barashF/TCP-chat/crypto" "github.com/barashF/TCP-chat/message" processedpacket "github.com/barashF/TCP-chat/processed_packet" ) type P2PKeys struct { SecretECDH []byte AES []byte } type ClientChat struct { Address string Conn net.Conn Keys cryptography.CryptoKeys TopologyTable map[string]P2PKeys } func NewClientChat(Addr string) *ClientChat { return &ClientChat{ Address: Addr, Keys: *cryptography.Gener





