Meteor + Mylar, 服务器叛变也不怕

我们所用的网站,数据大多以明文形式存储在服务器上,服务器端程序鉴别用户的身份、授予用户访问权限。不过业务逻辑复杂了,百密一疏,程序总是有这样那样的漏洞,连支付宝这么敏感的应用都不例外。此外,现在越来越多的网站搭建在公共云平台上,一个很大的顾虑是:云平台的所有者会不会窃取我的机密数据?

因此,数据在服务器上最好是加密存储,解密密钥在用户手里,也就是只有用户自己能看到明文,而网站所有者、云服务提供商、可能的服务器入侵者都只能看到密文。即将发表在网络领域顶级会议 NSDI 2014 的 Building web applications on top of encrypted data using Mylar 就是这样一种解决方案。

服务器 == 数据库

传统的网站一般是“瘦客户端”应用,即业务逻辑大多运行在服务器端,客户端的浏览器只负责渲染服务器发来的网页。不过随着 JavaScript 等前端技术的发展,客户端开始有越来越多页面表现、用户交互的代码。客户端与服务器端代码的接口成了一大麻烦。为什么不把服务器端的业务逻辑移到客户端呢?第一,有些与“权限”相关的业务逻辑不能让用户随意操控;第二,把数据传到客户端处理可能会浪费带宽、增加延迟。

要实现一个“胖客户端、瘦服务器”的 Web 应用,就需要服务器端提供一个包括权限管理和快速查找在内的数据模型。Meteor 就是这样一个著名的 Web 框架,它的服务器端基本上就是个文档模型的数据库(像 MongoDB),客户端与服务器间传输的不再是 HTML 而是结构化的数据。基于 reactive programming 和 “页面元素绑定数据” 的理念,Meteor 不仅能够完全在客户端轻松实现大多数种类的 Web 应用,还有一些很酷的新特性:

  • 一个客户端更新数据,其他客户端页面自动更新
  • 客户端更新数据后无需等待网络延迟,自己的页面立即更新
  • 代码修改后可以无缝地更新而不会打断用户当前操作

挑战

不过,直接把 Meteor 用过来,在用户提交的每份文档上加个密是不行的。

  1. 被入侵的服务器可能向客户端发送被篡改的 JavaScript 代码。
  2. 每个用户用不同的密钥,文档无法在用户间共享。显然,把密钥上传到不可信的服务器是不安全的。
  3. 如果文档被加密了,快速查找在服务器端就无法进行。全部下载到客户端处理太不经济。
    Mylar 系统是这样解决上述挑战的:

  4. 网站所有者对代码进行签名,保证发送到客户端的代码不被篡改。在一个域中运行客户端代码,在另一个域中加载用户的内容,依赖浏览器的同源策略(same-origin policy)保证客户端代码不被恶意用户提交的数据篡改。

  5. 每个权限受控的文档使用一对公私钥,共享文档就是分发文档的私钥。还要对公钥进行数字签名以防假冒。
  6. 使用密码学方法预先计算 “密钥差”,只需提交一个加密查询请求,就能对用多个密钥加密的文档进行快速查找。
    实验表明这些增加的逻辑仅仅降低了 17% 的服务器吞吐量,增加了 50 毫秒的延迟,而且对每个 Meteor 应用只需平均修改 36 行代码就能用上 Mylar 的库。让我们具体看看 Mylar 是怎么实现的吧。

系统架构

CaptureCapture Mylar 由以下部分构成:

  • 浏览器插件,负责验证客户端代码的数字签名(与验证 HTTPS 网页的数字签名类似)
  • 客户端库,对发往服务器的数据进行加密,对来自服务器的数据进行解密。每个用户在每个应用中有一个用于识别的用户名和一对公私钥,称为 principal。私钥使用用户自己的密码加密存储在服务器上,登录的时候从服务器取回并解密。
  • 服务器端库,在加密数据中进行搜索(原理后面详述)。
  • 身份鉴别服务,如 OpenID,这需要是可信的第三方服务,对用户的用户名和公钥进行数字签名。
    由于服务器是不可信的,我们无法相信服务器能正确实施权限控制。因此每个权限控制单元(例如一个聊天室、一份文件)都需要加密,让有权访问的用户共享密钥。每个权限控制单元有一个用于识别的名字和一对公私钥,称为 principal(与用户 principal 的数据结构相同)。

