Learning Go: écrire un messager p2p avec un cryptage de bout en bout

Encore un autre P2P Messenger


La lecture des revues et de la documentation linguistique ne suffit pas pour apprendre à y écrire des applications plus ou moins utiles.


Assurez-vous de consolider, vous devez créer quelque chose d'intéressant afin que les développements puissent être utilisés dans d'autres tâches.


Exemple d'interface utilisateur de chat ReactJs


Cet article est destiné aux débutants intéressés par la langue go et les réseaux peer-to-peer.
Et pour les professionnels qui peuvent proposer des idées raisonnables ou critiquer de manière constructive.


Je programme depuis un certain temps avec différents degrés d'immersion en java, php, js, python.
Et chaque langage de programmation est bon dans son domaine.


Le domaine principal de Go est la création de services distribués, de microservices.
Le plus souvent, un microservice est un petit programme qui exécute ses fonctionnalités hautement spécialisées.


Mais les microservices devraient toujours pouvoir communiquer entre eux, donc l'outil de création de microservices devrait permettre une mise en réseau facile et indolore.
Pour tester cela, nous allons écrire une application organisant un réseau décentralisé de pairs (Peer-To-Peer), le plus simple est un messager p2p (au fait, existe-t-il un synonyme russe pour ce mot?).


Dans le code, j'invente activement des vélos et monte sur le râteau pour sentir le golang, obtenir des critiques constructives et des suggestions rationnelles.


On fait quoi


Peer (peer) - une instance unique du messager.


Notre messager devrait pouvoir:


  • Trouver des fêtes à proximité
  • Établir une connexion avec d'autres pairs
  • Crypter l'échange de données avec des pairs
  • Recevoir des messages de l'utilisateur
  • Afficher les messages à l'utilisateur

Pour rendre la tâche un peu plus intéressante, faisons-la passer par un seul port réseau.


Le schéma conditionnel du messager


Si vous tirez ce port sur HTTP, nous obtenons une application React qui tire le même port en établissant une connexion de socket Web.


Si vous tirez le port via HTTP et non depuis la machine locale, nous affichons la bannière.


Si un autre homologue est connecté à ce port, une connexion permanente est établie avec un chiffrement de bout en bout.


Déterminer le type de connexion entrante


Tout d'abord, ouvrez le port d'écoute et nous attendrons de nouvelles connexions.


net.ListenTCP("tcp", tcpAddr) 

Sur la nouvelle connexion, lisez les 4 premiers octets.


Nous prenons la liste des verbes HTTP et comparons nos 4 octets avec elle.


Maintenant, nous déterminons si une connexion est établie à partir de la machine locale, et sinon, nous répondons avec une bannière et raccrochons.


  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 connexion est locale, nous répondons avec le fichier correspondant à la demande.


J'ai alors décidé d'écrire le traitement moi-même, même si je pouvais utiliser le gestionnaire disponible dans la bibliothèque standard.


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

Si le chemin /ws demandé, alors nous essayons d'établir une connexion websocket.


Depuis que j'ai assemblé le vélo dans le traitement des demandes de fichiers, je vais faire le traitement de la connexion ws en utilisant la bibliothèque gorilla / websocket .


Pour ce faire, créez MyWriter et implémentez-y des méthodes correspondant aux interfaces http.ResponseWriter et http.Hijacker .


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

Détection de pairs


Pour rechercher des homologues dans un réseau local, nous utiliserons la multidiffusion UDP.


Nous enverrons des paquets contenant des informations sur nous-mêmes à l'adresse IP de multidiffusion.


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

Et écoutez séparément de Multicast IP pour tous les paquets 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) } 

Ainsi nous nous déclarons et apprenons l'apparition d'autres fêtes.


Il serait possible d'organiser cela au niveau IP, et même dans la documentation officielle du package IPv4, seul le paquet de données de multidiffusion est donné comme exemple de code.


Protocole d'interaction entre pairs


Nous emballerons toutes les communications entre pairs dans une enveloppe (enveloppe).


Sur toute enveloppe, il y a toujours un expéditeur et un destinataire, à cela nous ajouterons une commande (qu'il emporte avec lui), un identifiant (jusqu'à présent, c'est un nombre aléatoire, mais peut être fait comme un hachage de contenu), la longueur du contenu et le contenu de l'enveloppe elle-même - un message ou des paramètres de commande.


Octets d'enveloppe


La commande, (ou le type de contenu) est placée avec succès au tout début de l'enveloppe et nous définissons une liste de commandes de 4 octets qui ne se croisent pas avec les noms des verbes HTTP.


L'enveloppe entière pendant la transmission est sérialisée en un tableau d'octets.


Poignée de main


Lorsque la connexion est établie, le festin tend immédiatement la main pour une poignée de main, donnant son nom, sa clé publique et sa clé publique éphémère pour générer une clé de session partagée.


En réponse, l'homologue reçoit un ensemble similaire de données, enregistre l'homologue trouvé dans sa liste et calcule (CalcSharedSecret) la clé de session commune.


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

Échange de fête


Après une poignée de main, les pairs échangent leurs listes de pairs =)


