Aprendendo: escrevendo um messenger p2p com criptografia de ponta a ponta

Mais um P2P Messenger


Ler análises e documentação do idioma não é suficiente para aprender a escrever aplicativos mais ou menos úteis.


Certifique-se de consolidar, você precisa criar algo interessante para que os desenvolvimentos possam ser usados ​​em outras tarefas.


Exemplo de interface do usuário do ReactJs Chat


Este artigo é destinado a iniciantes interessados ​​no idioma go e nas redes ponto a ponto.
E para profissionais que podem oferecer idéias razoáveis ​​ou criticar construtivamente.


Eu tenho programado por algum tempo com vários graus de imersão em java, php, js, python.
E toda linguagem de programação é boa em seu campo.


A principal área do Go é a criação de serviços distribuídos, microsserviços.
Na maioria das vezes, um microsserviço é um pequeno programa que executa sua funcionalidade altamente especializada.


Como os microsserviços ainda devem se comunicar, a ferramenta para criar microsserviços deve permitir uma rede fácil e sem problemas.
Para testar isso, escreveremos um aplicativo organizando uma rede descentralizada de pares (ponto a ponto), o mais simples é um mensageiro p2p (a propósito, existe um sinônimo russo para essa palavra?).


No código, eu invento ativamente bicicletas e passo o rake para sentir golang, receber críticas construtivas e sugestões racionais.


O que fazemos


Par (par) - uma instância única do messenger.


Nosso messenger deve ser capaz de:


  • Encontrar festas próximas
  • Estabelecer uma conexão com outros pares
  • Criptografe a troca de dados com colegas
  • Receber mensagens do usuário
  • Mostrar mensagens para o usuário

Para tornar a tarefa um pouco mais interessante, vamos fazer com que tudo passe por uma porta de rede.


O esquema condicional do mensageiro


Se você puxar essa porta por HTTP, obteremos um aplicativo React que puxa a mesma porta estabelecendo uma conexão de soquete da web.


Se você puxar a porta via HTTP e não da máquina local, mostramos o banner.


Se outro ponto estiver conectado a essa porta, será estabelecida uma conexão permanente com criptografia de ponta a ponta.


Determinar o tipo de conexão de entrada


Primeiro, abra a porta para ouvir e aguardaremos novas conexões.


net.ListenTCP("tcp", tcpAddr) 

Na nova conexão, leia os 4 primeiros bytes.


Pegamos a lista de verbos HTTP e comparamos nossos 4 bytes com ele.


Agora, determinamos se uma conexão é feita a partir da máquina local e, se não, respondemos com um banner e desligamos.


  buf, err := readWriter.Peek(4) /*   */ if ItIsHttp(buf) { handleHttp(readWriter, conn, p) } else { peer := proto.NewPeer(conn) p.HandleProto(readWriter, peer) } /* ... */ if !strings.EqualFold(s, "127") && !strings.EqualFold(s, "[::") { response.Body = ioutil.NopCloser(strings.NewReader("Peer To Peer Messenger. see https://github.com/easmith/p2p-messenger")) } 

Se a conexão for local, responderemos com o arquivo correspondente à solicitação.


Decidi escrever o processamento pessoalmente, embora pudesse usar o manipulador disponível na biblioteca padrão.


  //   func processRequest(request *http.Request, response *http.Response) {/*    */} //     fileServer := http.FileServer(http.Dir("./front/build/")) fileServer.ServeHTTP(NewMyWriter(conn), request) 

Se o caminho /ws solicitado, tentamos estabelecer uma conexão com o websocket.


Desde que montei a bicicleta no processamento de solicitações de arquivos, processarei a conexão ws usando a biblioteca gorilla / websocket .


Para fazer isso, crie MyWriter e implemente métodos nele para corresponder às interfaces http.ResponseWriter e http.Hijacker .


  // w - MyWriter func handleWs(w http.ResponseWriter, r *http.Request, p *proto.Proto) { c, err := upgrader.Upgrade(w, r, w.Header()) /*          */ } 

Detecção por pares


Para procurar pares em uma rede local, usaremos o multicast UDP.