Meteor + Mylar 使用 MongoDB 的文档模型,开发者需要标出文档集合(collection)中需要加密的字段(field)并指定其 principal 的名称。当 Alice 邀请 Bob 加入聊天室时,Alice 的客户端代码需要把 Bob 的 principal 用这个聊天室的 principal 签名,相当于授权 Bob 进入聊天室。

数据共享

数据共享,也就是密钥分发,需要考虑到两点:

  • 只有授权用户才能访问受限区域(权限控制单元)的私钥。这就是下面的 Access Graph。
  • 用户能够鉴别受限区域的公钥,不然服务器可能发送虚假公钥,诱骗用户把机密内容用虚假公钥加密。这就是下面的 Certification Graph。

分发私钥:Access Graph

如下图所示,如何把聊天室 party 授权给 Alice 和 Bob 呢?注意,这个“授权”指的是 Alice 要能够拿到聊天室 party 的私钥,否则 Alice 无法解密聊天室中的内容。

CaptureCapture

如果将聊天室的私钥明文存储在服务器上,显然是不安全的。Mylar 把聊天室的私钥(作为数据)用 Alice 和 Bob 的公钥分别加密,将这两份被加密的私钥存储在服务器上。有多少人有权访问一个区域,这个区域就要把自己的私钥用多少人的公钥分别加密并保存在服务器上。Alice 需要访问聊天室时,取回用 Alice 公钥加密过的聊天室私钥,再用其私钥解密,得到聊天室的私钥。

授权是传递性的,也就是如果 A 授权 B 访问,B 授权 C 访问,则 C 有权访问 A。因此这种授权关系形成了一张图,称为 Access Graph。

鉴别公钥:Certification Graph

继续上节的例子,Bob 如果受 Alice 之邀加入聊天室 party,如何鉴别这个聊天室确实是 Alice 所建呢?毕竟另一个人(如 Boss)可能伪装成 Alice,诱骗 Bob 加入其聊天室并说出不想让老板知道的内容。这就是数字签名派上用场的地方了。

第一种情况:聊天室都是由管理员创建的。应用开发者创建一对硬编码在程序代码中的公私钥(私钥用管理员密码加密存储),当管理员创建一个聊天室时,需要指定一个独特的名字,并自动生成一对聊天室的公私钥。管理员输入密码,将硬编码的私钥解密,再用它签名聊天室名字和聊天室公钥。由于应用代码是不可篡改的(原理见下节),这形成了一条完整的信任链。用户通过聊天室的名字即可确认进入了可信的聊天室。

第二种情况:任何人都可以创建聊天室。Alice 创建聊天室的时候,需要用自己的私钥,对聊天室的公钥进行签名,这个签名可以用 Alice 的公钥验证。假设任何人都可以可信地取得 Alice 的公钥,应用程序可以验证聊天室的公钥并在聊天室窗口上标注 “Alice 创建的聊天室”,其他人却无法伪造这个标识。

为了验证 Alice 的公钥,需要有另一个可信的机构对 Alice 的公钥进行签名。

  • 如果是公司内部应用,也就是用户都是管理员创建的,这个可信机构可以是应用本身。由于管理员有硬编码私钥的密码,可以拿到私钥并用它来对新建的用户进行签名。
  • 如果是用户可以随意注册的公共应用,这个可信机构可以是第三方 OpenID 服务。注意,第三方服务是对其用户名和公钥进行签名,不需要拿到用户的私钥。这些可信机构的公钥要么由更高级的可信机构签名,要么预装在浏览器或操作系统中。事实上现在的 HTTPS 数字签名系统就是这样工作的。

验证代码

熟悉 HTTPS 的朋友应该知道,使用 HTTPS 载入的 HTML、CSS、JS 等文件都是可信的,因为它们经过了服务器端的签名。在 Mylar 中,由于服务器不再可信,对服务器做签名的 HTTPS 不再可用。但应用开发者可以在自己的电脑上对代码签好名再上传到服务器,服务器每次提供给用户的都是经过签名的静态代码和资源文件,用户的浏览器上安装插件来验证这些文件上的签名。

