学习去:编写具有端到端加密的p2p Messenger

另一个P2P Messenger


仅阅读评论和语言文档不足以学习如何在其上编写或多或少有用的应用程序。


确保合并,您需要创建一些有趣的东西,以便将这些开发成果用于其他任务。


ReactJs聊天界面示例


本文面向对围棋语言和对等网络感兴趣的初学者。
对于可以提出合理想法或建设性批评的专业人员。


我在Java,PHP,JS,Python中使用不同程度的沉浸程度已经进行了一段时间的编程。
而且每种编程语言在其领域都是不错的。


Go的主要领域是创建分布式服务微服务。
通常,微服务是执行其高度专业化功能的小型程序。


但是微服务仍应能够相互通信,因此创建微服务的工具应允许轻松,轻松地进行网络连接。
为了测试这一点,我们将编写一个组织去中心化对等网络(Peer-To-Peer)的应用程序,最简单的是p2p Messenger(顺便说一句,这个单词有俄语同义词吗?)。


在代码中,我积极发明自行车并踩踏耙子,以感受戈朗,得到建设性的批评和合理的建议。


我们该怎么办


对等(peer)-Messenger的唯一实例。


我们的使者应该能够:


  • 寻找附近的盛宴
  • 与其他同伴建立联系
  • 加密与对等方的数据交换
  • 接收来自用户的消息
  • 向用户显示消息

为了使任务更加有趣,让我们全部通过一个网络端口进行。


信使的条件方案


如果通过HTTP拉该端口,我们将获得一个React应用程序,该应用程序通过建立Web套接字连接来拉同一个端口。


如果您通过HTTP而不是从本地计算机拉端口,则我们将显示标语。


如果另一个对等方连接到该端口,则使用端到端加密建立永久连接。


确定传入连接的类型


首先,打开端口进行监听,我们将等待新的连接。


net.ListenTCP("tcp", tcpAddr) 

在新连接上,读取前4个字节。


我们获取HTTP动词列表,然后将其4个字节进行比较。


现在,我们确定是否从本地计算机建立了连接,如果没有建立连接,我们将显示一条横幅并挂断。


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

如果连接是本地连接,那么我们将使用与请求相对应的文件进行响应。


然后,我决定自己编写处理程序,尽管我可以使用标准库中提供的处理程序。


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

如果请求路径/ws ,那么我们尝试建立一个websocket连接。


由于我在处理文件请求时组装了自行车,因此我将使用gorilla / websocket库来处理ws连接。


为此,创建MyWriter并在其中实现方法以对应于接口http.ResponseWriterhttp.Hijacker


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

对等检测


为了在局域网中搜索对等体,我们将使用UDP多播。


我们会将带有有关我们自己的信息的数据包发送到多播IP地址。


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

并与多播IP分开侦听所有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) } 

因此,我们宣布自己,并了解其他盛宴的出现。


可以在IP级别上进行组织,甚至在IPv4包官方文档中,也仅将多播数据包作为代码示例给出。


对等交互协议


我们将同伴之间的所有通信都封装在一个信封(信封)中。


在任何信封上总是有一个发送者和一个接收者,为此我们将添加一条命令(他随身携带),一个标识符(到目前为止这是一个随机数,但可以作为内容的哈希值),内容的长度和信封本身的内容-消息或命令参数。


信封字节


该命令(或内容的类型)已成功放置在信封的最开始,并且我们定义了一个4字节的命令列表,该列表与HTTP动词的名称不相交。


在传输过程中,整个信封被序列化为字节数组。


握手


建立连接后,盛宴立即伸出手进行握手,提供其名称,公共密钥和临时公共密钥以生成共享会话密钥。


作为响应,对等方收到一组相似的数据,注册在其列表中找到的对等方,并计算(CalcSharedSecret)公共会话密钥。


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

盛宴交流


握手后,对等方交换其对等方列表=)


为此,将发送带有LIST命令的信封,并在其内容中放置一个JSON对等体列表。
作为回应,我们得到了类似的信封。


我们在新列表中找到,并尝试与每个列表建立联系,握手,交流盛宴等...


用户消息


自定义消息对我们来说是最大的价值,因此我们将对每个连接进行加密和签名。


关于加密


在来自crypto软件包的标准(google)golang库中,实现了许多不同的算法(没有GOST标准)。


我认为最方便的签名是Ed25519曲线。 我们将使用ed25519库对消息进行签名。


从一开始,我就考虑过使用从ed25519获得的密钥对不仅用于签名,还用于生成会话密钥。


但是,用于签名的密钥不适用于计算共享密钥-您仍然需要想到它们:


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

因此,决定生成临时密钥,并且一般来说,这是正确的方法,不会使攻击者有机会获得通用密钥。


对于数学爱好者,以下是Wiki链接:
Diffie协议—椭圆曲线上的Hellman_
数字签名EdDSA


共享密钥的生成是非常标准的:首先,对于新连接,我们生成临时密钥,我们将带有公共密钥的信封发送到套接字。


另一侧的操作相同,但顺序不同:它接收带有公钥的信封,生成自己的对,然后将公钥发送到套接字。


现在,每个参与者都有别人的公共和私人临时密钥。


乘以它们,我们就得到了相同的密钥,我们将用它来加密消息。


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

我们将使用已建立的AES算法以块耦合模式(CBC)对消息进行加密。


所有这些实现都可以在golang文档中轻松找到。


唯一的改进是使用零字节自动填充消息,以确保其长度乘以加密块的长度(16字节)。


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

