验证神锁离线版插件的安全机制

2020-12-31

“在互联网上,没人知道你是一条狗”。知名国际大厂也未必像他们说的一样保护我们的数据安全:

备受舆论压力的大厂都这样,那管理所有密码的密码管理器能放心吗?神锁离线版插件声称采用了端到端加密技术,连内部员工都无法偷取用户密码是不是真的呢?

来,我们告诉你怎么亲手验证我们的安全技术!

神锁离线版可能是唯一一个能告诉用户怎样验证安全技术的密码管理器。

不用太担心不懂技术细节,只需要有一点耐心,就可以和我们一起动手做实验!

比萨斜塔 来自 Saffron Blaze

首先,本次实验的目的是验证:

基本原理

回顾一下神锁离线版插件架构和原理:

我们在app中选择要填充的账号后,用户名和密码是怎么从手机发送到插件的呢?

  1. app加密用户名和密码等信息;
  2. app启动手机浏览器,通过浏览器打开网页程序,将要传输的加密数据发送到云端;
  3. 插件从云端接收到加密数据后,解密出用户名和密码,填充到网页中。

插件实现端到端加密(End-to-end encryption, E2EE)的关键在于借助ECDH算法,可以在插件和app(下图中Alice和Bob)之间建立共享的加密密钥,确保通信安全。

来自 David Göthberg

主要步骤:

实验准备

实验条件:安装并使用神锁离线版app和插件。

以 Edge 浏览器为例,先设置断点,以便截获传输数据进行分析。步骤如下:

  1. 从浏览器菜单中,找到 扩展,打开插件管理界面

  2. 在插件管理中找到神锁离线版,点击 背景页 链接,打开浏览器调试窗口

  3. 设置截获数据的断点:

    这里稍微麻烦一点,点击4次,分别选择:

    1. 源代码(Sources),
    2. 页面(Page),
    3. 程序脚本(background.js),
    4. 找到onServerMessage 函数的定义,在左边行号上点击,设置红色的断点。

实验操作

  1. 浏览器中打开登录网页 http://www.stealmylogin.com/demo.html ,会看到扫码填充框

    scan

  2. 使用神锁离线版app扫码,app从二维码中可以得到正在填充的网站域名和插件公钥

    实验中不要使用真实账号密码,以防泄密给第三方网站。

  3. 选择账号后,app会打开手机浏览器开始填充。点击浏览器的地址栏,复制地址并记录下来。地址有点长,大概是这个样子:

    https://app-link.bluespace.tech:8081/remote-fill/index.html#a=&t=1609300964&host=www.stealmylogin.com&to=15579832-e41c-4b17-8615-b6507a02dd4b&key=MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAELzVA%2B9qJXZJShDcwWxo5%2FWWQTOGi%2FuaRAi1%2BHrcLWw6vISQmzusRptXE%2BCTSVi2HWxQ3BQjCDDd%2FPqTyATgkeUyv3GbfBBXVw2q8LjVpt1wnlrFG729QsnExYVqgI5WP&iv=v6DyQ%2Bmx16mt71Ab&cipher=1Ug43zUPuRkQn74m1309FL0J5BNGcqT5FHcwubzYnpBLAscF%2Bn78RfmurjiWIgOP3cbZ1%2F5Kz%2Fa9sjWyZozdUv1HXmo3u0k3%2F16KkPGuWaaTsHt8uCV7tQP0MNIs84fxr4REnBY%3D

    简单讲解一下,# 符号前面是网页地址,后面是传递给网页程序的数据项目,以 & 分隔,每个项目格式是 名字=数据。整理其中的数据项目:

    • t=1609300964 是填充的时间,用于检测是否过期
    • host=www.stealmylogin.com 是当前正在填充的网站
    • to=15579832-e41c-4b17-8615-b6507a02dd4b 是插件的接收地址,每次填充都随机生成
    • 接下来是端到端加密三元素,使用Base64编码
      • key 是app密钥对的公钥,可以和插件的私钥一起生成加密密钥
      • ivAES-GCM加密参数,随机初始向量(Initial Vector)
      • cipher 是加密后的密文
  4. 很快插件背景页就会在断点捕获到传输过来的数据,大概是这个样子:

  5. 鼠标移动到 message 上,查看捕获的消息,大概是这个样子:

    { to: "15579832-e41c-4b17-8615-b6507a02dd4b", from: "a3ae6e09-8b2c-43bf-a61b-08f4022adfc5", ts: "1609300964", id: "83072ef7-11fe-49fe-bac3-787f92881bbf", data: "{ "key":"MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAELzVA+9qJXZJShDcwWxo5/WWQTOGi/uaRAi1+HrcLWw6vISQmzusRptXE+CTSVi2HWxQ3BQjCDDd/PqTyATgkeUyv3GbfBBXVw2q8LjVpt1wnlrFG729QsnExYVqgI5WP", "iv":"v6DyQ+mx16mt71Ab", "cipher":"1Ug43zUPuRkQn74m1309FL0J5BNGcqT5FHcwubzYnpBLAscF+n78RfmurjiWIgOP3cbZ1/5Kz/a9sjWyZozdUv1HXmo3u0k3/16KkPGuWaaTsHt8uCV7tQP0MNIs84fxr4REnBY=" }" }

    这个消息内容也要记录下来分析(记录的时候,可以像上面一样调整顺序换行)。