签名的颁发与信任机制与 HTTPS 所依赖的 SSL 证书系统没有区别,我们可以从可信的证书颁发机构获取可用于再次签名的证书(即 code-signing certificate),每次修改代码,都用这个证书给每个代码文件和资源文件分别签名。

用户提交的富文本信息中可能嵌入 JavaScript 代码,进而篡改经过了签名的应用代码。这不是 Mylar 独有的问题,而是一种常见的 Web 攻击,称为 Cross-Site Scripting(XSS)。Mylar 的做法是:

  1. 网站首页的代码中要有版本号。
  2. 网站开发者要对网站首页进行签名。
  3. 网站首页载入的所有内容必须使用另一个域(domain)。这可以防止用户提交的内容篡改网站首页中的应用代码。
  4. 所有 URL 中必须包含版本号作为参数,HTTP 响应中也应包含这个版本号,客户端会检查版本号的一致性。这可以防止中间人返回旧版本的内容,除非中间人从一开始就返回旧版的网站首页。

加密搜索

首先,要让加密的文档能够在服务器端被搜索(而无需把所有数据传到客户端),就需要按照单词(word)给文档建立索引。由于服务器不允许看到明文,需要对每个单词进行加密。也就是用户发送的查询词和文档中的词按照同一种加密算法和密钥进行加密,这样相同的词得到的密文也相同,就可以在文档的范围内快速查找了。

不过 Mylar 中的文档如果是在不同的权限控制单元中,就会是被不同的密钥加密的。用户如果对每个权限控制单元分别发送一个查询请求,开销太大了。最好是用户发一个用某个密钥加密的查询请求,服务器自动把这个加密查询请求转变成用其他密钥加密的查询请求,这样就可以减少客户端与服务器间的通信量。当然服务器在这个过程中不能解密查询请求,更不能拿到用户的任何密钥。这可能吗?

基于椭圆曲线密码学,Mylar 提出了一个可靠的解决方案。

CaptureCapture

继续前面的例子,Bob 被授权访问聊天室 party 时,Bob 可以计算出 Bob 的私钥和聊天室 party 的私钥之差,并把这个差存储在聊天室 party 中。当 Bob 被授权访问聊天室 work 时,Bob 可以把其私钥和聊天室 work 的私钥之差存储在聊天室 work 中。

当 Bob 需要在所有聊天室中搜索时,只需将待查词用自己的私钥加密并发给服务器,服务器会利用之前存储的 “私钥差” 把待查词用 party 和 work 私钥加密的结果分别计算出来,再利用 party 和 chat 聊天室中已经存在的索引,就可以在较短时间内找到包含待查词的文档。如果索引是用 hash 表建立,假设时间复杂度为常数,则整个查询所需时间是与 Bob 加入的聊天室个数成正比的。(实测结果如下图所示,可见论文提出的服务器端 multi-key 搜索比客户端下载再搜索快很多)

CaptureCapture

加密搜索的密码学基础

下面我们用不严格的初等数学描述 “密钥差” 的大致思想。encrypted 是加密后的单词,token 是加密后的待查词,请注意两者的区别。

CaptureCapture

已知用一个密钥加密后的待查词,以及两个密钥之“差”,可以求出用另一个密钥加密后的待查词。

CaptureCapture

为了避免恶意者通过统计词频或字典攻击的方法窃取明文,需要对单词在加密之前进行 hash,再把加密后的结果加上盐(随机字符串),再做 hash。盐以明文形式存储在权限控制单元里。“加盐” 是避免彩虹表之类 hash 攻击的常用方法,在目前的网站里用户密码一般就是先 hash,加盐再 hash 才存进数据库的。

CaptureCapture

细心的读者可能已经注意到,通过 “私钥差” 做查询词转换只能进行一次,因为转换后的结果已经是加密后的词而不再是 token。如果 Access Graph 有两层以上级别,应用代码就要决定让哪一层做私钥转换最经济。

