Learning go: Schreiben eines P2P-Messenger mit End-to-End-Verschlüsselung

Noch ein P2P Messenger


Das Lesen von Rezensionen und Sprachdokumentationen reicht nicht aus, um zu lernen, wie man mehr oder weniger nützliche Anwendungen darauf schreibt.


Stellen Sie sicher, dass Sie etwas Interessantes erstellen, damit die Entwicklungen für andere Aufgaben verwendet werden können.


Beispiel für die Chat-Benutzeroberfläche von ReactJs


Dieser Artikel richtet sich an Anfänger, die sich für die Go-Sprache und Peer-to-Peer-Netzwerke interessieren.
Und für Profis, die vernünftige Ideen anbieten oder konstruktiv kritisieren können.


Ich programmiere seit einiger Zeit mit unterschiedlichem Eintauchen in Java, PHP, JS, Python.
Und jede Programmiersprache ist auf ihrem Gebiet gut.


Der Hauptbereich für Go ist die Schaffung verteilter Dienste, Microservices.
Meistens ist ein Microservice ein kleines Programm, das seine hochspezialisierte Funktionalität ausführt.


Microservices sollten jedoch weiterhin in der Lage sein, miteinander zu kommunizieren, sodass das Tool zum Erstellen von Microservices eine einfache und schmerzlose Vernetzung ermöglichen sollte.
Um dies zu testen, werden wir eine Anwendung schreiben, die ein dezentrales Netzwerk von Peers (Peer-To-Peer) organisiert. Am einfachsten ist ein P2P-Messenger (gibt es übrigens ein russisches Synonym für dieses Wort?).


Im Code erfinde ich aktiv Fahrräder und trete auf den Rechen, um mich wie ein Golang zu fühlen, konstruktive Kritik und rationale Vorschläge zu erhalten.


Was machen wir?


Peer (Peer) - eine einzigartige Instanz des Messenger.


Unser Bote sollte in der Lage sein:


  • Finden Sie in der Nähe Feste
  • Stellen Sie eine Verbindung mit anderen Kollegen her
  • Verschlüsseln Sie den Datenaustausch mit Peers
  • Nachrichten vom Benutzer empfangen
  • Nachrichten dem Benutzer anzeigen

Um die Aufgabe ein wenig interessanter zu gestalten, lassen Sie uns alles über einen Netzwerkport laufen.


Das bedingte Schema des Boten


Wenn Sie diesen Port über HTTP ziehen, erhalten wir eine React-Anwendung, die denselben Port durch Herstellen einer Web-Socket-Verbindung abruft.


Wenn Sie den Port über HTTP nicht vom lokalen Computer abrufen, wird das Banner angezeigt.


Wenn ein anderer Peer mit diesem Port verbunden ist, wird eine permanente Verbindung mit End-to-End-Verschlüsselung hergestellt.


Bestimmen Sie den Typ der eingehenden Verbindung


Öffnen Sie zuerst den Port zum Abhören und wir warten auf neue Verbindungen.


net.ListenTCP("tcp", tcpAddr) 

Lesen Sie bei der neuen Verbindung die ersten 4 Bytes.


Wir nehmen die Liste der HTTP-Verben und vergleichen unsere 4 Bytes damit.


Jetzt stellen wir fest, ob eine Verbindung vom lokalen Computer hergestellt wird, und wenn nicht, antworten wir mit einem Banner und legen auf.


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

Wenn die Verbindung lokal ist, antworten wir mit der Datei, die der Anfrage entspricht.


Dann habe ich beschlossen, die Verarbeitung selbst zu schreiben, obwohl ich den in der Standardbibliothek verfügbaren Handler verwenden konnte.


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

Wenn der Pfad /ws angefordert wird, versuchen wir, eine Websocket-Verbindung herzustellen.


Da ich das Fahrrad bei der Verarbeitung von Dateianfragen zusammengebaut habe, werde ich die ws-Verbindung mithilfe der Gorilla / Websocket-Bibliothek verarbeiten .


Erstellen Sie dazu MyWriter und implementieren Sie darin Methoden, die den Schnittstellen http.ResponseWriter und http.Hijacker .


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

Peer-Erkennung


Um in einem lokalen Netzwerk nach Peers zu suchen, verwenden wir UDP-Multicast.


Wir senden Pakete mit Informationen über uns an die Multicast-IP-Adresse.


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

