前言
通过对端到端加密技术的调研,我们通过研究海外主流App,包括WhatsApp, Signal, Telegram,Snapchat等。其中WhatsApp采用的是Signal的加密技术,Signal 起源于 Open Whisper Systems 这个组织,其前身是 Whisper Systems 公司,成立于2010年。Open Whisper Systems 成立后致力于加密通信方面的研究工作,其首个产品是名为 TextSecure 的 Android 平台短信加密应用程序,它采用 Signal Protocol 协议实现了端对端加密,然后最终演变为今天更广泛使用的 Signal 应用程序。通过研究Signal的加密技术,发现其他厂商在端到端加密技术上跟Signal的技术原理没有太大差别,本质上是一致的。
为了更好了了解端到端加密技术,在这里先简单介绍Signal是如何实现端到端加密的,其中的原理是怎么样的?
Signal协议包括单聊和群聊,两种会话类型的实现方式不太一样。但是Signal协议的目标主要就是保证信息只能在端上加解密,任何第三方包括传输链路中间的路由器或者服务器,都只能看到密文,而不能轻易的对信息进行解密。同时为了提供信息的安全性,对于单聊的加密还支持向前向后安全,群聊只有向前安全。
端是什么
端可以理解成是一个设备,一个设备就是一个端,可以是PC,也是可以手机端。在Signal协议中,端到端加密技术,本质上是设备跟设备之间的加密通讯,其他设备或者第三方通讯无法解密端到端之间的加密数据。
那么用什么来作为端的唯一标识呢?Signal是通过一个身份密钥对(Identity Key Pair)来标记一个端的唯一身份,每个端在启动的时候第一时间创建一个128的随机身份密钥对,用来作为端的唯一标识。密钥对生成主要采用了Curve25519算法,通过Curve25519算法可以计算得到一个共享密钥,也就是后面要讲的一个DH协议。
Curve25519算法
Curve25519是一种用于密码学的椭圆曲线。它於2005年由Daniel J. Bernstein提出。该曲线定义在GF(2^255 - 19)的素域上,因为它安全性高,效率高,因此被广泛地应用于密码学领域。
Curve25519是以它的素域命名的,该素域为一个稍小于2255的大素数。它的方程式为y2 = x^3 + 486662x^2 + x,其中x和y是曲线上的点。基点P在曲线上是以x,标为9的点。
Curve25519在密码学中有几个优点,比如它能够抵御离散对数问题等攻击,并且在各种硬件平台上都能够高效运行。因此,它被广泛地用于一些协议,例如TLS和Signal协议的加密通信。
什么是DH协议
DH协议(迪菲-赫尔曼密钥交换),由Curve25519算法生成的密钥对,双方交换彼此的公钥,根据自己的私钥和对方的公钥,是通过模数取幂的方式得出一个共享的秘密值,而这个结果对外界来说是不可知的,具体的数据原理就不展开了,有兴趣的小伙伴可以通过点击链接了解下。
如上图,Alice和Bob各自创建了一个密钥对,然后彼此交换了公钥,双方根据对方的公钥和私钥计算得到了一个密钥,这个密钥就是共享密钥,在这期间只需要暴露交换公钥,结合自己的私钥就能够计算得到相同的共享密钥。通过DH协议就能够在不安全的网络情况下,通过交换彼此的公钥,协商计算出相同的共享密钥。
DH协议是没有防止中间人攻击和回放能力的,中间人可以分别对双方进行2次DH协商得到共享秘钥,从而导致信息泄露,因此需要其他安全机制来保证安全性,比如数字证书、数字签名或者安全通讯通道(TLS/SSL),通过其他安全机制增加对中间人攻击的保护。
什么是端到端
通过上面的简单介绍,我们了解端的定义,也知道通过DH协议可以得到共享密钥,那么端到端在Signal中是什么概念?
要理解端到端加密,我们要先理解端到端,所谓的端到端,就是两个端完成公钥交换,然后协商出共享密钥的一个过程,而且该共享密钥只能在这两个端中使用,其他端无法单独使用。也就是说,如果该过程没有完成,就无法建立端到端加密通讯。
一、端到端如何完成协商的
首先,在Signal中,采用3XDH协议来完成实现共享密钥的协商。3XDH是在DH协议的基础上扩展出来的协议,本质上还是DH协议,只是多了几次DH协商。
3XDH协议需要以下三对基础密钥对和一个临时密钥对,包括如下:
IPK(Identity Key Pair): 身份密钥对,一个长期的符合DH协议的密钥对,用户注册时创建,与用户身份绑定(安装时候随机生成)
SPK(Signed Pre Key): 已签名的预共享密钥,一个中期的符合DH协议的密钥对,用户注册时创建,由身份密钥签名,并定期进行轮换,此密钥可能是为了保护身份密钥不被泄露;(SPK包含了身份校验签名)(默认7天更新)
OPK(One-Time Pre Keys): 一次性使用的 Curve25519 密钥对,安装时生成(默认100对),不足时补充。
EPK(Ephemeral Public Key): 临时的密钥对(回复消息时创建),跟OPK作用类似,棘轮过程中创建使用,用来向后安全保证,后续介绍棘轮的时候会展开。
上图表达的意思是,Alice主动和Bob进行会话协商,计算出共享密钥S的过程。具体需要完成以下动作:
Alice提供自己的两个私钥,IPK-Private(启动时创建) 和 EPK-Private(临时创建)
Alice获取到Bob的三个公钥,IPK-Public、SPK-Public、OPK-Public(不足时没有)
分别计算出DH1、DH2、DH3、DH4
然后根据KDF算法计算得到一个共享密钥S
备注:其中DH4是通过Alice的EPK-Private和Bob的OPK-Public计算得到,由于OPK是由客户端随机生成上传到密钥服务器,只能使用一次,因此会有不足的情况,所以当OPK不足的时候也没有及时补充,就会导致OPK没有,这里不影响协商。
二、基础密钥对是怎么创建的
一般情况下,在首次安装启动的时候,会在端上根据协议要求用Curve25519创建一个IPK、一个SPK以及100个OPK密钥对,并且保存在端上,同时将IPK的公钥和SPK公钥以及100个OPK的公钥上传到公钥管理服务器。
沿用以上例子,Alice和Bob在首次安装的时候,就会在本地使用urve25519算法创建以下密钥对。
同时把密钥对的公钥上传到了公钥管理服务器。这时候服务器和Alice/Bob端就存储了以下密钥信息,所有密钥对的私钥都只保存在了端上。
三、公钥管理服务器
公钥管理服务器,就来管理保存所有端上传的公钥信息,同时提供给其查询某端的公钥信息组。通过公钥管理服务器,就可以实现在对方端离线状态下,也能够完成会话协商,而不用等待对方上线。
备注: 公钥管理服务器需要自己开发实现
每次查询,会返回一组公钥信息,包括一个IPK_Public,一个SPK_Public和签名信息,以及一个OPK_Public,当一个OPK_Public被查询使用之后,服务器就需要对其进行删除,这也是为什么OPK_Public会不足的原因。
目前Signal协议是建议每次端上传100个OPK信息,当不足的时候,端可以在适当的时候继续补充上传,这块逻辑需要接入方自行开发。
四、Alice和Bob是如何完成会话协商计算共享密钥
对于Alice发起端,在和Bob建立端到端加密之前,Alice需要从公钥管理服务器查询获取Bob的公钥组信息,同时创建出一个临时密钥对EPK,根据3XDH协议完成协商,计算出共享秘钥S,后续可以根据共享密钥S进行加密传输数据。
为了保证Bob也可以拿到Alice的公钥信息写上计算出相同的共享密钥,Alice在发送加密信息的同时,会携带上自己的公钥信息,包括EPK_Public、IPK_Public,这样Bob在收到第一条加密消息的时候,就无需再向公钥管理服务器查询Alice的公钥信息,而是通过携带的公钥信息结合自己的私钥准确计算出共享密钥。
Alice在获取Bob的公钥信息,会根据Bob的SPK_Public、IPK_Public和Sign进行签名身份校验,如果发现签名不对,则会拒绝协商。通过SPK的身份签名校验,来进一步保证公钥信息是跟身份绑定要一致的。
Bob在收到Alice携带的公钥信息时,则无需进行签名身份校验。
五、小结
通过通过从介绍了端到端的相关概念以及通过3XDH进行协商计算得到相同的共享秘钥,这样基本上完成了端到端加密共享密钥的初始化,也拉开了端到端加密的序幕。但是如果单纯的通过共享秘钥S进行加解密,当共享密钥泄漏的时候,也会造成信息泄漏,Signal为了解决这个问题,采用了KDF棘轮链算法和DH双棘轮来保证向前向后的安全性。
KDF棘轮保证向前安全
先简单了解一下KDF算法(Key Derivation Function),KDF算法可用于更安全地保存用户密码,普通的密码管理方式是服务器保存用户密码的哈希值,以避免服务器被攻击后黑客拿到用户密码原文,但是一些简单密码的哈希值仍然可以通过少量的碰撞破解出来,比如123456的哈希值就很容易被碰撞出来。更加安全的做法是在用户哈希值附加其它信息(比如用户注册时间,用户住址等等),通过KDF算法导出,得出的密钥具有非常强的随机性,就很难被碰撞出来。比如原始密码是123456的哈希值为"hash (123456)",使用KDF算法得出最终密钥。比如:KDF(hash (123456),用户注册时间)=最终密钥,而服务器只保存最终密钥。这样的密码管理方式的好处是,不管用户设置的密码多么简单,服务器保存的密钥都是非常随机的,很难被碰撞出来。
KDF是一种密钥导出函数,通过附加一些数据(数据被称为“盐”,即salt,附加数据又称“加盐”),将原始密钥导出新的密钥,提高原始密钥的保密性。公式表达为
KDF (原密钥,盐) = 导出密钥
也就是我们在计算得到共享密钥S之后,采用S加密数据,然后对S进行一次KDF运算得到下一个S',也就是S'=KDF(S,盐),由于KDF很难通过S'反推出S,无法通过一个密钥推倒出之前的密钥,因此它是向前安全的,这就是我们说的加密向前安全。
KDF就像是一个棘轮,只能向一个方向转,也就是通过KDF可以向后推导出新的密钥,但是无法推导之前旧的密钥。
通过KDF棘轮算法可以知道,当Alice和Bob通过3XDH协商出共享密钥S之后,只要双方通过约定,采用共同的盐,就可以根据原始的共享密钥S计算出下一个密钥S' 。
当其中某个共享密钥泄漏之后,也可以保证之前的密钥不能被推算出来,但是如果盐也泄漏了,那么别人也可以根据KDF算法根据盐和S推导出后面是密钥。因为KDF只能解决了向前安全性,但是无法保证向后安全。
DH棘轮保证向后安全
KDF无法保证向后安全,是因为盐不变,是否可以动态的改变盐,使得每次进行KDF运算的时候保证盐都不同,保证盐的随机性。
Signal协议巧妙的通过DH协议来计算出这个盐,前面在介绍端到端协商的时候提到了一个EPK,这个就是来解决这个盐的问题。
在首次协商的时候,发起端Alice会在本地创建一对临时密钥EPK,同时通过获取对方的公钥信息进行协商,在没有收到对方回复消息之前,发起端的盐计算是一直保持一致,并且是由Alice的EPK_Private 和Bob的SPK_Public 计算得来,整个过程如下:
Alice通过首次协商会话创建共享密钥之后,给Bob发送第一条消息,首次加密会携带上Alice的相关公钥信息,并且首次创建KDF棘轮链的盐会用自己的临时密钥EPK私钥和对方的签名密钥公钥进行DH得到
如果Alice继续给Bob发送消息,会直接从S和 Salt进行KDF计算出下一个密钥S1,这时候在没有收到Bob的消息时,会一直采用相同的盐进行计算。
S' = KDF(S, Salt)
Bob收到Alice的第一条消息,同样会进行会话协商完成计算出共享密钥S
暂时无法在Lark文档外展示此内容
后续Alice的用Salt加密的消息,Bob只要通过相同S和Salt进行KDF处理,就可以得到下一个S1。
当Bob收到Alice的消息,并且完成解密之后,想要回复Alice消息,Bob会创建一个新的临时密钥EPK,然后重新计算出新的盐,根据KDF运算出新的密钥,然后加密后发给Alice,同时携带Bob新创建的EPK_Public
当Alice收到Bob的消息回复之后,需要处理Bob携带的EPK_Public,并且使用之前的创建的EPK_Private(Alice)进行DH运算得到新的盐,然后根据KDF(S,Slat)计算得到相同的密钥S'
每次当有一端回复消息的时候,都需要重新创建EPK,用来计算下一个新的盐,这样用来保证当某个密钥或者盐泄漏的情况下,不会对后面或者前面的消息造成泄漏风险。
群聊如何实现端到端加密
通过3XDH、KDF棘轮和DH棘轮,就可以实现端到端的向前向后安全,但是这个只能解决单聊一对一的端到端安全问题,如果用在群聊中,由于人数较多,如果也通过单聊的类似的方案,密钥的保存和计算将会导致性能降低。由于群聊的私密性相对于群聊会较低,因此Signal通过另外一种方案来解决群聊的加密问题。
Signal Protocol的群组聊天是通过KDF棘轮算法+公钥签名来进行加密通讯的。通讯流程是这样的,
(1) 每个群组成员都要首先生成随机 32 字节的KDF链密钥(Chain Key),用于生成消息密钥,以保障消息密钥的前向安全性,同时还要生成一个随机Curve25519 签名密钥对,用于消息签名。
(2) 每个群组成员用向其它成员采用单聊的加密方式发送链密钥(Chain Key)和签名公钥。此时每一个成员都拥有群内所有成员的链密钥和签名公钥。
(3) 当一名成员发送消息时,首先用KDF链棘轮算法生成的消息密钥加密消息,然后使用私钥签名,再将消息发给服务器,由服务器发送给其它成员。
(4) 其它成员收到加密消息后,首先使用发送人的签名公钥验证,验证成功后,使用相应的链密钥生成消息密钥,并用消息密钥解密。
(5) 当群组成员离开时,所有的群组成员都清除自己链密钥和签名公钥并重新生成,再次单独发给每一位成员。这样操作,离开的成员就无法查看群组内的消息了。
由上可知,一个人在不同的群组里,会生成不同的链密钥和签名密钥对,以保障群组之间的隔离。在每个群组中,每个成员还要存储其它成员的KDF链和签名公钥,如果群组成员过多,加解密运算量非常大,会影响发送和接收速度,同时密钥管理数据库也会非常大,读取效率也会降低。所以,群组聊天使用signal Protocol协议,群人数不宜太多。