Aprendizaje: escribir un mensajero p2p con cifrado de extremo a extremo

Otro mensajero P2P


Leer reseñas y documentación de idiomas no es suficiente para aprender a escribir aplicaciones más o menos útiles en él.


Asegúrese de consolidar, debe crear algo interesante para que los desarrollos puedan utilizarse en otras tareas.


Ejemplo de IU de chat de ReactJs


Este artículo está dirigido a principiantes interesados ​​en el lenguaje go y las redes punto a punto.
Y para profesionales que pueden ofrecer ideas razonables o criticar constructivamente.


He estado programando durante bastante tiempo con diferentes grados de inmersión en java, php, js, python.
Y cada lenguaje de programación es bueno en su campo.


El área principal de Go es la creación de servicios distribuidos, microservicios.
Muy a menudo, un microservicio es un pequeño programa que realiza su funcionalidad altamente especializada.


Pero los microservicios aún deberían poder comunicarse entre sí, por lo que la herramienta para crear microservicios debería permitir una conexión en red fácil e indolora.
Para probar esto, escribiremos una aplicación que organice una red descentralizada de pares (Peer-to-Peer), el más simple es un mensajero p2p (por cierto, ¿hay un sinónimo ruso para esta palabra?).


En el código, invento activamente bicicletas y me subo al rastrillo para sentir golang, recibir críticas constructivas y sugerencias racionales.


Que hacemos


Peer (peer): una instancia única del messenger.


Nuestro mensajero debería poder:


  • Encuentra fiestas cercanas
  • Establecer una conexión con otros compañeros
  • Cifrar el intercambio de datos con compañeros
  • Recibir mensajes del usuario
  • Mostrar mensajes al usuario

Para hacer la tarea un poco más interesante, hagamos que todo pase por un puerto de red.


El esquema condicional del mensajero.


Si extrae este puerto a través de HTTP, obtenemos una aplicación React que extrae el mismo puerto estableciendo una conexión de socket web.


Si extrae el puerto a través de HTTP no desde la máquina local, entonces mostramos el banner.


Si hay otro par conectado a este puerto, se establece una conexión permanente con cifrado de extremo a extremo.


Determinar el tipo de conexión entrante


Primero, abra el puerto para escuchar y esperaremos nuevas conexiones.


net.ListenTCP("tcp", tcpAddr) 

En la nueva conexión, lea los primeros 4 bytes.


Tomamos la lista de verbos HTTP y comparamos nuestros 4 bytes con ella.


Ahora determinamos si se realiza una conexión desde la máquina local, y si no, respondemos con un banner y colgamos.


  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")) } 

Si la conexión es local, respondemos con el archivo correspondiente a la solicitud.


Luego decidí escribir el procesamiento yo mismo, aunque podía usar el controlador disponible en la biblioteca estándar.


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

Si /ws solicita la ruta /ws , intentamos establecer una conexión websocket.


Desde que ensamblé la bicicleta procesando solicitudes de archivos, haré el procesamiento de la conexión ws usando la biblioteca gorilla / websocket .


Para hacer esto, cree MyWriter e implemente métodos para que correspondan a las interfaces http.ResponseWriter y http.Hijacker .


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

Detección de pares


Para buscar pares en una red local, utilizaremos la multidifusión UDP.


Enviaremos paquetes con información sobre nosotros a la dirección IP de multidifusión.


  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) } } 

Y escuche por separado de Multicast IP para todos los paquetes 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) } 

Así nos declaramos y aprendemos sobre la aparición de otras fiestas.


Sería posible organizar esto a nivel de IP, e incluso en la documentación oficial del paquete IPv4, solo se proporciona el paquete de datos de multidifusión como un ejemplo de código.


Protocolo de interacción entre pares


Empaquetaremos toda la comunicación entre pares en un sobre (Sobre).


En cualquier sobre siempre hay un remitente y un destinatario, a esto le agregaremos un comando (que lleva con él), un identificador (hasta ahora es un número aleatorio, pero se puede hacer como un hash de contenido), la longitud del contenido y el contenido del sobre en sí, un mensaje o parámetros de comando.


Bytes de sobre


El comando (o el tipo de contenido) se coloca con éxito al comienzo del sobre y definimos una lista de comandos de 4 bytes que no se cruzan con los nombres de los verbos HTTP.


Todo el sobre durante la transmisión se serializa en una matriz de bytes.


Apretón de manos


Cuando se establece la conexión, la fiesta se extiende inmediatamente para un apretón de manos, dando su nombre, clave pública y clave pública efímera para generar una clave de sesión compartida.


