最近和几个同学刚做完一个为期大半年的分布式聊天系统项目,于是有一些些心得想要记录下来。可能会有些又臭又长,想大致了解的话重点看消息加密设计和实现就好了。最后附上项目链接。

设计

聊天系统就是像我们常用的QQ、微信这些。而我们的分布式聊天系统主要包括三个部分:数据库服务端、Web客户端和Android客户端。理想状态下,服务端和Web客户端各部署在独立的两台服务器上,Android客户端则被打包成app安装于用户手机上。但是为了方便合作和互相监管,我们将这三部分分别写作了项目下的三个模块。
我们主要的通讯思路是服务端和客户端之间通过websocket协议传输json文件,文件包含对数据库读写的请求。具体可以查看我们项目的Wiki,这里就不赘述了。因为我主要负责的是Web客户端的实现,所以这篇文章主要也会详讲这一部分的结构和遇到的问题。如果真的对我们整个系统感兴趣的话,项目库里面也是有具体的报告,在final_report_latex下。

需求及功能

已实现功能

  • 注册登录
    注册时检查sql注入,要求密码包含三种类型字符等,如不符合要求将拒绝注册;发送注册请求前对密码进行md5哈希计算,保证服务端无法获取原密码;由于本项目消息加密的实现使得私钥只储存于注册设备,所以目前登录只允许注册账号的设备登录该账号。Web客户端发起登录请求前将首先检查本地内存localStorage是否存在相关私钥(私钥不参与登录验证过程,但是和消息加解密有关)。

  • 用户搜索及添加
    本项目发起聊天不需要经过对方同意,更类似于陌生人聊天室,可以通过搜索功能找到符合搜索关键词模式的所有用户,并生成新的聊天窗口,聊天窗口不会保留至下次登录,聊天记录也不会保存至本地。

  • 双方消息加密
    用户聊天内容的私密性要进行保护。和国内常见聊天软件不一样的是,我们的项目更强调端对端的消息加密和用户的隐私保护,也即服务端无法获取用户实际聊天内容,只储存加密后的消息,并且不直接储存用户密钥。

    我们采用AES+Diffie-Hellman的结合实现信息加密。基本设计是:首先每个用户在新注册时都会本地生成新的密钥对(符合Diffie-Hellman交换规则),并将公钥进行Base64编码发送至服务端保存,这个密钥对将用来计算两方共同密钥 Shared Secret 进行AES的对称加解密。

  • 历史记录获取
    这其实就是一个请求查询数据库的过程,Web端的主要处理是在得到消息后进行解密并根据时间排序以及呈现。这里发生过一个有趣的问题,就是时区的问题,当时我们几个人在不同的地区(一些人由于疫情提前回家了),所以当我们远程连接数据库的时候发现消息时间出现了不一致,也就导致消息展现顺序错乱。后来我们的解决方法也很简单粗暴,就是直接改服务端的系统时间[笑哭.jpg],毕竟赶时间。当然实际还有更细致的解决方法,我目前能想到的就是直接储存1970年1月1日至今经过的时间数,客户端在接收到时间后再根据系统时区进行转换。

可扩展功能

  • 不限字数长度的聊天消息,需要解决数据库储存、JSON请求长度等问题;
  • 消息类型增加emoji、图片、视频和文档;
  • 群组聊天多方加密;

加密算法

AES对称加密

首先大家都知道密码算法按照密钥特点分为两种:对称加密和非对称加密。其中对称加密比较常见的是AES算法和DES算法,非对称加密则是RSA算法和ECC算法。

我们选择对称加密来加解密消息的原因一个是非对称加密的效率比较低下,另一个是一条非对称加密后生成密文只能被接收方解密成明文,就连发送方也无法解密,这会造成历史记录获取失败,除非发送方每次都推一条重复的消息(用自己的公钥加密)到服务端数据库。

说完了选择对称加密的原因,我们再来了解一下AES算法,维基百科里说的很详细这里就不多赘述,简而言之就是一系列不太复杂的逻辑操作但是我脑子记不住。我以前学密码学的时候做过实现AES的Java程序,过不久就忘得一干二净。我觉得大概理解了就差不多了,毕竟我们搞开发又不是做研究,重点还是应该放在API调用上。这里我用的是CryptoJS的AES-256-CBC,其中256指的是单位为bit的密钥长度,CBC是块加密模式。

加密还有一种分法,就是分为流加密和块加密,其中流加密的密钥随机生成并且长度和明文一致,按位(bit)计算,而且为了保证安全性要求密钥每次都不一样的,这显然不能用在我们的项目里;同时AES算法则属于密钥长度确定的块加密,其实不管是流加密还是块加密都要求密钥长度和明文长度一致,但块加密的解决方案是将过长的原文拆分成一块块,每块长度与密钥长度一致,不足的使用一定填充模式进行填充,本项目使用的是Pkcs7填充,CryptoJS默认也是这个模式。