Und hören Sie alle UDP-Pakete getrennt von Multicast-IP ab.


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

So erklären wir uns und lernen über das Erscheinen anderer Feste.


Es wäre möglich, dies auf IP-Ebene zu organisieren, und selbst in der offiziellen Dokumentation des IPv4-Pakets wird nur das Multicast-Datenpaket als Beispiel für Code angegeben.


Peer-Interaktionsprotokoll


Wir werden die gesamte Kommunikation zwischen Kollegen in einen Umschlag (Umschlag) packen.


Auf jedem Umschlag gibt es immer einen Absender und einen Empfänger. Dazu fügen wir einen Befehl (den er mit sich führt), eine Kennung (soweit dies eine Zufallszahl ist, aber als Hash von Inhalten erfolgen kann), die Länge des Inhalts und den Inhalt des Umschlags selbst hinzu - eine Nachricht oder Befehlsparameter.


Umschlagbytes


Der Befehl (oder die Art des Inhalts) wird erfolgreich ganz am Anfang des Umschlags platziert, und wir definieren eine Liste von Befehlen mit 4 Bytes, die sich nicht mit den Namen der HTTP-Verben überschneiden.


Die gesamte Hüllkurve während der Übertragung wird in ein Array von Bytes serialisiert.


Handschlag


Wenn die Verbindung hergestellt ist, greift das Fest sofort nach einem Handschlag und gibt seinen Namen, seinen öffentlichen Schlüssel und seinen kurzlebigen öffentlichen Schlüssel an, um einen gemeinsamen Sitzungsschlüssel zu generieren.


Als Antwort empfängt der Peer einen ähnlichen Datensatz, registriert den in seiner Liste gefundenen Peer und berechnet (CalcSharedSecret) den gemeinsamen Sitzungsschlüssel.


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

Festtausch


Nach einem Handschlag tauschen Peers ihre Peer-Listen aus =)


Zu diesem Zweck wird ein Umschlag mit dem Befehl LIST gesendet und eine JSON-Liste von Peers in den Inhalt eingefügt.
Als Antwort erhalten wir einen ähnlichen Umschlag.


Wir finden in den Listen der neuen und mit jedem von ihnen versuchen wir uns zu verbinden, Hände zu schütteln, Feste auszutauschen und so weiter ...


User Messaging


Benutzerdefinierte Nachrichten sind für uns von größtem Wert, daher verschlüsseln und signieren wir jede Verbindung.


Über Verschlüsselung


In den Standard-Golang-Bibliotheken (Google) aus dem Crypto-Paket sind viele verschiedene Algorithmen implementiert (es gibt keine GOST-Standards).


Am bequemsten für Signaturen ist meiner Meinung nach die Ed25519-Kurve. Wir werden die Bibliothek ed25519 verwenden, um Nachrichten zu signieren.


Ganz am Anfang habe ich darüber nachgedacht, ein Schlüsselpaar aus ed25519 nicht nur zum Signieren, sondern auch zum Generieren eines Sitzungsschlüssels zu verwenden.


Die Schlüssel zum Signieren gelten jedoch nicht für die Berechnung des gemeinsam genutzten Schlüssels. Sie müssen sie dennoch beschwören:


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

Aus diesem Grund wurde beschlossen, kurzlebige Schlüssel zu generieren. Im Allgemeinen ist dies der richtige Ansatz, bei dem Angreifer keine Chance haben, einen gemeinsamen Schlüssel zu finden.


Für Mathematikliebhaber gibt es hier die Wiki-Links:
Diffie- Protokoll - Hellman_ auf elliptischen Kurven
Digitale Signatur EdDSA


Die Generierung eines gemeinsam genutzten Schlüssels ist Standard: Für eine neue Verbindung generieren wir zunächst kurzlebige Schlüssel und senden einen Umschlag mit einem öffentlichen Schlüssel an den Socket.


Die gegenüberliegende Seite macht dasselbe, jedoch in einer anderen Reihenfolge: Sie empfängt einen Umschlag mit einem öffentlichen Schlüssel, generiert ein eigenes Paar und sendet den öffentlichen Schlüssel an den Socket.


Jetzt hat jeder Teilnehmer die öffentlichen und privaten kurzlebigen Schlüssel eines anderen.


