--- title: "附录" weight: 70 type: docs --- {{< boxes/warning >}} 本页面的翻译未经核对,可能存在翻译质量不佳、错翻、漏翻等情况。您可以在 Forgejo 存储库 打开 Issue、提交 Pull Request 或邮件联系我们提出改进建议和参与翻译与核对。 {{< /boxes/warning >}} ## 无补位的 Base64 *无补位* Base64 指的是 [RFC 4648](https://tools.ietf.org/html/rfc4648) 中定义的“标准”Base64 编码,但不包含等号 `"="` 补位。具体来说,RFC 4648 要求编码数据长度需为 4 的整数倍时,使用 `=` 字符进行补位,而无补位 Base64 则省略此补位。 供参考,RFC 4648 中 Base64 的编码字母表如下: Value Encoding Value Encoding Value Encoding Value Encoding 0 A 17 R 34 i 51 z 1 B 18 S 35 j 52 0 2 C 19 T 36 k 53 1 3 D 20 U 37 l 54 2 4 E 21 V 38 m 55 3 5 F 22 W 39 n 56 4 6 G 23 X 40 o 57 5 7 H 24 Y 41 p 58 6 8 I 25 Z 42 q 59 7 9 J 26 a 43 r 60 8 10 K 27 b 44 s 61 9 11 L 28 c 45 t 62 + 12 M 29 d 46 u 63 / 13 N 30 e 47 v 14 O 31 f 48 w 15 P 32 g 49 x 16 Q 33 h 50 y 使用无补位 Base64 编码的字符串示例: UNPADDED_BASE64("") = "" UNPADDED_BASE64("f") = "Zg" UNPADDED_BASE64("fo") = "Zm8" UNPADDED_BASE64("foo") = "Zm9v" UNPADDED_BASE64("foob") = "Zm9vYg" UNPADDED_BASE64("fooba") = "Zm9vYmE" UNPADDED_BASE64("foobar") = "Zm9vYmFy" 在解码 Base64 时,建议各实现尽可能接受含有或不含有补位字符的输入,以最大程度增强互操作性。 ## 二进制数据 在一些情况下,需要封装二进制数据,例如公钥或签名。鉴于 JSON 无法安全地表示原始二进制数据,所有二进制值应按上述无补位 Base64 字符串编码并在 JSON 中表示。 当 Matrix 规范中提到“opaque byte”或“opaque Base64”值时,指的是在 Base64 解码之后的对应值应视作不可解释的二进制数据,而不是其编码表示。 无论何时,客户端或服务端实现都可以检测 Base64 编码值的正确性,并直接拒绝非法编码的值。但这并非强制要求,可视为实现细节。 针对未来协议转换(如不用 JSON 的场景),如无需 Base64 编码即可安全表示二进制值,则 Base64 可完全省略。 ## JSON 签名 Matrix 规范中多个位置要求对 JSON 对象进行加密签名。这要求将 JSON 编码为二进制字符串。不幸的是,同样的 JSON 结构可以通过更改空白符或调整键顺序产生不同的字节表示。 因此,对对象签名需使用 [规范化 JSON](#canonical-json) 编码为字节序列,并计算该序列的签名,然后将签名添加到原始 JSON 对象。 ### 规范化 JSON 为确保所有实现都使用相同方式编码 JSON,我们定义“规范化 JSON”。本定义与规格外对“Canonical JSON”的其它使用不同。 对于一个值,规范化编码指以 UTF-8 最短编码、字典键按 Unicode 码点字典序排序的 JSON 字符串。JSON 中的数字须为 `[-(2**53)+1, (2**53)-1]` 范围内的整数,不允许指数形式或小数,不允许出现负零 `-0`。 我们选用 UTF-8 作为编码方式,因为所有平台均可用且网络上的 JSON 多为 UTF-8 编码。采用键排序以保证顺序一致。整数范围限定为可用 IEEE 双精度浮点精确表示的范围,因许多 JSON 库以此存储数字。 {{% boxes/warning %}} 房间版本 1、2、3、4 和 5 中的事件可能并不完全符合上述限制。服务器应尽可能能够处理被这些限制判为无效的 JSON。 需特别注意的是,整数可能不在上述指定范围。 {{% /boxes/warning %}} {{% boxes/note %}} 此编码不允许浮点型。 {{% /boxes/note %}} ```py import json def canonical_json(value): return json.dumps( value, # 将 ASCII 之外的码点编码为 UTF-8,而不是 \u 转义 ensure_ascii=False, # 移除多余空白。 separators=(',',':'), # 字典键进行排序。 sort_keys=True, # 结果 Unicode 编码成 UTF-8 字节。 ).encode("UTF-8") ``` #### 语法 参照 的语法,移除无关紧要的空白、分数、小数及冗余字符转义。 value = false / null / true / object / array / number / string false = %x66.61.6C.73.65 null = %x6E.75.6C.6C true = %x74.72.75.65 object = %x7B [ member *( %x2C member ) ] %x7D member = string %x3A value array = %x5B [ value *( %x2C value ) ] %x5D number = [ %x2D ] int int = %x30 / ( %x31-39 *digit ) digit = %x30-39 string = %x22 *char %x22 char = unescaped / %x5C escaped unescaped = %x20-21 / %x23-5B / %x5D-10FFFF escaped = %x22 ; " 引号 U+0022 / %x5C ; \ 反斜杠 U+005C / %x62 ; b 退格符 U+0008 / %x66 ; f 换页符 U+000C / %x6E ; n 换行 U+000A / %x72 ; r 回车 U+000D / %x74 ; t 制表符 U+0009 / %x75.30.30.30 (%x30-37 / %x62 / %x65-66) ; u000X / %x75.30.30.31 (%x30-39 / %x61-66) ; u001X #### 示例 为帮助开发兼容实现,以下测试值可用于验证规范化转换代码。 给定如下 JSON 对象: ```json {} ``` 应输出如下规范化 JSON: ```json {} ``` 给定如下 JSON 对象: ```json { "one": 1, "two": "Two" } ``` 应输出如下规范化 JSON: ```json {"one":1,"two":"Two"} ``` 给定如下 JSON 对象: ```json { "b": "2", "a": "1" } ``` 应输出如下规范化 JSON: ```json {"a":"1","b":"2"} ``` 给定如下 JSON 对象: ```json {"b":"2","a":"1"} ``` 应输出如下规范化 JSON: ```json {"a":"1","b":"2"} ``` 给定如下 JSON 对象: ```json { "auth": { "success": true, "mxid": "@john.doe:example.com", "profile": { "display_name": "John Doe", "three_pids": [ { "medium": "email", "address": "john.doe@example.org" }, { "medium": "msisdn", "address": "123456789" } ] } } } ``` 应输出如下规范化 JSON: ```json {"auth":{"mxid":"@john.doe:example.com","profile":{"display_name":"John Doe","three_pids":[{"address":"john.doe@example.org","medium":"email"},{"address":"123456789","medium":"msisdn"}]},"success":true}} ``` 给定如下 JSON 对象: ```json { "a": "日本語" } ``` 应输出如下规范化 JSON: ```json {"a":"日本語"} ``` 给定如下 JSON 对象: ```json { "本": 2, "日": 1 } ``` 应输出如下规范化 JSON: ```json {"日":1,"本":2} ``` 给定如下 JSON 对象: ```json { "a": "\u65E5" } ``` 应输出如下规范化 JSON: ```json {"a":"日"} ``` 给定如下 JSON 对象: ```json { "a": null } ``` 应输出如下规范化 JSON: ```json {"a":null} ``` 给定如下 JSON 对象: ```json { "a": -0, "b": 1e10 } ``` 应输出如下规范化 JSON: ```json {"a":0,"b":10000000000} ``` ### 签名细节 对 JSON 进行签名时,需先移除 `signatures` 和 `unsigned` 相关的键,然后采用上述规范化编码方式编码对象。对获得的字节数据进行签名,并将签名结果采用[无补位 Base64](#unpadded-base64)编码。得到的 base64 签名应按*签名密钥标识符*添加至 `signatures` 中,其下为签名实体的名称,共同返回原始 JSON 对象,并同时恢复 `unsigned` 字段。 *签名密钥标识符*由*签名算法*和*密钥标识符*拼接而成。*签名算法*标识用于签名的算法,目前支持的值为 `ed25519`,参见 NACL ()。*密钥标识符*用于区分同一实体使用的不同签名密钥。 `unsigned` 和 `signatures` 字段不受签名覆盖。因此,中间实体可添加未签名数据(比如时间戳)或额外签名。 ```json { "name": "example.org", "signing_keys": { "ed25519:1": "XSl0kuyvrXNj6A+7/tkrB9sxSbRi08Of5uRhxOqZtEQ" }, "unsigned": { "age_ts": 922834800000 }, "signatures": { "example.org": { "ed25519:1": "s76RUgajp8w172am0zQb/iPTHsRnb4SkrzGoeCOSFfcBY2V/1c8QfrmdXHpvnc2jK5BD1WiJIxiMW95fMjK7Bw" } } } ``` ```py def sign_json(json_object, signing_key, signing_name): signatures = json_object.pop("signatures", {}) unsigned = json_object.pop("unsigned", None) signed = signing_key.sign(encode_canonical_json(json_object)) signature_base64 = encode_base64(signed.signature) key_id = "%s:%s" % (signing_key.alg, signing_key.version) signatures.setdefault(signing_name, {})[key_id] = signature_base64 json_object["signatures"] = signatures if unsigned is not None: json_object["unsigned"] = unsigned return json_object ``` ### 验证签名 要检查某实体是否已对 JSON 对象签名,具体步骤如下: 1. 检查对象的 `signatures` 成员下是否有该实体的条目。如缺失则验证失败。 2. 从该条目中移除本实现不理解算法的*签名密钥标识符*。如剩下的*签名密钥标识符*为空则验证失败。 3. 从本地缓存或可信密钥服务器查询剩余*签名密钥标识符*的*校验密钥*。找不到则验证失败。 4. 解码 base64 编码的签名字节。失败则验证失败。 5. 移除对象中的 `signatures` 和 `unsigned` 成员。 6. 使用[规范化 JSON](#canonical-json)方式编码剩余 JSON。 7. 用*校验密钥*对签名字节和编码对象进行校验。如失败则验证失败,否则成功。 ## 标识符语法 部分标识符特定于某些房间版本,详见 [房间版本规范](/rooms)。 ### 通用命名空间标识符语法 {{% added-in v="1.2" %}} 本规范定义了一些标识符采用*通用命名空间标识符语法*。该语法适用于非面向用户的标识符,并为实现创建新标识符提供统一机制。 语法定义如下: * 标识符长度不少于 1 个字符且不超过 255 个字符。 * 标识符必须以 `[a-z]` 开头,全由 `[a-z]`、`[0-9]`、`-`、`_` 和 `.` 构成。 * 以 `m.` 开头的标识符为 Matrix 官方保留,不得使用。 * 规范未描述的标识符应遵循 Java 包命名约定进行命名空间区分,通常采用反向域名格式,如 `com.example.identifier`。 {{% boxes/note %}} 标识符可继承本规范的语法。例如,“该标识符使用通用命名空间标识符语法,但无强制命名空间要求”——这意味着 `m.` 依然保留,但实现可以不使用反向 DNS 格式命名自定义标识符。 {{% /boxes/note %}} {{% boxes/rationale %}} ASCII 字符不会因同形异义或编码差异影响标识符用途。此外,全部小写可避免大小写带来的敏感性问题。 {{% /boxes/rationale %}} ### 服务器名 一个主服务器通过服务器名唯一标识。此值在多个标识符中被引用,详见下文。 服务器名表示其他主服务器可通信的该服务器的地址。下述语法包括所有有效服务器名: server_name = hostname [ ":" port ] port = 1*5DIGIT hostname = IPv4address / "[" IPv6address "]" / dns-name IPv4address = 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT IPv6address = 2*45IPv6char IPv6char = DIGIT / %x41-46 / %x61-66 / ":" / "." ; 0-9, A-F, a-f, :, . dns-name = 1*255dns-char dns-char = DIGIT / ALPHA / "-" / "." ——换句话说,服务器名为主机名,后可带可选的数字端口。主机名可以是点分十进制 IPv4 地址、方括号包裹的 IPv6 地址,或 DNS 名称。 IPv4 字面量须为 0-255 的四段十进制数字,用 `.` 分隔。IPv6 字面量参见 [RFC3513, section 2.2](https://tools.ietf.org/html/rfc3513#section-2.2)。 用于 Matrix 的 DNS 名称应遵守互联网主机名的通用约束:以 `.` 分隔的标签,每个标签为字母数字或连字符。 有效服务器名示例: - `matrix.org` - `matrix.org:8888` - `1.2.3.4`(IPv4 字面量) - `1.2.3.4:1234`(IPv4 字面量含端口) - `[1234:5678::abcd]`(IPv6 字面量) - `[1234:5678::abcd]:5678`(IPv6 字面量含端口) {{% boxes/note %}} 此语法基于互联网主机名标准 [RFC1123, section 2.1](https://tools.ietf.org/html/rfc1123#page-13),扩展了对 IPv6 字面量的支持。 {{% /boxes/note %}} 服务器名必须区分大小写:例如,`@user:matrix.org` 与 `@user:MATRIX.ORG` 表示不同用户。 关于服务器名选择,建议如下: - 服务器名总长度不应超过 230 个字符。 - 服务器名不应包含大写字母。 ### 通用标识符格式 Matrix 协议为多个实体(如用户、事件、房间)分配唯一标识符,采用通用格式: &string 其中 `&` 为前缀符号(sigil);`string` 为标识符内容。 前缀符号如下: - `@`:用户 ID - `!`:房间 ID - `$`:事件 ID - `#`:房间别名 用户 ID、房间 ID、房间别名及部分事件 ID 格式如下: &localpart:domain 其中 `domain` 为创建该标识符的主服务器 [服务器名](#server-name),`localpart` 由该主服务器分配。 标识符的具体格式与类型相关。例如,事件 ID 有时可以包含 `domain`,详见下方的 [事件 ID](#event-ids) 章节。 #### 用户标识符 {{% changed-in v="1.8" %}} Matrix 系统内用户通过用户 ID 唯一标识。用户 ID 归属于分配账户的主服务器,格式如下: @localpart:domain 用户 ID 的 `localpart` 为该用户的不透明标识符。不得为空,仅可包含 `a-z`、`0-9`、`.`、`_`、`=`、`-`、`/`、`+` 字符。 `domain` 为创建账户的主服务器 [服务器名](#server-name)。 用户 ID 总长(含 `@` 和域名)不得超过 255 字节。 合法用户 ID 完整语法如下: user_id = "@" user_id_localpart ":" server_name user_id_localpart = 1*user_id_char user_id_char = DIGIT / %x61-7A ; a-z / "-" / "." / "=" / "_" / "/" / "+" {{% boxes/rationale %}} 在定义用户 ID 可用字符时,我们综合考虑了若干要素。 首先,排除了 US-ASCII 基本字符集外的字符。用户 ID 主要用于协议层级标识,作为人类可读名称属于第二层。在一些情况下用户 ID 也用于区分具有相似显示名的用户。支持全 Unicode 字符集将大大增加人工区分难度。选择有限字符集意味着即使不熟悉拉丁字母的用户,也能辨别相似用户 ID。 我们禁止大写字符,是因为不希望出现仅以大小写区分的两个用户 ID:例如可用 `@USER:matrix.org` 替代 `@user:matrix.org`。但在部分场景(如 `m.room.member` 事件的 `state_key`)中用户 ID 必须区分大小写。禁止大写字符并要求主服务器在为新用户生成 ID 时统一小写,是避免 `@USER:matrix.org` 与 `@user:matrix.org` 产生歧义的简便做法。 我们还限制了可用标点符号,以减少特殊字符冲突。例如,部分 API(如过滤器 API)用 `"*"` 作为通配符,所以不能作为合法用户 ID 字符。 长度限制则源自事件中 `sender` 关键字的长度上限。由于用户 ID 出现在用户发送的每个事件中,为防止其长度大幅超过实际内容而设限。 {{% /boxes/rationale %}} Matrix 用户 ID 有时被非正式地称为 MXID。 ##### 历史用户 ID 本规范早期版本允许更广泛的字符作为用户 ID `localpart`。目前仍有活跃用户 ID 不符合现行字符集要求,也有部分房间历史事件的 `sender` 不合规。为了兼容这些房间,客户端和服务器*必须*能够接收 `localpart` 为除 `:` 和 `NUL`(U+0000)以外的任意合法非代理 Unicode 码点(包括其它控制字符及空字符串)的用户 ID。 如 `localpart` 含 U+0021~U+007E 以外的字符,或为空,用户 ID 被视为不合规。对于当前房间版本,服务器在联邦传递事件时仍需接受这些历史用户 ID,但*不应*在事件上下文外将其转发给客户端(如设备列表更新等数据应被丢弃)。 未来房间版本可能直接阻止使用历史字符集的用户参与。历史字符集*已废弃*。 ##### 跨字符集映射 某些情况下需将更广泛字符集映射到受限的用户 ID `localpart` 字符集。例如主服务器接受 `/register` 注册时根据用户名创建用户 ID,或桥接其他协议的用户 ID。 具体映射方式实现可自定。由于用户 ID 对外为不透明字符串,唯一要求是实现能保持映射一致。建议算法如下: 1. 以 UTF-8 编码字符字符串。 2. 将 `A-Z` 字节转为小写。 - 若桥接需区分大小写用户,先加 `_` 转义大写再转换。如 `A` 编码为 `_a`,真实 `_` 则写为 `__`。 3. 其余不在允许字符集的字节及 `=`,以十六进制值、前缀 `=` 形式编码。例如 `#` 写为 `=23`,`á` 写为 `=c3=a1`。 {{% boxes/rationale %}} 建议映射试图最大化简单 ASCII 标识符的人类可读性(区别于 base32),同时能编码*所有*字符(区别于 punycode,不支持 ASCII 标点编码)。 {{% /boxes/rationale %}} #### 房间 ID 房间有唯一的房间 ID。格式如下: !opaque_id:domain `domain` 为创建房间的主服务器 [服务器名](#server-name) 仅用于名称空间防止冲突,并不一定代表房间现仍由该服务器维护。 房间 ID 区分大小写。不建议为人类可读,客户端应完全以不可解释字符串处理之。 房间 ID 的 `localpart`(即 `opaque_id`)可包含除 `:` 和 `NUL`(U+0000)外的任意合法非代理 Unicode 码点(包括控制字符),但建议生成时仅用 ASCII 字母与数字 (`A–Z`, `a–z`, `0–9`)。 房间 ID 总长(含 `!` 和域名)不得超过 255 字节。 #### 房间别名 房间可有零个或多个别名。格式如下: #room_alias:domain `domain` 为创建别名的主服务器 [服务器名](#server-name),其他服务器可通过该主服务器解析别名。 别名的 `localpart` 可包含除 `:` 和 `NUL` 的任意合法非代理 Unicode 码点。 房间别名总长(含 `#` 和域名)不得超过 255 字节。 #### 事件 ID 事件有唯一事件 ID。格式如下: $opaque_id 但具体格式依 [房间版本规范](/rooms) 而异。早期房间版本事件 ID 包含 `domain` 组件,较新版本则省略,使用 base64 编码散列。 除房间版本要求外,事件 ID (含 `$` 和 domain,如有)总长不得超过 255 字节。 事件 ID 区分大小写。不建议为人类可读,客户端应完全视为不透明字符串处理。 ### URI Matrix 内资源有两种主要引用方式:matrix.to 及 `matrix:` URI。当前规范均认定上述两者为有效的实体/资源引用方式。 房间、用户及别名均可通过 URI 表示。可用于特定上下文引用对象,如在消息中提及用户或提供房间历史的永久链接(permalink)。 #### Matrix URI 方案 {{% added-in v="1.2" %}} Matrix URI 格式定义如下(`[]` 表存在可选部分,`{}` 表变量): ``` matrix:[//{authority}/]{type}/{id without sigil}[/{type}/{id without sigil}...][?{query}][#{fragment}] ``` 作为模式,可表示为: ``` MatrixURI = "matrix:" hier-part [ "?" query ] [ "#" fragment ] hier-part = [ "//" authority "/" ] path path = entity-descriptor ["/" entity-descriptor] entity-descriptor = nonid-segment / type-qualifier id-without-sigil nonid-segment = segment-nz ; 见 RFC 3986 type-qualifier = segment-nz "/" ; 见 RFC 3986 id-without-sigil = string ; 详见上方 Matrix 标识符规范 query = query-element *( "&" query-item ) query-item = action / routing / custom-query-item action = "action=" ( "join" / "chat" ) routing = "via=” authority custom-query-item = custom-item-name "=" custom-item-value custom-item-name = 1*unreserved ; 反向 DNS 名 custom-item-value = ``` 此格式遵循 [RFC 3986](https://tools.ietf.org/html/rfc3986),以确保与现有工具最大兼容。方案名(`matrix`)已被 IANA 注册:[点此查看](https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml)。 目前,`authority` 与 `fragment` 未由本规范使用,仅作预留。Matrix 并无可合理填充 `authority` 的中央权威。`schema` 中的 `nonid-segment` 也为未来预留。 `type` 指明实体类型,`id without sigil` 即去除开头符号的标识符。目前用法如下: * `r`:房间别名 * `u`:用户 * `roomid`:房间 ID(区别于房间别名) * `e`:事件(位于房间 ID `roomid` 之后),跟在房间别名 `r` 后的 `e` 已不推荐使用。 {{% boxes/note %}} 在此 URI 格式开发过程中曾使用过 `user`、`room`、`event` 等类型,现在必须转为 `u`、`r`、`e`。`roomid` 没有变化。 {{% /boxes/note %}} {{% boxes/note %}} {{% changed-in v="1.11" %}} 在通过房间别名(`r`)而非房间 ID(`roomid`)引用房间内事件 ID 的方式现已弃用。未发现实际用例,以及房间别名本身可变,故不再支持。 {{% /boxes/note %}} `id without sigil` 即实体标识符去掉 sigil。例如 `!room:example.org` 变为 `room:example.org`(`!` 为房间 ID sigil),sigil 定义见 [通用标识符格式](#common-identifier-format)。 `query` 可选,用于提示客户端 URI 意图。当前规范如下: * `action`——用于指示客户端具体动作。无 `action` 则表明 URI 只标识资源,无建议操作(如可跳转到对象信息页)。 * `action=join`——表明客户端应尝试加入 URI 所示房间,仅适用于房间相关 URI。若用户未加入房间,客户端应先询问用户。 * `action=chat`——表明客户端应尝试发起/打开与 URI 指定用户的 DM(私聊),仅适用于用户相关 URI。如支持 Canonical DM,应重用现有 DM。若未跳转到已有 DM,客户端宜先询问用户。 * `via`——指定解析或操作资源时可尝试的服务器(authority 语法)。如 [下文](#routing) 所述,用于房间 ID 路由推荐,也适合用于非公开联邦的标识符解析。更完整方案见 [MSC3020](https://github.com/matrix-org/matrix-spec-proposals/pull/3020)。 自定义参数可用 [通用命名空间标识符格式](#common-namespaced-identifier-grammar),并按规定编码(如百分比编码和转义 `&`)。与规范参数冲突时,客户端应优先标准项。为兼容不同客户端,建议自定义参数保持一致;建议广泛有用的参数提案纳入规范。 常见 URI 示例: * 指向 `#somewhere:example.org` 的链接:`matrix:r/somewhere:example.org` * 指向 `!somewhere:example.org` 的链接:`matrix:roomid/somewhere:example.org?via=elsewhere.ca` * 指向房间 `!somewhere:example.org` 内事件 `$event` 的链接:`matrix:roomid/somewhere:example.org/e/event?via=elsewhere.ca` * 与 `@alice:example.org` 私聊链接:`matrix:u/alice:example.org?action=chat` 推荐客户端实现算法见 [原始 MSC 文档](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2312-matrix-uri.md#recommended-implementation)。 #### matrix.to 导航 {{% boxes/note %}} matrix.to 是早于 `matrix:` URI 方案的命名空间 URI。*不要*将其视为可公开解析 Web 服务,仅做规范说明。 {{% /boxes/note %}} matrix.to URI 格式如下,基于 [RFC 3986](https://tools.ietf.org/html/rfc3986): ``` https://matrix.to/#//? ``` 其中 `` 可为房间 ID、房间别名或用户 ID。仅在引用事件 ID 的永久链接中使用 ``。matrix.to URI 需以 `https://matrix.to/#/` 和标识符开头。 `` 及前导问号可选,详见下文。 客户端不应用后台回退至 Web 服务器而应在客户端内解析。例如点击房间别名 URI 时弹出参与房间的界面。 URI 组成部分应按 RFC 3986 百分号编码。 matrix.to URI 示例: * 指向 `#somewhere:example.org` 的链接:`https://matrix.to/#/%23somewhere%3Aexample.org` * 指向 `!somewhere:example.org` 的链接:`https://matrix.to/#/!somewhere%3Aexample.org?via=elsewhere.ca` * 指向房间 `!somewhere:example.org` 内事件 `$event` 的链接:`https://matrix.to/#/!somewhere%3Aexample.org/%24event%3Aexample.org?via=elsewhere.ca` * 指向 `@alice:example.org` 的链接:`https://matrix.to/#/%40alice%3Aexample.org` {{% boxes/note %}} {{% changed-in v="1.11" %}} 通过房间别名指代房间内事件 ID 的方式现已弃用。未发现实际用例,且房间别名非唯一、可变,不应继续支持。 {{% /boxes/note %}} {{% boxes/note %}} 历史上,客户端生成的 URI 未总是充分编码。客户端应尽力兼容,例:未编码的房间别名应尽量支持。 {{% /boxes/note %}} {{% boxes/note %}} 解码 matrix.to URI 可能出现多余斜杠,这与部分 [房间版本](/rooms) 有关。生成 URI 时推荐编码斜杠。 {{% /boxes/note %}} {{% boxes/note %}} 旧版规范曾提及用“群组”组织房间,但未正式引入规范内容,现已被 [Spaces](/client-server-api/#spaces) 取代。历史 matrix.to URI 指向群组形式如 `https://matrix.to/#/%2Bexample%3Aexample.org`(`+` sigil 可编码也可不编码)。 {{% /boxes/note %}} #### 路由 房间 ID 本身不可路由,无可靠域名接收请求。引入 `via` 参数后可部分缓解,但不可彻底解决。客户端应尽力选择好路由目标,但也应注意相关 [问题 #1579](https://github.com/matrix-org/matrix-spec/issues/355)。 若房间或其永久链接未用房间别名,建议 URI 查询参数含至少一个 `via` 指定服务器。可多次添加 `via` 指定多个服务器。 `via` 参数内容建议用于 [客户端服务器 `/join/{roomIdOrAlias}` API](/client-server-api/#post_matrixclientv3joinroomidoralias)。 生成房间链接和永久链接时,应选能长期存在的高概率服务器。具体选择方法为实现细节,当前建议选取 3 个唯一服务器,规则如下: - 第一个服务器为房间内最高权限且等级不低于 50 的用户所在服务器,若无则选成员最多的服务器。高权限用户(通常100)稳定性高,更能长期留在房间。 - 第二个服务器为按规模排序的下一个或成员最多的服务器。因用户多的服务器更可能长期参与,不易被移除。 - 第三个服务器为下一个成员最多的服务器。 - 被服务器 ACL 阻止的服务器不得被选择。 - IP 地址不得作服务器,应更倾向使用域名。IP 不可迁移,风险高。 - 三个服务器应互不重复。实际不足三台则只指定现有数量。例如仅 2 用户的房间最多提供 2 个 `via`。 ### 不透明标识符 规范定义某些标识符采用*不透明标识符语法*。本语法适用于无需解析或解释的非用户可见标识符,仅要求全局唯一。 语法定义: * 标识符仅可含 `[0-9]`、`[A-Z]`、`[a-z]`、`-`、`.`、`_` 和 `~`。 * 若无特殊说明,长度不少于 1 字符且不超过 255 字符。 {{% boxes/note %}} 字符集与 [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986#section-2.3) 的 unreserved 字符一致。 {{% /boxes/note %}} ## 密钥表示约定 有时需在用户界面展示私有加密密钥。 此时,密钥 *应* 以如下字符串格式呈现: 1. 创建字节数组,先为 `0x8B` 和 `0x01` 两字节,再接原始密钥字节。 2. 将上述所有字节(含前两字节)异或,得出校验字节,并将该字节附加至数组尾。 3. 用 base58 编码全部字节,字母表为 `123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz`。 4. 每 4 个字符插入一个空格。 读取密钥时,客户端应忽略所有空白,并按 1-4 步逆序解析。 {{% boxes/note %}} base58 字母表与[比特币地址](https://en.bitcoin.it/wiki/Base58Check_encoding#Base58_symbol_chart)一致。 {{% /boxes/note %}} ## 3PID 类型 第三方标识符(3PID)是指在其它命名空间中与某人关联的标识符。由 `medium`(标识命名空间)与 `address`(该空间内的字符串)元组组成。`address` 必须是标识符的规范格式,即在可有多种表示的情况下,只能选用其中一种。 例如,电邮的 `medium` 为 'email',`address` 为 email 地址,如 `bob@example.com`。域名解析不区分大小写,因此 `bob@Example.com` 的 3PID 也应写为 `bob@example.com`(小写 'e'),而非 `bob@Example.com`。 本规范下定义命名空间如下,后续版本可扩展更多。 ### 电子邮件 Medium: `email` 表示电子邮件地址,`address` 格式为 `user@domain` 且域名全部小写,不能含姓名、尖括号或 mailto: 前缀。 除将电邮域名部分转为小写外,实现还应遵循 [Unicode 标准第五章“无大小写匹配”](https://www.unicode.org/versions/Unicode13.0.0/ch05.pdf#G21790) 算法进行大小写归一。例如 `Strauß@Example.com` 处理时应视作 `strauss@example.com`。 ### PSTN 电话号码 Medium: `msisdn` 表示公用电话网电话号码。`address` 以 MSISDN(E.164 编号规则)格式表示的电话号码。注意 MSISDN 不带 '+' 前缀。 ## Glob 风格匹配 某些场景下需要用 glob(通配符)匹配字符串。Matrix 的 glob 匹配遵循: * `*` 匹配零个或多个字符。 * `?` 匹配恰好一个字符。 ## 点分属性路径 通过“点”连接属性名能直观表达事件属性路径,如 `content.body` 表示事件的 `content` 下 `body` 属性。 如属性名自身包含点,为避免歧义,点与反斜杠应以反斜杠转义。例如,`content.m\.relates_to` 表示 `content` 下名为 `m.relates_to` 的属性。类似地,`content.m\\foo` 表示名为 `m\foo` 的属性。 其它转义序列原样保留,例如 `\x` 按字面解释为反斜杠加 x。建议实现不做冗余转义,因其它转义序列将来也可能赋予特殊含义。 ## 安全威胁模型 ### 拒绝服务 攻击者可能尝试阻止消息送达或发送,从而: - 破坏竞争对手的服务或市场推广 - 审查讨论或某参与者 - 实施一般性破坏 #### 威胁:资源耗尽 攻击者可能导致受害服务器消耗殆尽某项资源(如开放 TCP 连接、CPU、内存、磁盘) #### 威胁:不可恢复一致性冲突 攻击者可发送消息导致集群产生无法恢复的“脑裂”状态,致使受害服务器无法得出房间一致状态。 #### 威胁:篡改历史 攻击者可能诱使受害者接受无效消息,被其他服务器拒绝,并因此波及依赖其消息的后续消息。 #### 威胁:阻断网络流量 攻击者可试图隔离受害服务器与部分或全部房间服务器间流量。 #### 威胁:高流量消息攻击 攻击者可向目标房间发送高流量消息,致其难以使用。 #### 威胁:无授权用户封禁 攻击者可能未经授权封禁房间用户。 ### 冒充 攻击者可能尝试伪造受害者身份发送消息,以: - 从事非法活动冒充受害者 - 窃取受害者权限 #### 威胁:篡改消息内容 攻击者可能修改受害者已发消息内容。 #### 威胁:伪造消息 "origin" 字段 攻击者可能发送新消息,冒充受害者并伪造 "origin" 字段。 ### 垃圾信息(Spam) 攻击者可尝试向受害者批量发送消息,以: - 寻找诈骗对象 - 推销不需要的商品 #### 威胁:非请求消息 攻击者可向不愿接收者发送消息。 #### 威胁:辱骂消息 攻击者可向受害者发送辱骂或威胁消息。 ### 窃听 攻击者可尝试获取并非发往自身的受害者消息内容或元数据,以: - 获取敏感个人信息或商业信息 - 利用信息冒充受害者(如重置密码邮件) - 了解受害者的交流对象与时间 #### 威胁:传输泄露 攻击者可能在服务器间传输中泄露消息内容或元数据。 #### 威胁:泄露给房间外服务器 攻击者可能诱使房间内服务器向未获授权的攻击者服务器发送消息。 #### 威胁:泄露给房间内服务器 攻击者占有房间内服务器后可暴露该房间消息及元数据。 ## 加密测试向量 为帮助开发兼容实现,以下测试值可用于验证加密事件签名代码。 ### 签名密钥 以下所有测试向量均使用下列 base64 编码字符串解码得出的 32 字节值,作为生成 `ed25519` 签名密钥的种子: SIGNING_KEY_SEED = decode_base64( "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA1" ) 均采用以下服务器名及密钥 ID: SERVER_NAME = "domain" KEY_ID = "ed25519:1" ### JSON 签名 给定空 JSON 对象: ```json {} ``` 签名算法应输出: ```json { "signatures": { "domain": { "ed25519:1": "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ" } } } ``` 给定有内容的 JSON 对象: ```json { "one": 1, "two": "Two" } ``` 签名算法应输出: ```json { "one": 1, "signatures": { "domain": { "ed25519:1": "KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw" } }, "two": "Two" } ``` ### 事件签名 极简事件如下: ```json { "room_id": "!x:domain", "sender": "@a:domain", "origin": "domain", "origin_server_ts": 1000000, "signatures": {}, "hashes": {}, "type": "X", "content": {}, "prev_events": [], "auth_events": [], "depth": 3, "unsigned": { "age_ts": 1000000 } } ``` 签名算法应输出: ```json { "auth_events": [], "content": {}, "depth": 3, "hashes": { "sha256": "5jM4wQpv6lnBo7CLIghJuHdW+s2CMBJPUOGOC89ncos" }, "origin": "domain", "origin_server_ts": 1000000, "prev_events": [], "room_id": "!x:domain", "sender": "@a:domain", "signatures": { "domain": { "ed25519:1": "KxwGjPSDEtvnFgU00fwFz+l6d2pJM6XBIaMEn81SXPTRl16AqLAYqfIReFGZlHi5KLjAWbOoMszkwsQma+lYAg" } }, "type": "X", "unsigned": { "age_ts": 1000000 } } ``` 带有可撤回内容的事件如下: ```json { "content": { "body": "Here is the message content" }, "event_id": "$0:domain", "origin": "domain", "origin_server_ts": 1000000, "type": "m.room.message", "room_id": "!r:domain", "sender": "@u:domain", "signatures": {}, "unsigned": { "age_ts": 1000000 } } ``` 签名算法应输出: ```json { "content": { "body": "Here is the message content" }, "event_id": "$0:domain", "hashes": { "sha256": "onLKD1bGljeBWQhWZ1kaP9SorVmRQNdN5aM2JYU2n/g" }, "origin": "domain", "origin_server_ts": 1000000, "type": "m.room.message", "room_id": "!r:domain", "sender": "@u:domain", "signatures": { "domain": { "ed25519:1": "Wm+VzmOUOz08Ds+0NTWb1d4CZrVsJSikkeRxh6aCcUwu6pNC78FunoD7KNWzqFn241eYHYMGCA5McEiVPdhzBA" } }, "unsigned": { "age_ts": 1000000 } } ``` ## Matrix API 约定 本节主要用于指导 Matrix 新 API 的设计人员,统一 API 行为规则,以保持协议一致性,提升开发体验。 ### HTTP 接口和 JSON 属性命名 HTTP 传输的 API 接口名称一律使用下划线分隔。例如 `/delete_devices`。 API 中 JSON 对象的键名也遵循该命名约定。 {{% boxes/note %}} 历史上有部分例外,如 `/createRoom`。这些不一致的问题未来版本可能会统一。 {{% /boxes/note %}} ### 分页 支持多页结果的 REST API 端点应符合如下约定。 * 若有更多结果,接口返回名为 `next_batch` 的属性,其值为字符串 token,可在后续接口请求中用以获取下一页。 若无更多结果,则 *省略* `next_batch` 属性。 * 接口接受名为 `from` 的查询参数,客户端应赋以前次返回的 `next_batch` 值。 * 若接口支持双向分页(如 `/messages`,可向前/后遍历时间线),则应返回 `prev_batch` 属性,可指向上一页。 避免用额外的“方向”参数。`next_batch` 与 `prev_batch` 的 token 应已包含区分页向的信息。