最近在研究 Matrix ,遇到了很多问题,这篇文章想讲讲我配置 end to end encryption(下称 E2E Encryption) 时遇到的问题。我正在写一个简单的 Matrix Client 以熟悉 Matrix Client-Server API,在写到上传文件时,使用了自己的 synapse 服务器上的账号与 matrix.org 服务器上的账号通信进行测试。但是因为和 matrix.org 服务器上的账号所创建的房间 end-to-end encrypted(这是因为我 matrix.org 服务器上的账号是经过加密的),所以导致我无法发送文件,必须让我的 client 也开启 E2E Encryption。
我是使用的 matrix-jk-sdk 来写 client,因为想节省时间,所以没有准备在浏览器里整,直接就是一个 node 脚本。读者小伙伴可以尝试用其他的 Matrix SDK,甚至还有 rust version desu。但是猫猫没有测试过其他的 SDK 存不存在这个问题。~~
想要开启 E2E Encryption,猫先参考了这里 https://github.com/matrix-org/matrix-js-sdk#end-to-end-encryption-support
⚠⚠⚠警告:被线划掉的这个guidance目前完全过时,千万不要看这个,想要知道简单的正确配置方法请拉到本文章的总结部分!
具体成代码就是:
安装 olm 作为 dependency:
1
2cd ~/neko-matrix-client
pnpm install https://packages.matrix.org/npm/olm/olm-3.2.1.tgz --save-optionalolm 没有发布在 npmjs.com 上,是在 https://packages.matrix.org/npm/olm/ 上,可以自行替换为最新版本的链接(上方代码块内的链接为猫猫写作时此链接提供的最新版本)。optional 安装则是因为不开 E2E Encryption 这个简单的 client 也可以工作。
没有使用 npm 而是使用 pnpm 是因为 pnpm 好用
强烈安利电梯:https://pnpm.io/在引入 matrix-js-sdk 前引入 olm
1
global.Olm = require("olm");
猫猫第一次见到直接对 global object 进行修改的引入操作……
众所周知,非严格模式下,JS 世界的
var
导致了所有被定义的 variable 都是 global variable,这就导致了这些 variable 的管理不当会引发奇奇怪怪的 bug。这篇文章解释了 global variable 的一些隐患。在浏览器中,global variable 是 window,在 nodejs 中,global variable 是 global。正常的引入是const path = require("path");
这样的,path
对于引入它的模块(node 世界每个文件就是一个模块)来说是 private variable。而在 global object 上定义,在其他的“没有引入” olm 的地方也能够访问。那么为什么不在只是用到 olm 的地方引入 olm 呢?那是没有用的。matrix-js-sdk 一定需要 global variable 上有 olm 这个元素才可以使用,不然只会冷冰冰的报一个:UnhandledPromiseRejectionWarning: Error: End-to-end encryption not supported in this js-sdk build: did you remember to load the olm library?
这让我感到非常的诡异,为什么一定要这样?Matrix 的通信中 E2E Encryption 诚然是非常重要的环节,如果开启,几乎所有的操作都要经过加密,那为什么不是封装,作为一个 createClient 的 option?(我看了一眼 olm 的源码,大量数学内容,看不懂,但这不影响)如果光看体积的话,build 之后的 olm 有 205KiB,而 SDK 是 2.32MB,确实 olm 有些大了,但是 Matrix 本身就是追求隐私安全的协议,olm 直接封装引入也不是很矛盾吧?猫猫问号……
在
createClient()
后,startClient()
前开启加密1
2
3
4
5
6const { createClient } = require("matrix-js-sdk");
const client = createClient({/* your config object */})
+ client.initCrypto();
client.startClient();
根据 matrix-js-sdk 的 docs 来看,我们的 E2E Encryption 配置已经完成了,跑一下试试吧~
哈~报错了↓
UnhandledPromiseRejectionWarning: Error: Cannot enable encryption: no sessionStore provided
猫看着这个提示是懵的……嘛,搜搜 issue 吧……于是乎搜到了这个↓
https://github.com/matrix-org/matrix-js-sdk/issues/731
果不其然找到了解决方法:
https://github.com/matrix-org/matrix-js-sdk/issues/731#issuecomment-746301545
可怜这位老哥两年才发现如何解决……
挑重点就是:
1 | + const { WebStorageSessionStore } = require("matrix-js-sdk"); |
我炒,我看完大受震撼,这都是些什么奇技淫巧or猫屎。
姑且不管上面 require 的都是些什么,我先去查了 createClient 接收的参数。
……根本没有啊摔。翻遍文档也没有 full list of options😢于是直接看 Intellisense↓
好欸,是 ICreateClientOpts
~
然后找 ICreateClientOpts
的源码~
首先是 sessionStore
:
原来是要开启 sessionStore
才能打开 E2E crypto……文档教程什么的根本没说,气。但是后面 SessionStore 又是什么东西?那搜索一下这个家伙↓
好吧再去找 WebStorageSessionStore
。WebStorageSessionStore
是 matrix-js-sdk 内置的对象,那么我们来 new 一个吧。
1 | - const { createClient } = require("matrix-js-sdk"); |
欸,WebStorageSessionStore
需要参数。那要传什么东西进去呢?继续搜源码↓
1
2 * @param {WebStorage} webStore A web storage implementation, e.g.
* 'window.localStorage' or 'window.sessionStorage' or a custom implementation.
上文说过,window 是浏览器里的 global variable,然而我是 nodejs ……我思考了一下 Storage API 的本质,这里的 storage 完全可以是 JSON 或者 对象,为什么一定要是 Web Storage 或者是它的实现,为什么没有 for nodejs alternative……但是还是得要来解决这个问题。上面谈到了那个 issue 里面的解决方案,它是用的 node-localstorage 这个包来完成的。于是我们先来看看这个包(地铁猫咪看手机)↓
hmmm怎么说呢,感觉有点稀奇古怪的……还是用 CoffeeScript 写的,有一点害怕😰
然后咕米老师建议我干脆自己来实现一个 Storage 对象。于是查了 Storage API Spec 就开搞了:
1 | function Storage() { |
实现是实现了,但是得搞定存储问题呢,不能存了每次都丢了,所以不如用 fs 存到一个 JSON 里吧?(猫想)sqlite 狂热粉丝咕米老师跳出来说:“数据量大的话会很慢,所以最可靠的方法是用数据库,通常用 sqlite,sqlite 就是做这个的🤩”
然后我抵住了咕米老师的盛情推荐,用 JSON 先。
那么首先就是判断创建和读取这个 JSON:
要把 JSON 的内容转换为 Storage Object,所以需要取走内容新建一个实例。
1 | const { existsSync, readFileSync } = require("fs"); |
那么 Storage Object 的定义就改成这样:
1 | function Storage(content) { |
然后又因为感觉将 length 改为 getter 更加规范一些,但是这又不是 TS 我也没给这简单东西整上 Babel 于是就变成了这样:
1 | function Storage(content) { |
(中间还研究了一下 TS 私有变量 desu,有兴趣的读者可以看看这个了解一下)
在修改 Storage 的情况时,异步存给 JSON:
1 | const { writeFile } = require("fs/promises"); |
别忘了导出 readContent
函数~
合并的效果就是这样:
1 | const { existsSync, readFileSync } = require("fs"); |
这一步完成之后,我们引入实现的 Storage Object
1 | const { createClient, WebStorageSessionStore } = require("matrix-js-sdk"); |
再次启动。log 有所变化,但是还是报错,看错误描述几乎全是 cryptoStore
相关,猜到也要去设置 cryptoStore
了。那继续回到 ICreateClientOpts
的源码,来看 cryptoStore
的描述。
好家伙,这个也是必须要开的。但是 CryptoStore
又是什么类型呢?回到 SDK 文档搜索如下:
CryptoStore
不能直接拿来用,是 Internal module。那么就选用上面的几个 CrytoStore
的 child class。根据环境来看,选用 MemoryCryptoStore
是比较好的选择。那么就查看 MemoryCryptoStore 的源码。
可以看到实例化 MemoryCryptoStore
并没有要求参数,那么我们直接 new 一个。
1 | + const { createClient, WebStorageSessionStore, MemoryCryptoStore } = require("matrix-js-sdk"); |
OK,那么现在应该不用再加什么配置了吧(猫咪生气)。再次跑起。零零星星还是有一些 error。可恶,吃猫猫拳!
稍微看了一下,可以发现是缺少 deviceId
。因为理论上来说,每个登录设备都应该保存 unique 的 deviceId
才对,但是我不知道 deviceId
要怎么去配置,直接搜源码得到的信息是这样的↓
这和没说没区别,于是我又去翻了 Spec(Spec 真的是七零八落)。
因为服务器也是我自己的,它说服务器自动生成的……那我直接 getDeviceId()
就能有了?但是 getDeviceId()
的结果是 null……好吧,它应该是直接读 client 给的 device_id 了,但是我没有设定所以就是 null,那我服务器没分配吗?迷。但是这不是重点,于是我就自己配了一个 device_id 给我的 client 来读。于是就变成了这样:
1 | const { deviceId } = require("./info.json"); |
那么再跑一下试试。
log 出的信息显示 E2E 已经正常打开了,并且下载了其他群聊的加密信息。但是还是有 error。
Error uploading one-time keys TypeError: account.unpublished_fallback_key is not a function
这算是个什么信息…… 查看位置提示指向 matrix-js-sdk 里面的 OlmDevice.js
那再次查找源码。(给出的源码链接是编译前的 TS 文件,图中是编译后的 npm package 源码,所以稍有不同)
这是 new 一个出来的对象怎么会没有上面那个函数呢……问题应该在那个 unpickle 的位置。那就只能看一下 Olm 的源码了。让我们来打开 Olm 的源码……
淦,编译成这样看个大头鬼,遂去找 olm repo……直接全局搜索找到这个文件。
这不是好好定义在这里吗……太怪了吧。于是我又搜索了报错的 unpublished_fallback_key
这个函数,也是在的……那只能查 issue 了,遂搜索到了这条↓
欸,所以要升级 olm……但是之前显示 olm package 下载的网址很久没有更新了……,最新的 package 在 https://gitlab.matrix.org/matrix-org/olm/-/releases 找。Matrix 的文档真的一言难尽呢……
虽然写作时间已经更新到了 3.2.10,但是只有 source code,我并不准备编译,所以选了 3.2.8 这个最新的有 npm package 的版本。
更新的方法:
删去
package.json
里的 olm 那一条配置
.npmrc
1
echo @matrix-org:registry=https://gitlab.matrix.org/api/v4/packages/npm/ >> .npmrc
直接选择在安装时指好 registry 也是可以的喵!
安装
1
2
3
4
5
6# 删去原来过时的 olm 包(忘记执行这一步会导致 require 到的还是原来的包desu,所以不要忘记了,不然就会遇到奇怪的问题,e.g.引入失败)
# 但是我猜多半还是 node_modules 里面还是会有,所以建议暴力删除 node_modules
# 不确定是 pnpm 的问题还是什么……
pnpm install
pnpm install @matrix-org/olm --save-optional将引入 olm 改写成这样
1
global.Olm = require("@matrix-org/olm");
重新 start 一下看看。终于没有 error 了!!!!可喜可贺~🎊
总结
全过程:
引入 Olm
1
npm install @matrix-org/olm --registry=https://gitlab.matrix.org/api/v4/packages/npm/packages/npm/
1
2// Please require olm package before require matrix-js-sdk
global.Olm = require("@matrix-org/olm");为 createClient 添加三个 option:
deviceId
,cryptoStore
和sessionStore
deviceId
配置请参考 Matrix Docs 了解 deviceId 是做什么的再配置。for Browser:
sessionStore
使用WebStorageSessionStore
实例化,其中需要填写任一一种 Web Storage(localStorage
或者sessionStorage
)cryptoStore
可以选择 matrix-js-sdk 提供的IndexedDBCryptoStore
或者LocalStorageCryptoStore
或者MemoryCryptoStore
,具体使用方法请参考对应的文档~1
2
3
4
5
6
7
8const client = sdk.createClient({
baseUrl: "https://example.org",
accessToken: exampleToken,
userId: exampleId,
sessionStore: new sdk.WebStorageSessionStore(/* fill the initialization by yourself */),
cryptoStore: new sdk.MemoryCryptoStore(),
deviceId: deviceId,
});for nodejs:
❗ 因为 WebStorageSessionStore 实例化只接受 Web Storage 及其类似 Implementation 作为参数,所以对于 node 必须使用
node-localstorage
这样的包或者由你自己实现 Storage。个人比较推荐自己实现,根据 SDK 描述需要
key
,getItem
,setItem
,removeItem
,length
五个 API。如果不会请参考 Web Storage Spec 或者看猫怎么写的(觉得有帮助的话给猫的 repo 点个✨也是可以的喵ฅ•ω•ฅ)。1
2
3
4
5
6
7
8const client = sdk.createClient({
baseUrl: "https://example.org",
accessToken: exampleToken,
userId: exampleId,
sessionStore: new sdk.WebStorageSessionStore(/* fill the initialization later */),
cryptoStore: new sdk.MemoryCryptoStore(),
deviceId: deviceId,
});在
startClient()
之前,createClient()
之后初始化加密1
client.initCrypto();
嘿嘿,希望对想要了解 Matrix 想要自己写 Client 的人有所帮助!!!猫猫会很开心哒~
Author: Alendia
Permalink: https://alendia.dev/2022/04/01/Matrix-E2E-guidance-and-doubt/
文章默认使用 CC BY-NC-SA 4.0 协议进行许可,使用时请注意遵守协议。
Comments