前面提到的CBC (Cipher Book Chaining)则是块加密模式的一种,中文称为密文分组链接模式,块加密模式定义了块之间是如何合作产生最终密文的。在CBC加密模式中,我们要首先定义一个不需要很长的IV (Initialization Vector)初始化向量,这个向量只用一次,就是与第一块明文进行XOR运算,然后用密钥对混淆过的第一块明文进行加密,生成第一块密文,紧接着每块密文又会成为下一块明文的“IV”,直到所有明文块都生成对应的密文块。一般而言每次加密都会生成不一样的IV,使得即便同一段明文也总会生成不一样的密文,防止攻击者使用选择明文攻击法。

Diffie-Hellman密钥交换协议

前面说到我们用AES对称加密算法来加密消息,但我们发现还有问题,那就是我们要怎么安全地传输对称密钥呢?要怎么储存已生成的对称密钥呢?

那有没有安全的通信通道或者我们用非对称加密对密钥再加密一次?从维基百科的介绍中我们可以大致理解到:目前并没有真正的、第三方无法接触的安全通信通道(也许以后量子网络能做到),都是使用了一些技巧,就像是魔术师的障眼法。后者理论上能阻止第三方得到对称密钥(作为非对称加密的明文),但密钥储存的问题也还是没有解决:因为密钥不能放在服务端的话就只能放在客户端,这就会给客户端造成十分大的压力,尤其是当聊天对象增多以后。

那有没有什么办法能在不安全的通道中安全地传递密钥呢?有人看到这里可能就要说我在想peach或者劝我吃花生米了,但你别说,还真有,就是Diffie-Hellman密钥交换协议。不过如果非要抠字眼的话,这协议并不是传递密钥,而是通过传递中间物进一步计算出密钥(Shared Secret)。

这里我们又要请出我们的老伙计维基百科了,里面用了一个颜料叠加的形象例子,好像中文界面就没有这个图。简单来说,就是Alice和Bob分别用自己的私钥和一个提前约定好的数g进行计算,计算结果告知对方,这个计算结果就是我前面所说的双方传递的中间物。然后双方拿到对方的中间物再和自己的私钥进行计算,这样两个人就能得到一致的结果了,就是Alice.private_key + g + Bob.private_key(这里的+不是四则运算里的加法,其实是mod取余,除数记为p是预先设定好的质数),你会发现这不就是结合律吗?

这个协议要统一g和p这两个数,其中p要是很大的质数,并且这两个数都不是需要保密的对象,也就不用担心泄露。涉及到的计算利用了著名的离散对数问题,也即在知道(a^k) mod p = b这个式子中的a, p, b的时候无法高效计算出k值的问题,当然前提p得是个大质数。

Web客户端

  • JS运行时:Node.js 6.13.4
  • JS框架:Vue 2.6.10 + Element 2.13.0
  • 加密算法库:Diffie-Hellman密钥交换协议(Crypto 1.0.1);
  • AES对称加密(CryptoJS 2.1.3)
  • 其它:WebSocket

Node.js是异步事件驱动的JavaScript运行时,所谓运行时其实就是程序的依赖库,异步事件就是指被调用的异步函数不管任务是否完成都立即返回,运行的线程不被挂起继续进行下一个语句,而该异步函数完成任务后会进行回调(即通知调用的线程)。提到Node.js又不得不提一下npm (Node Package Manager),这是非常实用的node包管理器,只需要$ npm install <package-name>就可以把许多主流的JS依赖包下载到项目中来。

Vue提供了响应式和组件化的视图组件,响应式指的是网页在移动设备端打开时可以自适应设备屏幕分辨率和尺寸,优化用户体验;组件化指的是将可能重复出现的视图封装成可重复调用的自定义组件。Vue将组件写成.vue文件,并在上层页面中使用标签进行调用(e.g. 程序员写了一个名为co-example.vue的组件,调用时使用<co-example></co-example>)。Element是基于vue的ui库,说实话当时是随便找的,后来才知道是饿了么的UI,总的来说还不错吧,挺多有用的小组件,颜色主题的想法我也挺喜欢的。不过一定要说的话,有一个小问题就是Layout布局中只有列间距没有行间距的参数。

然后是加密算法库。当然了,这个项目肯定不止用了加密算法的库,只不过信息加密是聊天系统十分重要的一点,所以这两个加密算法库就单独拿出来讲讲。我们使用了Diffie-Hellman密钥交换协议和AES对称加密,其中Crypto是Node.js原生库,我们原计划用它来实现加密的整个流程,只是由于某些尚不清楚的原因,Crypto的AES相关函数无法被找到进行调用,不得以我们又用了另一个加密算法库CryptoJS。又因为CryptoJS只实现了加解密算法,并没有实现Diffie-Hellman,我们最终采用了二者共用的策略。密钥交换协议和对称加密这两者在该项目中缺一不可,具体会在后面提到。

“WebSocket是一种网络传输协议,可在单个TCP连接上进行全双工通信,位于OSI模型的应用层”。WebSocket被我们用来进行客户端和服务端之间的通信,因为是全双工,所以两端可以双向同时传输消息,并且建立连接后可以持续通信。