实验分析

收集到所有数据后,我们就可以进行数据分析了。

先对比手机端发送的数据和插件接收到的数据:

数据项目 浏览器发送 插件接收 一致
时间 t=1609300964 ts: "1609300964" Y
接收地址 to=15579832-...-b6507a02dd4b to: "15579832-...-b6507a02dd4b" Y
ECC公钥 key=MHY...5WP "key":"MHY...5WP" Y1
加密IV iv=v6DyQ%2Bmx16mt71Ab "iv":"v6DyQ+mx16mt71Ab" Y1
密文 cipher=1Ug...BY%3D "cipher":"1Ug...BY=" Y1
填充网站 host=www.stealmylogin.com NA N
消息ID NA id: "83072ef7-...-787f92881bbf" N
发送地址 NA from: "a3ae6e09-...-08f4022adfc5" N
1 /, +, = 等特殊字符在网页地址中会被替换为 %2F %2B %3D

  1. 填充的网站 host=www.stealmylogin.com 没有发送给插件。这个数据项只用于在手机浏览器中向用户展示,云端并不知道用户当前填充的网站,隐私无忧。

  2. 发送地址和消息ID不是app通过浏览器发送给插件的,是网页发送程序在发送消息的时候生成的随机UUID,用于标记消息。

要验证这两点,需要查看 https://app-link.bluespace.tech:8081/remote-fill/index.htmlscript.js 的源代码,后面有为程序员准备的详解

数据就这些了,对于第一个目标,我们只需把它们和密码对比一下,就很容易确认有没有偷偷发送给云端。

但是还不能完全确认:会不会在传输的数据中夹带密钥,让云端有能力解密出传输的数据呢?

深度分析加密数据

接下来我们要验证有没有带私货给云端解密。

图片来自 Metinegrioglu

发送的其他数据都很简单,只有加密三元组:公钥key,加密IV和密文数据,看起来很奇怪,也比较长,会不会夹带密钥呢?

  1. key 公钥是否夹带了额外数据?

    验证方法:截获的公钥长度是不是160个字符?

    知识点

    • 插件使用ECC P-384曲线,公钥使用 spki 格式编码,长度是120字节。
    • Base64编码会将3字节二进制数据变换成4个字符。

    示例

    截获的数据 MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAELzVA+9qJXZJShDcwWxo5/WWQTOGi/uaRAi1+HrcLWw6vISQmzusRptXE+CTSVi2HWxQ3BQjCDDd/PqTyATgkeUyv3GbfBBXVw2q8LjVpt1wnlrFG729QsnExYVqgI5WP 与公钥长度一致,没有夹带额外数据。

  2. iv 是否夹带了额外数据?

    验证方法:截获的iv长度是不是16个字符?

    知识点

    • AES-256-GCM加密的初始向量IV是完全随机的12字节二进制数据。
    • Base64编码会将3字节二进制数据变换成4个字符。

    示例

    截获的数据 v6DyQ+mx16mt71Ab 正好是16个字符,没有夹带额外数据。

    如果我们多做几次实验,会发现即使是同一个账号,每次IV也是不一样的。

    程序员在调试的时候,还可以把密钥打印出来和IV对比,看看是不是一样的。

  3. cipher 密文是否夹带了额外数据?

    验证方法:是否能够成功填充密码?

    知识点

    • 数据加密使用 AES-256-GCM 算法,可以帮助检测密文数据的完整性。如果密文有改动,解密就会失败。

    程序员在调试的时候,看到 decryptMessage 函数执行没有出错就可以证明没有夹带额外数据了。