Enviaremos pacotes com informações sobre nós mesmos para o endereço IP Multicast.


  func startMeow(address string, p *proto.Proto) { conn, err := net.DialUDP("udp", nil, addr) /* ... */ for { _, err := conn.Write([]byte(fmt.Sprintf("meow:%v:%v", hex.EncodeToString(p.PubKey), p.Port))) /* ... */ time.Sleep(1 * time.Second) } } 

E ouça separadamente do IP Multicast para todos os pacotes UDP.


  func listenMeow(address string, p *proto.Proto, handler func(p *proto.Proto, peerAddress string)) { /* ... */ conn, err := net.ListenMulticastUDP("udp", nil, addr) /* ... */ _, src, err := conn.ReadFromUDP(buffer) /* ... */ // connectToPeer handler(p, peerAddress) } 

Assim, nos declaramos e aprendemos sobre a aparência de outras festas.


Seria possível organizar isso no nível do IP e, mesmo na documentação oficial do pacote IPv4, apenas o pacote de dados multicast é fornecido como um exemplo de código.


Protocolo de interação entre pares


Empacotaremos toda a comunicação entre pares em um envelope (Envelope).


Em qualquer envelope, há sempre um remetente e um destinatário; a isso, adicionaremos um comando (que ele carrega com ele), um identificador (até agora este é um número aleatório, mas pode ser feito como um hash de conteúdo), o comprimento do conteúdo e o próprio conteúdo do envelope - uma mensagem ou parâmetros de comando.


Bytes de envelope


O comando (ou o tipo de conteúdo) é colocado com sucesso no início do envelope e definimos uma lista de comandos de 4 bytes que não se cruzam com os nomes dos verbos HTTP.


Todo o envelope durante a transmissão é serializado em uma matriz de bytes.


Aperto de mão


Quando a conexão é estabelecida, o banquete imediatamente pede um aperto de mão, fornecendo seu nome, chave pública e chave pública efêmera para gerar uma chave de sessão compartilhada.


Em resposta, o par recebe um conjunto de dados semelhante, registra o par encontrado em sua lista e calcula (CalcSharedSecret) a chave de sessão comum.


  func handShake(p *proto.Proto, conn net.Conn) *proto.Peer { /* ... */ peer := proto.NewPeer(conn) /*     */ p.SendName(peer) /*     */ envelope, err := proto.ReadEnvelope(bufio.NewReader(conn)) /* ... */ } 

Troca de festa


Após um aperto de mão, os pares trocam suas listas =)


Para fazer isso, um envelope com o comando LIST é enviado e uma lista JSON de pares é colocada em seu conteúdo.
Em resposta, obtemos um envelope semelhante.


Encontramos nas listas de novos e com cada um deles tentamos nos conectar, cumprimentar, trocar festas e assim por diante ...


Mensagens do usuário


As mensagens personalizadas são de grande valor para nós, portanto criptografamos e assinamos cada conexão.


Sobre criptografia


Nas bibliotecas padrão (google) golang do pacote de criptografia, muitos algoritmos diferentes são implementados (não há padrões GOST).


A mais conveniente para assinaturas, eu acho, é a curva Ed25519. Usaremos a biblioteca ed25519 para assinar mensagens.


No começo, pensei em usar um par de chaves obtido do ed25519 não apenas para assinar, mas também para gerar uma chave de sessão.


No entanto, as chaves para assinatura não são aplicáveis ​​ao cálculo da chave compartilhada - você ainda precisa conjurá-las:


 func CreateKeyExchangePair() (publicKey [32]byte, privateKey [32]byte) { pub, priv, err := ed25519.GenerateKey(nil) /* ... */ copy(publicKey[:], pub[:]) copy(privateKey[:], priv[:]) curve25519.ScalarBaseMult(&publicKey, &privateKey) /* ... */ } 

Portanto, foi decidido gerar chaves efêmeras e, de um modo geral, essa é a abordagem correta que não deixa os atacantes a chance de escolher uma chave comum.


Para os amantes da matemática, aqui estão os links do wiki:
Protocolo Diffie - Hellman_ sobre curvas elípticas
Assinatura digital EdDSA


A geração de uma chave compartilhada é bastante padrão: primeiro, para uma nova conexão, geramos chaves efêmeras, enviamos um envelope com uma chave pública para o soquete.


O lado oposto faz o mesmo, mas em uma ordem diferente: recebe um envelope com uma chave pública, gera seu próprio par e envia a chave pública para o soquete.