例如在病历数据库里,所有医生有权限访问一个叫做 “职工” 的权限控制单元,“职工” 又有权访问所有病人的权限控制单元。服务器端可以存储 “职工” 私钥与每个病人私钥的 “私钥差”。医生每次搜索时,取得 “职工” 权限控制单元的私钥并将其解密,使用 “职工” 私钥加密查询词,发送到服务器,服务器再将其转换成各个病人的加密结果,在各个病人的权限控制单元里查询。

实现细节

Mylar 对开发者提供如下的 API(除非特别说明,API 都是客户端调用的):

  • 用户控制:

    • idp_config(url, pubkey): 从第三方 OpenID 服务(IDP)取回用户的 principal。
    • create_user(uname, password, auth_princ): 创建用户 uname,其密码为 password,公私钥为 auth_princ。
    • login(uname, password): 用户登录。
    • logout(): 当前用户登出。
  • 数据操作:

    • collection.encrypted([field: princ_field], …): 指定一个文档集合中哪些字段是需要加密的,分别使用什么密钥加密。
    • collection.auth_set([princ_field, fields], …): 用给定的密钥鉴定一个文档集合中若干字段的一致性,即这几个字段作为一个整体的校验和是正确的。这是为了防止恶意服务器用文档 B 的 X 字段值替换文档 A 的 X 字段,破坏数据完整性。
    • collection.searchable(field): 由服务器端调用。将文档集合的某个字段标记为可搜索的,以建立索引,提高搜索效率。
    • collection.search(word, field, princ, filter, proj): 在文档集合中搜索 field 字段值等于 word 的文档,使用 princ 生成加密搜索关键词;搜索结果用 filter 过滤后输出由 proj 指定的字段。
  • 密钥管理:

    • princ_create(name, creator_princ): 创建一对公私钥,用其创建者的密钥对其签名,并授权其创建者访问这对公私钥。
    • princ_create_static(name, password): 由服务器端调用。创建一对硬编码在应用中的公私钥,并用 password 加密私钥。
    • princ_static(name, password): 返回硬编码在应用中的公钥。如果密码正确,同时返回私钥。
    • princ_current(): 获取当前用户的公私钥。
    • princ_lookup(name1, …, namek, root): 检查从根密钥(如第三方 OpenID)开始的密钥链 root, namek, …, name1。
    • granter.add_access(grantee): 授予 grantee 对 granter 密钥的访问权限。
    • grantee.allow_search(granter): 授予 grantee 搜索 granter 内容的权限。
      以聊天室应用为例,代码框架为:

CaptureCapture

可见把一个应用转变为 Mylar 应用确实不需要太多改动。论文中还描述了一些其他类型的应用,代码修改量都不大:

CaptureCapture

增加了加密环节之后,服务器性能是否会明显下降呢?答案是否定的。

CaptureCapture

更多评测结果,请看原始论文。Mylar 是个开源项目,其主页是 http://css.csail.mit.edu/mylar/

小结

Meteor + Mylar 把数据的所有权真正交给用户,在不显著增加应用开发者和服务器负担的情况下,实现了 “端到端”(客户端 A 到客户端 B)的安全。

为了安全,Mylar 不得不在某些方面做一些牺牲:

  • 权限一经授予便无法解除。这是数字签名机制的共同弱点,除非引入 “证书吊销列表”。
  • 无法分开授予读权限和写权限。
  • 服务器端无法对加密数据执行计数、排序等操作。
  • 增加了交换密钥和加解密的过程,增大了用户的访问延迟。
  • 加密的私钥和用于搜索的 “密钥差” 需要保存多份,占用更多的存储空间。
    不论个人数据还是企业应用,向云端的迁移都是大势所趋,但很少有人完全放心云端的数据安全。在 Meteor + Mylar 架构下,云端成了加密的数据库,服务器 “叛变” 不再是隐私的噩梦。尽管这种安全性是以性能为代价的,但随着以 Mylar 为代表的 Web 框架的完善和网络条件的改善,相信越来越多的应用会开始采用 “端到端” 的安全模型。