进行这3个数据项的检验,就可以判定是否夹带了额外数据。

程序员详解

前面讲解了验证方法,但是没有详细程序实现,显然不是程序员的菜。接下来讲解程序实现中的关键点。

插件和网页发送程序的源代码都没有最小化和混淆,清晰可读,非常方便程序员们帮忙审计。

数据发送

查看 https://app-link.bluespace.tech:8081/remote-fill/index.html 网页源码很容易找到主要代码 script.js

  1. 解析传递给网页程序的参数

    const params = new URLSearchParams(window.location.hash); const timestamp = params.get('t'); const host = params.get('host'); const to = params.get('to'); const key = params.get('key'); const iv = params.get('iv'); const cipher = params.get('cipher');

    向网页传递参数使用 #(window.location.hash) 分隔,而不是 ?,可以防止我们的app开发者写了bug导致泄漏用户信息,因为 # 后面的数据,不会传递到云端。

  2. 将解析出来的数据 t, to, key, iv, cipher 组装成一个Json对象的消息。

    const data = JSON.stringify({key: key, iv: iv, cipher: cipher}); const uuid = uuidv4(); const request = {to: to, from: uuid, ts: timestamp, id: uuidv4(), data: data};

    这里可以确认,host 不会发送到云端,不会泄漏用户隐私。

  3. 再接下来就是使用AJAX将组装的消息发送到云端。

数据接收

数据通过websocket接收,入口是 onmessage 事件,数据处理就是断点的函数 onServerMessage,核心解密部分是crypto.js 中的 decryptMessage 函数:

importPublicKeyText = (Base64Text) => window.crypto.subtle.importKey(this.publicKeyFormat, Base64.decode(Base64Text), this.curve, true, []); deriveKey = (publicKey, privateKey) => window.crypto.subtle.deriveKey({name: "ECDH", public: publicKey}, privateKey, {name: "AES-GCM", length: 256}, true, ["encrypt", "decrypt"]); decrypt = (ivBytes, cipherBytes, secretKey) => window.crypto.subtle.decrypt({name: "AES-GCM", iv: ivBytes}, secretKey, cipherBytes); decryptMessage = async function(keyPair, data) { const encrypted = JSON.parse(data); const publicKey = await this.importPublicKeyText(encrypted.key); const secretKey = await this.deriveKey(publicKey, keyPair.privateKey); const ivBytes = Uint8Array.from(window.atob(encrypted.iv), c => c.charCodeAt(0)); const cipherBytes = Uint8Array.from(window.atob(encrypted.cipher), c => c.charCodeAt(0)); const bytes = await this.decrypt(ivBytes, cipherBytes, secretKey); return new TextDecoder().decode(bytes); };

主要三个操作:

  1. importPublicKeyText 导入 spki 格式的公钥;
  2. deriveKey 调用 ECDH 算法生成256位的AES密钥;
  3. decrypt 使用 AES-GCM 模式解密出原文。GCM是一种特别的AES模式,可以校验数据的完整性,防止篡改。

验证总结

尽管密码学和网络安全都是很专业的技术领域,我们仍然希望尽可能让更多人了解和验证我们使用的安全技术。在设计神锁离线插件时,我们不仅会努力创造最安全的技术,同时还会尽力尝试为用户提供验证技术的方法。

如果没有相关专业知识,可能还是会留下一些疑问。不过我们相信,通过这样详细的验证方法讲解,足以让具备一定相关知识的技术人员能够弄清楚每个细节。


神锁离线版插件系列文章