Agora, cada participante tem as chaves efêmeras públicas e privadas de outra pessoa.


Ao multiplicá-los, obtemos a mesma chave para ambos, que usaremos para criptografar as mensagens.


 //CalcSharedSecret Calculate shared secret func CalcSharedSecret(publicKey []byte, privateKey []byte) (secret [32]byte) { var pubKey [32]byte var privKey [32]byte copy(pubKey[:], publicKey[:]) copy(privKey[:], privateKey[:]) curve25519.ScalarMult(&secret, &privKey, &pubKey) return } 

Criptografaremos mensagens pelo algoritmo AES de longa data no modo de acoplamento de bloco (CBC).


Todas essas implementações são facilmente encontradas na documentação do golang.


O único refinamento é preencher automaticamente a mensagem com zero bytes para a multiplicidade de seu comprimento até o comprimento do bloco de criptografia (16 bytes).


  //Encrypt the message func Encrypt(content []byte, key []byte) []byte { padding := len(content) % aes.BlockSize if padding != 0 { repeat := bytes.Repeat([]byte("\x00"), aes.BlockSize-(padding)) content = append(content, repeat...) } /* ... */ } //Decrypt encrypted message func Decrypt(encrypted []byte, key []byte) []byte { /* ... */ encrypted = bytes.Trim(encrypted, string([]byte("\x00"))) return encrypted } 

Em 2013, ele implementou o AES (com um modo semelhante ao CBC) para criptografar mensagens no Telegram como parte de um concurso de Pavel Durov.


Naquela época, o protocolo Diffie-Hellman mais comum era usado em telegramas para gerar uma chave efêmera.


E, para excluir a carga de conexões falsas, antes de cada troca de chaves, os clientes resolviam o problema de fatoração.


GUI


Precisamos mostrar uma lista de pares e uma lista de mensagens com eles e também responder a novas mensagens aumentando o contador ao lado do nome do mesmo.


Aqui sem problemas - ReactJS + websocket.


As mensagens de soquete da Web são essencialmente envelopes exclusivos, mas não contêm textos cifrados.


Todos eles são "herdeiros" do tipo WsCmd e são serializados em JSON na transferência.


  //Serializable interface to detect that can to serialised to json type Serializable interface { ToJson() []byte } func toJson(v interface{}) []byte { json, err := json.Marshal(v) /*  err */ return json } /* ... */ //WsCmd WebSocket command type WsCmd struct { Cmd string `json:"cmd"` } //WsMessage WebSocket command: new Message type WsMessage struct { WsCmd From string `json:"from"` To string `json:"to"` Content string `json:"content"` } //ToJson convert to JSON bytes func (v WsMessage) ToJson() []byte { return toJson(v) } /* ... */ 

Portanto, uma solicitação HTTP chega à raiz ("/"), agora para exibir a frente, procure no diretório "front / build" e dê index.html


Bem, a interface é composta, agora a escolha para os usuários é: executá-la em um navegador ou em uma janela separada - WebView.


Para a última opção usada zserge / webview


  e := webview.Open("Peer To Peer Messenger", fmt.Sprintf("http://localhost:%v", initParams.Port), 800, 600, false) 

Para criar um aplicativo com ele, você precisa instalar outro sistema


  sudo apt install libwebkit2gtk-4.0-dev 

Ao pensar na GUI, encontrei muitas bibliotecas para GTK, QT e a interface do console ficaria muito nerd - https://github.com/jroimartin/gocui - na minha opinião, uma ideia muito interessante.


Lançamento do Messenger


Instalação Golang


Claro, você primeiro precisa instalar o go.
Para fazer isso, eu recomendo usar a instrução golang.org/doc/install .


Instruções simplificadas para bash script


Faça o download de um aplicativo no GOPATH


É tão organizado que todas as bibliotecas e até seus projetos devem estar no chamado GOPATH.


Por padrão, isso é $ HOME / go. Go permite que você puxe a fonte do repositório público com um comando simples:


  go get github.com/easmith/p2p-messenger 

Agora, no $HOME/go/src/github.com/easmith/p2p-messenger fonte da ramificação principal aparecerá


Instalação NPM e montagem frontal


Como escrevi acima, nossa GUI é um aplicativo da Web com uma frente no ReactJs, portanto a frente ainda precisa ser montada.


