1005 lines
36 KiB
Markdown
1005 lines
36 KiB
Markdown
---
|
||
title: "附录"
|
||
weight: 70
|
||
type: docs
|
||
---
|
||
|
||
{{< boxes/warning >}}
|
||
本页面的翻译未经核对,可能存在翻译质量不佳、错翻、漏翻等情况。您可以在 <a href="https://codeberg.org/wholetrans/docs-matrix-spec">Forgejo 存储库</a> 打开 Issue、提交 Pull Request 或<a href="mailto:errata@wholetrans.org">邮件联系</a>我们提出改进建议和参与翻译与核对。
|
||
{{< /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")
|
||
```
|
||
|
||
#### 语法
|
||
|
||
参照 <http://tools.ietf.org/html/rfc7159> 的语法,移除无关紧要的空白、分数、小数及冗余字符转义。
|
||
|
||
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 (<http://nacl.cr.yp.to/>)。*密钥标识符*用于区分同一实体使用的不同签名密钥。
|
||
|
||
`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/#/<identifier>/<extra parameter>?<additional arguments>
|
||
```
|
||
|
||
其中 `<identifier>` 可为房间 ID、房间别名或用户 ID。仅在引用事件 ID 的永久链接中使用 `<extra parameter>`。matrix.to URI 需以 `https://matrix.to/#/` 和标识符开头。
|
||
|
||
`<additional arguments>` 及前导问号可选,详见下文。
|
||
|
||
客户端不应用后台回退至 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 应已包含区分页向的信息。
|