En respuesta, el par recibe un conjunto similar de datos, registra al par encontrado en su lista y calcula (CalcSharedSecret) la clave de sesión común.


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

Intercambio de fiestas


Después de un apretón de manos, los compañeros intercambian sus listas de compañeros =)


Para hacer esto, se envía un sobre con el comando LIST y se coloca una lista JSON de pares en su contenido.
En respuesta, obtenemos un sobre similar.


Encontramos en las listas de nuevos y con cada uno de ellos tratamos de conectarnos, estrecharnos la mano, intercambiar fiestas, etc.


Mensajes de usuario


Los mensajes personalizados son de gran valor para nosotros, por lo que encriptaremos y firmaremos cada conexión.


Sobre el cifrado


En las bibliotecas de golang estándar (google) del paquete de cifrado, se implementan muchos algoritmos diferentes (no hay estándares GOST).


La más conveniente para las firmas, creo, es la curva Ed25519. Utilizaremos la biblioteca ed25519 para firmar mensajes.


Al principio, pensé en usar un par de claves obtenido de ed25519 no solo para firmar, sino también para generar una clave de sesión.


Sin embargo, las claves para firmar no son aplicables para calcular la clave compartida; aún debe conjurarlas:


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

Por lo tanto, se decidió generar claves efímeras y, en términos generales, este es el enfoque correcto que no deja a los atacantes la oportunidad de elegir una clave común.


Para los amantes de las matemáticas, aquí están los enlaces wiki:
Protocolo Diffie - Hellman_ en curvas elípticas
Firma digital EdDSA


La generación de una clave compartida es bastante estándar: primero, para una nueva conexión, generamos claves efímeras, enviamos un sobre con una clave pública al socket.


El lado opuesto hace lo mismo, pero en un orden diferente: recibe un sobre con una clave pública, genera su propio par y envía la clave pública al socket.


Ahora cada participante tiene las claves efímeras públicas y privadas de otra persona.


Multiplicándolos, obtenemos la misma clave para ambos, que usaremos para cifrar mensajes.


 //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 } 

Cifraremos los mensajes mediante el algoritmo AES de larga data en modo de acoplamiento de bloque (CBC).


Todas estas implementaciones se encuentran fácilmente en la documentación de Golang.


El único refinamiento es el llenado automático del mensaje con cero bytes para la multiplicidad de su longitud a la longitud del bloque de cifrado (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 } 

En 2013, implementó AES (con un modo similar a CBC) para cifrar mensajes en Telegram como parte de un concurso de Pavel Durov.


En ese momento, el protocolo Diffie-Hellman más común se usaba en telegramas para generar una clave efímera.


Y para excluir la carga de conexiones falsas, antes de cada intercambio de claves, los clientes resolvieron el problema de factorización.


GUI


Necesitamos mostrar una lista de pares y una lista de mensajes con ellos, y también responder a nuevos mensajes aumentando el contador al lado del nombre del par.


Aquí sin problemas: ReactJS + websocket.


Los mensajes de socket web son esencialmente sobres únicos, solo que no contienen textos cifrados.


Todos ellos son "herederos" del tipo WsCmd y se serializan en JSON tras la transferencia.


  //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) } /* ... */ 

Entonces, una solicitud HTTP llega a la raíz ("/"), ahora para mostrar el frente, busque en el directorio "front / build" y proporcione index.html


Bueno, la interfaz está hecha, ahora la opción para los usuarios es: ejecutarla en un navegador o en una ventana separada: WebView.


Para la última opción utilizada zserge / webview


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

Para construir una aplicación con él, necesita instalar otro sistema


  sudo apt install libwebkit2gtk-4.0-dev 

Mientras pensaba en la GUI, encontré muchas bibliotecas para GTK, QT, y la interfaz de la consola se vería muy geek, https://github.com/jroimartin/gocui , en mi opinión, una idea muy interesante.


Lanzamiento de Messenger


Instalación de Golang


Por supuesto, primero necesitas instalar go.
Para hacer esto, recomiendo usar las instrucciones golang.org/doc/install .


Instrucciones simplificadas para bash script


Descargar una aplicación en GOPATH


Está tan organizado que todas las bibliotecas e incluso sus proyectos deben estar en el llamado GOPATH.


Por defecto, esto es $ HOME / go. Go le permite extraer la fuente del repositorio público con un simple comando:


  go get github.com/easmith/p2p-messenger 

Ahora, en su $HOME/go/src/github.com/easmith/p2p-messenger aparecerá $HOME/go/src/github.com/easmith/p2p-messenger fuente de la rama maestra