Nodejs + npm - aqui como de costume.


Apenas no caso, aqui está a instrução para o ubuntu


Agora começamos a montagem frontal como padrão


 cd front npm update npm run build 

A frente está pronta!


Lançamento


Vamos voltar à raiz e lançar a festa do nosso mensageiro.


Na inicialização, podemos especificar o nome do nosso par, porta, arquivo com os endereços de outros pares e um sinalizador indicando se o WebView deve ser iniciado.


Por padrão, $USER@$HOSTNAME é usado como o nome do $USER@$HOSTNAME e a porta 35035.


Então, começamos e conversamos com amigos na rede local.


  go run app.go -name Snowden 

Feedback sobre programação golang


  • A coisa mais importante que eu gostaria de observar: em andamento, ele imediatamente implementa o que eu pretendia .
    Quase tudo que você precisa está na biblioteca padrão.
  • No entanto, houve uma dificuldade quando iniciei o projeto em um diretório diferente do GOPATH.
    Eu usei o GoLand para escrever código. E, a princípio, era embaraçoso formatar automaticamente o código com bibliotecas de importação automática.
  • Existem muitos geradores de código no IDE , o que nos permitiu focar no desenvolvimento e não no conjunto de códigos.
  • Você se acostuma rapidamente ao tratamento frequente de erros, mas um rosto de mão acontece quando você percebe que uma situação normal é quando a essência do erro é analisada de acordo com sua representação em cadeia.
     err != io.EOF 
  • As coisas estão um pouco melhores com a biblioteca do sistema operacional. Tais construções ajudam a entender a essência do problema.
     if os.IsNotExist(err) { /* ... */ } 
  • Pronto, ensina-nos a documentar corretamente o código e escrever testes.
    E há alguns mas. Nós descrevemos a interface com o método ToJson() .
    Portanto, o gerador de documentação não herda a descrição desse método aos métodos que o implementam; portanto, para remover avisos desnecessários, você deve copiar a documentação para cada método implementado (proto / mtypes.go).
  • Recentemente eu me acostumei com o poder do log4j em java, então não há um bom logger suficiente.
    Provavelmente vale a pena ver a vastidão do belo log do github com anexadores e formatadores.
  • Trabalho incomum com matrizes.
    Por exemplo, a concatenação ocorre através da função de append e a conversão de uma matriz de comprimento arbitrário em uma matriz de comprimento fixo através da copy .
  • switch-case funciona como if-elseif-else - mas esta é uma abordagem interessante, mas novamente com a mão:
    se quisermos o comportamento usual de switch-case , precisamos apresentar um fallthrough para cada caso.
    Você também pode usar goto , mas não vamos, por favor!
  • Não há operador ternário e, muitas vezes, isso não é conveniente.

O que vem a seguir?


Portanto, o mais simples mensageiro ponto a ponto é implementado.


Os cones estão abarrotados; você pode melhorar ainda mais a funcionalidade do usuário: enviar arquivos, fotos, áudio, emoticons, etc., etc.


E você não pode inventar seu protocolo e usar os Buffers de protocolo do Google,
Conecte blockchain e proteja-se contra spam usando os contratos inteligentes da Ethereum.


Em contratos inteligentes, organize bate-papos em grupo, canais, sistema de nomes, avatares e perfis de usuário.


Também é imperativo executar pares de sementes, implementar o desvio NAT e enviar mensagens de ponto a ponto.


Como resultado, você recebe um bom telegrama / telefone de substituição, basta transferir todos os seus amigos para lá =)


Utilidade


Alguns links

No decorrer do trabalho no messenger, achei páginas interessantes para um desenvolvedor iniciante.
Eu os compartilho com você:


golang.org/doc/ - documentação em idioma, tudo é simples, claro e com exemplos. A mesma documentação pode ser executada localmente com o comando


 godoc -HTTP=:6060 

gobyexample.com - uma coleção de exemplos simples


golang-book.ru - um bom livro em russo


github.com/dariubs/GoBooks é uma coleção de livros sobre o Go.


awesome-go.com - Uma lista de bibliotecas, estruturas e aplicativos interessantes em movimento. A categorização é mais ou menos, mas a descrição de muitos deles é muito escassa, o que não ajuda na pesquisa por Ctrl + F

Source: https://habr.com/ru/post/pt437686/


All Articles