Wenn wir sie multiplizieren, erhalten wir für beide den gleichen Schlüssel, mit dem wir Nachrichten verschlüsseln.


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

Wir werden Nachrichten mit dem seit langem etablierten AES-Algorithmus im Block Coupling Mode (CBC) verschlüsseln.


Alle diese Implementierungen sind leicht in der Golang-Dokumentation zu finden.


Die einzige Verfeinerung besteht darin, die Nachricht automatisch mit Null Bytes für die Vielzahl ihrer Länge mit der Länge des Verschlüsselungsblocks (16 Bytes) zu füllen.


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

Bereits 2013 implementierte er im Rahmen eines Wettbewerbs von Pavel Durov AES (mit einem ähnlichen Modus wie CBC) zum Verschlüsseln von Nachrichten in Telegram.


Zu dieser Zeit wurde das häufigste Diffie-Hellman-Protokoll in Telegrammen verwendet, um einen kurzlebigen Schlüssel zu generieren.


Und um die Last von gefälschten Verbindungen auszuschließen, lösten die Clients vor jedem Schlüsselaustausch das Faktorisierungsproblem.


GUI


Wir müssen eine Liste von Peers und eine Liste von Nachrichten mit ihnen anzeigen und auch auf neue Nachrichten reagieren, indem wir den Zähler neben dem Namen des Peers erhöhen.


Hier ohne Probleme - ReactJS + Websocket.


Web-Socket-Nachrichten sind im Wesentlichen eindeutige Umschläge, nur enthalten sie keine Chiffretexte.


Alle von ihnen sind "Erben" vom Typ WsCmd und werden bei der Übertragung in JSON serialisiert.


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

Eine HTTP-Anfrage kommt also zum Stammverzeichnis ("/"). Um nun die Vorderseite anzuzeigen, schauen Sie im Verzeichnis "front / build" nach und geben Sie index.html an


Nun, die Benutzeroberfläche ist aufgebaut, jetzt haben die Benutzer die Wahl: Führen Sie sie in einem Browser oder in einem separaten Fenster aus - WebView.


Für die letzte Option wurde zserge / webview verwendet


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

Um eine Anwendung damit zu erstellen, müssen Sie ein anderes System installieren


  sudo apt install libwebkit2gtk-4.0-dev 

Während ich über die GUI nachdachte, fand ich viele Bibliotheken für GTK, QT und die Konsolenschnittstelle würde sehr geeky aussehen - https://github.com/jroimartin/gocui - meiner Meinung nach eine sehr interessante Idee.


Messenger-Start


Golang Installation


Natürlich müssen Sie zuerst go installieren.
Zu diesem Zweck empfehle ich dringend, die Anweisung golang.org/doc/install zu verwenden.


Vereinfachte Anweisungen zum Bash-Skript


Laden Sie eine Anwendung in GOPATH herunter


Es ist so angeordnet, dass sich alle Bibliotheken und sogar Ihre Projekte im sogenannten GOPATH befinden sollten.


Standardmäßig ist dies $ HOME / go. Mit Go können Sie die Quelle mit einem einfachen Befehl aus dem öffentlichen Repository abrufen:


  go get github.com/easmith/p2p-messenger 

In Ihrem $HOME/go/src/github.com/easmith/p2p-messenger Quelle aus dem Hauptzweig $HOME/go/src/github.com/easmith/p2p-messenger


Npm Installation und Frontmontage


Wie ich oben geschrieben habe, ist unsere GUI eine Webanwendung mit einer Front auf ReactJs, daher muss die Front noch zusammengebaut werden.


Nodejs + npm - hier wie gewohnt.


Nur für den Fall, hier ist die Anleitung für Ubuntu


Jetzt starten wir die Frontmontage als Standard


 cd front npm update npm run build 

Die Front ist fertig!


Starten


Gehen wir zurück zur Wurzel und starten das Fest unseres Boten.


Beim Start können wir den Namen unseres Peers, Ports, die Datei mit den Adressen anderer Peers und ein Flag angeben, das angibt, ob WebView gestartet werden soll.


Standardmäßig wird $USER@$HOSTNAME als $USER@$HOSTNAME Name und Port 35035 verwendet.


Also starten wir und chatten mit Freunden im lokalen Netzwerk.


  go run app.go -name Snowden 