Instalación NPM y montaje frontal


Como escribí anteriormente, nuestra GUI es una aplicación web con un frente en ReactJs, por lo que el frente aún necesita ser ensamblado.


Nodejs + npm: aquí, como siempre.


Por si acaso, aquí están las instrucciones para ubuntu


Ahora comenzamos el montaje frontal como estándar


 cd front npm update npm run build 

¡El frente está listo!


Lanzamiento


Volvamos a la raíz y lancemos la fiesta de nuestro mensajero.


En el inicio, podemos especificar el nombre de nuestro par, puerto, archivo con las direcciones de otros pares y una bandera que indica si se debe iniciar WebView.


De manera predeterminada, $USER@$HOSTNAME se usa como el nombre de $USER@$HOSTNAME y el puerto 35035.


Entonces, comenzamos y chateamos con amigos en la red local.


  go run app.go -name Snowden 

Comentarios sobre la programación de golang


  • Lo más importante que me gustaría señalar: sobre la marcha, inmediatamente resulta implementar lo que pretendía .
    Casi todo lo que necesita está en la biblioteca estándar.
  • Sin embargo, hubo una dificultad cuando comencé el proyecto en un directorio distinto de GOPATH.
    Usé GoLand para escribir código. Y al principio fue vergonzoso formatear automáticamente el código con bibliotecas de importación automática.
  • Hay muchos generadores de código en el IDE , lo que nos permitió centrarnos en el desarrollo en lugar de en el conjunto de códigos.
  • Se acostumbra rápidamente al manejo frecuente de errores, pero se produce un error cuando se da cuenta de que una situación normal es cuando la esencia del error se analiza de acuerdo con su representación de cadena.
     err != io.EOF 
  • Las cosas están un poco mejor con la biblioteca os. Tales construcciones ayudan a comprender la esencia del problema.
     if os.IsNotExist(err) { /* ... */ } 
  • Fuera de la caja, go nos enseña a documentar correctamente el código y escribir pruebas.
    Y hay algunos pero. Hemos descrito la interfaz con el método ToJson() .
    Por lo tanto, el generador de documentación no hereda la descripción de este método para los métodos que lo implementan, por lo que para eliminar advertencias innecesarias, debe copiar la documentación en cada método implementado (proto / mtypes.go).
  • Recientemente me acostumbré al poder de log4j en Java, por lo que no hay suficiente buen registrador en marcha.
    Probablemente valga la pena observar la inmensidad del hermoso registro de github con apéndices y formateadores.
  • Trabajo inusual con matrices.
    Por ejemplo, la concatenación ocurre a través de la función append , y la conversión de una matriz de longitud arbitraria en una matriz de longitud fija mediante copy .
  • switch-case funciona como if-elseif-else , pero este es un enfoque interesante, pero una vez más:
    si queremos el comportamiento habitual de switch-case , debemos poner en fallthrough cada caso.
    También puedes usar goto , pero no, ¡por favor!
  • No existe un operador ternario y, a menudo, esto no es conveniente.

Que sigue


Por lo tanto, se implementa el mensajero punto a punto más simple.


Los conos están abarrotados, además puede mejorar la funcionalidad del usuario: envío de archivos, imágenes, audio, emoticones, etc., etc.


Y no puede inventar su protocolo y usar los Buffers de protocolo de Google,
Conecte blockchain y protéjase del spam utilizando los contratos inteligentes de Ethereum.


En contratos inteligentes, organice chats grupales, canales, un sistema de nombres, avatares y perfiles de usuario.


También es imprescindible ejecutar pares de inicialización, implementar bypass de NAT y enviar mensajes de igual a igual.


Como resultado, obtienes un buen telegrama / teléfono de reemplazo, solo tienes que transferir a todos tus amigos allí =)


Utilidad


Algunos enlaces

En el curso del trabajo en el messenger, encontré páginas interesantes para un desarrollador principiante.
Los comparto contigo:


golang.org/doc/ - documentación del idioma, todo es simple, claro y con ejemplos. La misma documentación se puede ejecutar localmente con el comando


 godoc -HTTP=:6060 

gobyexample.com - una colección de ejemplos simples


golang-book.ru - un buen libro en ruso


github.com/dariubs/GoBooks es una colección de libros sobre Go.


awesome-go.com : una lista de bibliotecas, marcos y aplicaciones interesantes sobre la marcha . La categorización es más o menos, pero la descripción de muchos de ellos es muy escasa, lo que no ayuda a la búsqueda por Ctrl + F

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


All Articles