早在2013年,作为Pavel Durov竞赛的一部分,他实现了AES(与CBC类似的模式)来加密Telegram中的消息。


当时,电报使用了最常见的Diffie-Hellman协议来生成临时密钥。


并且为了从假连接中排除负载,在每次密钥交换之前,客户端解决了分解问题。


图形用户界面


我们需要显示一个对等方的列表和与之相关的消息列表,还需要通过增加对等方名称旁边的计数器来响应新消息。


这里没有麻烦-ReactJS + websocket。


Web套接字消息本质上是唯一的信封,只有它们不包含密文。


它们都是WsCmd类型的“继承人”,并且在传输时以JSON序列化。


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

因此,一个HTTP请求到达根目录(“ /”),现在显示其前端,在“ front / build”目录中查找并给出index.html


界面已经好了,现在用户可以选择:在浏览器或单独的窗口-WebView中运行它。


对于最后使用的选项zserge / webview


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

要使用它构建应用程序,您需要安装另一个系统


  sudo apt install libwebkit2gtk-4.0-dev 

在考虑GUI的过程中,我发现许多用于GTK,QT的库,并且控制台界面看起来非常怪异-我认为这是一个非常有趣的主意-https: //github.com/jroimartin/gocui


信使发射


Golang安装


当然,您首先需要安装go。
为此,我强烈建议您使用golang.org/doc/install说明。


bash脚本的简化说明


在GOPATH中下载应用程序


如此安排,所有库甚至您的项目都应位于所谓的GOPATH中。


默认情况下,这是$ HOME / go。 Go允许您使用简单的命令从公共存储库中提取源代码:


  go get github.com/easmith/p2p-messenger 

现在,在您的$HOME/go/src/github.com/easmith/p2p-messenger将显示master分支$HOME/go/src/github.com/easmith/p2p-messenger


Npm安装和前部组装


就像我在上面写的那样,我们的GUI是一个Web应用程序,在ReactJs上有一个前端,因此前端仍然需要组装。


Nodejs + npm-像往常一样。


以防万一,这是Ubuntu说明


现在我们以标准的方式启动前部组件


 cd front npm update npm run build 

前面准备好了!


发射


让我们回到根源,启动我们的使者盛宴。


在启动时,我们可以指定对等体的名称,端口,文件以及其他对等体的地址以及一个标志,指示是否启动WebView。


缺省情况下, $USER@$HOSTNAME用作对$USER@$HOSTNAME名称和端口35035。


因此,我们开始与本地网络上的朋友聊天。


  go run app.go -name Snowden 

有关Golang编程的反馈


  • 我想指出的最重要的事情是: 在执行过程中,它立即可以实现我的预期
    您几乎需要的所有内容都在标准库中。
  • 但是,当我在GOPATH以外的目录中启动该项目时遇到了困难。
    我用GoLand编写代码。 首先,使用自动导入库自动格式化代码很尴尬。
  • IDE中有很多代码生成器 ,这些代码生成器使我们能够专注于开发而不是代码集。
  • 您很快就习惯了频繁的错误处理,但是当您意识到正常情况是根据错误的字符串表示来分析错误的本质时,就会遇到麻烦。
     err != io.EOF 
  • 使用os库,情况会好一些。 这样的构造有助于理解问题的实质。
     if os.IsNotExist(err) { /* ... */ } 
  • 开箱即用,go可以教我们正确编写代码和编写测试。
    还有一些。 我们已经使用ToJson()方法描述了该接口。
    因此,文档生成器不会继承实现此方法的方法的描述,因此,为了消除不必要的警告,您必须将文档复制到每个已实现的方法(proto / mtypes.go)中。
  • 最近,我已经习惯了Java中log4j的功能,因此没有足够好的logger。
    可能值得一看的是使用添加程序和格式化程序的github美丽日志记录的广阔之处。
  • 数组异常工作。
    例如,串联通过append函数发生,并且通过copy任意长度的数组转换为固定长度的数组。
  • switch-case工作方式类似于if-elseif-else但这是一种有趣的方法,但又需要面对面:
    如果我们想要常规的switch-case行为,则需要对每种情况进行检查。
    您也可以使用goto ,但是请不要使用!
  • 没有三元运算符,通常这不方便。

接下来是什么?


因此,实现了最简单的对等Messenger。


锥体塞满了,进一步可以改善用户功能:发送文件,图片,音频,表情符号等。


而且您不能发明协议,而使用Google协议缓冲区,
使用以太坊智能合约连接区块链并保护自己免受垃圾邮件的侵害。


在智能合约上,组织群聊,频道,名称系统,头像和用户个人资料。


还必须运行种子对等方,实现NAT绕过,并在对等方之间发送消息。


结果,您得到了一个很好的替代电报/电话,您只需要将所有朋友转移到那里=)


有用性


一些链接

在Messenger的工作过程中,我发现了一些对于初学者go开发人员来说很有趣的页面。
我与您分享:


golang.org/doc/-语言文档,所有内容都很简单,清晰并带有示例。 可以使用以下命令在本地运行相同的文档


 godoc -HTTP=:6060 

gobyexample.com-简单示例的集合


golang - book.ru-俄语中的一本好书


github.com/dariubs/GoBooks是有关Go的书籍的集合。


awesome - go.com-有趣的库,框架和应用程序列表。 分类或多或少,但其中许多描述非常稀缺,这不利于通过Ctrl + F进行搜索

Source: https://habr.com/ru/post/zh-CN437686/


All Articles