Pour ce faire, une enveloppe contenant la commande LIST est envoyée et une liste JSON de pairs est placée dans son contenu.
En réponse, nous obtenons une enveloppe similaire.


On en retrouve dans les listes de nouveaux et avec chacun d'eux on essaie de se connecter, se serrer la main, échanger des fêtes etc.


Messagerie utilisateur


Les messages personnalisés sont de la plus grande valeur pour nous, nous allons donc crypter et signer chaque connexion.


À propos du chiffrement


Dans les bibliothèques golang standard (google) du paquet crypto, de nombreux algorithmes différents sont implémentés (il n'y a pas de normes GOST).


Le plus pratique pour les signatures, je pense, est la courbe Ed25519. Nous utiliserons la bibliothèque ed25519 pour signer des messages.


Au tout début, j'ai pensé à utiliser une paire de clés obtenue à partir de ed25519 non seulement pour signer, mais aussi pour générer une clé de session.


Cependant, les clés de signature ne sont pas applicables pour le calcul de la clé partagée - vous devez toujours les conjurer:


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

Par conséquent, il a été décidé de générer des clés éphémères, et d'une manière générale, c'est la bonne approche qui ne laisse pas aux attaquants la possibilité de récupérer une clé commune.


Pour les amateurs de mathématiques, voici les liens wiki:
Protocole Diffie - Hellman_ sur les courbes elliptiques
Signature numérique EdDSA


La génération d'une clé partagée est assez standard: d'abord, pour une nouvelle connexion, nous générons des clés éphémères, nous envoyons une enveloppe avec une clé publique au socket.


Le côté opposé fait de même, mais dans un ordre différent: il reçoit une enveloppe avec une clé publique, génère sa propre paire et envoie la clé publique au socket.


Désormais, chaque participant dispose des clés éphémères publiques et privées de quelqu'un d'autre.


En les multipliant, nous obtenons la même clé pour les deux, que nous utiliserons pour crypter les messages.


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

Nous crypterons les messages par l'algorithme AES établi depuis longtemps en mode de couplage de blocs (CBC).


Toutes ces implémentations sont faciles à trouver dans la documentation de golang.


Le seul raffinement consiste à remplir automatiquement le message avec zéro octet pour la multiplicité de sa longueur à la longueur du bloc de chiffrement (16 octets).


  //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, il a implémenté AES (avec un mode similaire à CBC) pour crypter les messages dans Telegram dans le cadre d'un concours de Pavel Durov.


À cette époque, le protocole Diffie-Hellman le plus courant était utilisé dans les télégrammes pour générer une clé éphémère.


Et afin d'exclure la charge des fausses connexions, avant chaque échange de clés, les clients ont résolu le problème de factorisation.


GUI


Nous devons afficher une liste de pairs et une liste de messages avec eux, et également répondre aux nouveaux messages en augmentant le compteur à côté du nom du pair.


Ici sans problèmes - ReactJS + websocket.


Les messages de socket Web sont essentiellement des enveloppes uniques, mais ils ne contiennent pas de texte chiffré.


Tous sont des "héritiers" de type WsCmd et sont sérialisés en JSON lors du transfert.


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

Donc, une requête HTTP arrive à la racine ("/"), maintenant pour afficher le front, regardez dans le répertoire "front / build" et donnez index.html


Eh bien l'interface est constituée, maintenant le choix pour les utilisateurs est: l'exécuter dans un navigateur ou dans une fenêtre séparée - WebView.


Pour la dernière option utilisée zserge / webview


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

Pour créer une application avec celle-ci, vous devez installer un autre système


  sudo apt install libwebkit2gtk-4.0-dev 

Au cours de la réflexion sur l'interface graphique, j'ai trouvé de nombreuses bibliothèques pour GTK, QT, et l'interface de la console aurait l'air très geek - https://github.com/jroimartin/gocui - à mon avis, une idée très intéressante.


Lancement de Messenger


Installation de Golang


Bien sûr, vous devez d'abord installer go.
Pour ce faire, je recommande fortement d'utiliser l'instruction golang.org/doc/install .


Instructions simplifiées pour bash script


Téléchargez une application dans GOPATH


Il est tellement arrangé que toutes les bibliothèques et même vos projets doivent être dans le soi-disant GOPATH.


Par défaut, c'est $ HOME / go. Go vous permet d'extraire la source du référentiel public avec une simple commande:


  go get github.com/easmith/p2p-messenger 

Maintenant, dans votre $HOME/go/src/github.com/easmith/p2p-messenger source de la branche principale apparaîtra


Installation et montage avant du NPM


Comme je l'ai écrit ci-dessus, notre interface graphique est une application Web avec un front sur ReactJs, donc le front doit encore être assemblé.


Nodejs + npm - ici comme d'habitude.


Juste au cas où, voici les instructions pour Ubuntu


Maintenant, nous commençons l'assemblage avant en standard


 cd front npm update npm run build 

L'avant est prêt!


Lancement


Revenons à la racine et lançons la fête de notre messager.


Au démarrage, nous pouvons spécifier le nom de notre homologue, le port, le fichier avec les adresses des autres homologues et un indicateur indiquant s'il faut lancer WebView.


Par défaut, $USER@$HOSTNAME est utilisé comme nom d' $USER@$HOSTNAME et port 35035.


Donc, nous commençons et discutons avec des amis sur le réseau local.


  go run app.go -name Snowden 

Commentaires sur la programmation de Golang


  • La chose la plus importante que je voudrais noter: à l'aller, il s'avère immédiatement mettre en œuvre ce que je voulais .
    Presque tout ce dont vous avez besoin se trouve dans la bibliothèque standard.
  • Cependant, il y avait une difficulté lorsque j'ai commencé le projet dans un répertoire autre que GOPATH.
    J'ai utilisé GoLand pour écrire du code. Et au début, il était gênant de formater automatiquement le code avec des bibliothèques d'importation automatique.
  • Il y a beaucoup de générateurs de code dans l' IDE , ce qui nous a permis de nous concentrer sur le développement plutôt que sur le jeu de code.
  • Vous vous habituez rapidement à la gestion fréquente des erreurs, mais un visage se produit lorsque vous réalisez que pour aller, une situation normale est lorsque l'essence de l'erreur est analysée en fonction de sa représentation sous forme de chaîne.
     err != io.EOF 
  • Les choses vont un peu mieux avec la bibliothèque os. De telles constructions aident à comprendre l'essence du problème.
     if os.IsNotExist(err) { /* ... */ } 
  • Hors de la boîte, go nous apprend à documenter correctement le code et à écrire des tests.
    Et il y en a mais. Nous avons décrit l'interface avec la méthode ToJson() .
    Ainsi, le générateur de documentation n'hérite pas de la description de cette méthode pour les méthodes qui l'implémentent, donc pour supprimer les avertissements inutiles, vous devez copier la documentation dans chaque méthode implémentée (proto / mtypes.go).
  • Récemment, je me suis habitué à la puissance de log4j en java, donc il n'y a pas assez de bon enregistreur en route.
    Il vaut probablement la peine d'étudier l'immensité du github avec une belle journalisation avec des appender et des formateurs.
  • Travail inhabituel avec des tableaux.
    Par exemple, la concaténation se produit via la fonction append et la conversion d'un tableau de longueur arbitraire en un tableau de longueur fixe par copy .
  • switch-case fonctionne comme if-elseif-else - mais c'est une approche intéressante, mais encore une fois face à la main:
    si nous voulons le comportement de switch-case habituel, nous devons mettre en fallthrough chaque cas.
    Vous pouvez également utiliser goto , mais ne le faisons pas, s'il vous plaît!
  • Il n'y a pas d'opérateur ternaire et souvent ce n'est pas pratique.

Et ensuite?


Ainsi, le messager Peer-To-Peer le plus simple est implémenté.


Les cônes sont bondés, vous pouvez en outre améliorer les fonctionnalités de l'utilisateur: envoi de fichiers, d'images, d'audio, d'émoticônes, etc., etc.


Et vous ne pouvez pas inventer votre protocole et utiliser les tampons de protocole Google,
Connectez la chaîne de blocs et protégez-vous contre le spam à l'aide des contrats intelligents Ethereum.


Sur les contrats intelligents, organisez des discussions de groupe, des canaux, un système de noms, des avatars et des profils utilisateur.


Il est également impératif d'exécuter des homologues de départ, de mettre en œuvre un contournement NAT et d'envoyer des messages d'homologue à homologue.


En conséquence, vous obtenez un bon télégramme / téléphone de remplacement, il vous suffit d'y transférer tous vos amis =)


Utilité


Quelques liens

Au cours des travaux sur le messager, j'ai trouvé des pages intéressantes pour un développeur débutant.
Je les partage avec vous:


golang.org/doc/ - documentation en langue, tout est simple, clair et avec des exemples. La même documentation peut être exécutée localement avec la commande


 godoc -HTTP=:6060 

gobyexample.com - une collection d'exemples simples


golang-book.ru - un bon livre en russe


github.com/dariubs/GoBooks est une collection de livres sur Go.


awesome-go.com - Une liste de bibliothèques, de frameworks et d'applications intéressantes à emporter . La catégorisation est plus ou moins, mais la description de beaucoup d'entre eux est très rare, ce qui n'aide pas la recherche par Ctrl + F

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


All Articles