Feedback zur Golang-Programmierung


  • Das Wichtigste, was ich beachten möchte: Unterwegs stellt sich sofort heraus, was ich beabsichtigt habe .
    Fast alles, was Sie brauchen, befindet sich in der Standardbibliothek.
  • Es gab jedoch eine Schwierigkeit, als ich das Projekt in einem anderen Verzeichnis als GOPATH startete.
    Ich habe GoLand verwendet, um Code zu schreiben. Und zunächst war es peinlich, den Code automatisch mit automatisch importierten Bibliotheken zu formatieren.
  • Es gibt viele Codegeneratoren in der IDE , die es uns ermöglichten, uns eher auf die Entwicklung als auf den Codesatz zu konzentrieren.
  • Sie gewöhnen sich schnell an häufige Fehlerbehandlungen, aber ein Handgesicht tritt auf, wenn Sie feststellen, dass eine normale Situation für unterwegs darin besteht, das Wesen des Fehlers anhand seiner Zeichenfolgendarstellung zu analysieren.
     err != io.EOF 
  • Mit der OS-Bibliothek sieht es etwas besser aus. Solche Konstruktionen helfen, das Wesentliche des Problems zu verstehen.
     if os.IsNotExist(err) { /* ... */ } 
  • Nach dem Auspacken lernen wir, Code richtig zu dokumentieren und Tests zu schreiben.
    Und es gibt einige aber. Wir haben die Schnittstelle mit der ToJson() -Methode beschrieben.
    Daher dokumentiert der Dokumentationsgenerator die Beschreibung dieser Methode nicht an Methoden, die sie implementieren. Um unnötige Warnungen zu entfernen, müssen Sie die Dokumentation in jede implementierte Methode (proto / mtypes.go) kopieren.
  • Vor kurzem habe ich mich an die Leistung von log4j in Java gewöhnt, daher gibt es nicht genug guten Logger in go.
    Wahrscheinlich einen Blick wert auf die Weite des Github, schöne Protokollierung mit Appendern und Formatierern.
  • Ungewöhnliche Arbeit mit Arrays.
    Beispielsweise erfolgt die Verkettung durch die append Funktion und die Umwandlung eines Arrays beliebiger Länge in ein Array fester Länge durch copy .
  • switch-case funktioniert wie if-elseif-else - aber dies ist ein interessanter Ansatz, aber wieder Handfläche:
    Wenn wir das übliche switch-case Verhalten wünschen, müssen wir jeden Fall fallthrough .
    Sie können auch goto , aber bitte nicht!
  • Es gibt keinen ternären Operator und oft ist dies nicht bequem.

Was weiter?


So wird der einfachste Peer-To-Peer-Messenger implementiert.


Die Zapfen sind voll, und Sie können die Benutzerfunktionalität weiter verbessern: Senden von Dateien, Bildern, Audio, Emoticons usw. usw.


Und Sie können Ihr Protokoll nicht erfinden und die Google-Protokollpuffer verwenden.
Verbinden Sie die Blockchain und schützen Sie sich mit intelligenten Verträgen von Ethereum vor Spam.


Organisieren Sie bei intelligenten Verträgen Gruppenchats, Kanäle, ein Namenssystem, Avatare und Benutzerprofile.


Es ist auch wichtig, Seed-Peers auszuführen, NAT-Bypass zu implementieren und Nachrichten von Peer zu Peer zu senden.


Als Ergebnis erhalten Sie ein gutes Ersatztelegramm / Telefon, Sie müssen nur alle Ihre Freunde dorthin übertragen =)


Nützlichkeit


Einige Links

Während der Arbeit am Messenger fand ich Seiten interessant für einen Anfänger.
Ich teile sie mit Ihnen:


golang.org/doc/ - Sprachdokumentation, alles ist einfach, klar und mit Beispielen. Dieselbe Dokumentation kann lokal mit dem Befehl ausgeführt werden


 godoc -HTTP=:6060 

gobyexample.com - eine Sammlung einfacher Beispiele


golang-book.ru - ein gutes Buch auf Russisch


github.com/dariubs/GoBooks ist eine Sammlung von Büchern über Go.


awesome-go.com - Eine Liste interessanter Bibliotheken, Frameworks und Anwendungen für unterwegs. Die Kategorisierung ist mehr oder weniger, aber die Beschreibung vieler von ihnen ist sehr selten, was die Suche nach Strg + F nicht erleichtert

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


All Articles