房间版本 11

本页面的翻译未经核对,可能存在翻译质量不佳、错翻、漏翻等情况。您可以在 Forgejo 存储库 打开 Issue、提交 Pull Request 或邮件联系我们提出改进建议和参与翻译与核对。

本房间版本基于版本 10 ,并进一步明确了撤回(redaction)规则。

客户端注意事项

撤回

[New in this version] 顶层的 originmembershipprev_state 属性不再受到撤回保护。m.room.create 事件现在会保留整个 content 属性。m.room.redaction 事件保留 content 下的 redacts 属性。m.room.power_levels 事件保留 content 下的 invite 属性。

完整的撤回算法如下。

在收到修订事件(redaction event)后,服务器必须去除除以下列表外的所有字段:

  • event_id
  • type
  • room_id
  • sender
  • state_key
  • content
  • hashes
  • signatures
  • depth
  • prev_events
  • auth_events
  • origin_server_ts

content 对象中的所有字段也必须被移除,除非该事件类型属于以下之一:

事件格式

客户端不应再依赖 m.room.create 事件的 content 中的 creator 属性。在所有房间版本中,客户端可以通过 sender 属性来确定房间的创建者。

m.room.redaction 事件的格式已被修改。客户端应当在 content 下查找 redacts 键,而不是顶层属性。

m.room.member 事件的 third_party_invite 键不再被撤回,但撤回后仅包含 signed 键。

服务端实现组件

本部分内容仅供服务端实现者参考。使用 Client-Server API 的应用通常不受此处细节影响。上面“客户端注意事项”部分才是 Client-Server API 使用场景应关注的内容。

本房间版本更新了撤回算法,并修改了服务端应如何创建 m.room.createm.room.redaction 事件。

房间版本 11 以版本 10 为基础,并有以下要点。

撤回

见上文

事件格式

核心事件格式与房间版本 10 相同。不过,该房间版本改变了某些事件类型的部分属性。

本版本房间中的事件结构如下:

Persistent Data Unit


A persistent data unit (event) for room version 11 and beyond.

Persistent Data Unit
Name Type Description
auth_events [string]

Required: Event IDs for the authorization events that would allow this event to be in the room.

Must contain less than or equal to 10 events. Note that if the relevant auth event selection rules are used, this restriction should never be encountered.

content object

Required: The content of the event.

depth integer

Required: The maximum depth of the prev_events, plus one. Must be less than the maximum value for an integer (2^53 - 1). If the room’s depth is already at the limit, the depth must be set to the limit.

hashes Event Hash

Required: Content hashes of the PDU, following the algorithm specified in Signing Events.

origin_server_ts integer

Required: Timestamp in milliseconds on origin homeserver when this event was created.

prev_events [string]

Required: Event IDs for the most recent events in the room that the homeserver was aware of when it made this event.

Must contain less than or equal to 20 events.

room_id string

Required: Room identifier.

sender string

Required: The ID of the user sending the event.

signatures {string: {string: string}}

Required: Signatures for the PDU, following the algorithm specified in Signing Events.

state_key string

If this key is present, the event is a state event, and it will replace previous events with the same type and state_key in the room state.

type string

Required: Event type

unsigned UnsignedData

Additional data added by the origin server but not covered by the signatures.

Event Hash
Name Type Description
sha256 string

Required: The hash.

UnsignedData
Name Type Description
age integer

The number of milliseconds that have passed since this message was sent.

Examples

{
  "auth_events": [
    "$urlsafe_base64_encoded_eventid",
    "$a-different-event-id"
  ],
  "content": {
    "key": "value"
  },
  "depth": 12,
  "hashes": {
    "sha256": "thishashcoversallfieldsincasethisisredacted"
  },
  "origin_server_ts": 1404838188000,
  "prev_events": [
    "$urlsafe_base64_encoded_eventid",
    "$a-different-event-id"
  ],
  "room_id": "!UcYsUzyxTGDxLBEvLy:example.org",
  "sender": "@alice:example.com",
  "signatures": {
    "example.com": {
      "ed25519:key_version": "these86bytesofbase64signaturecoveressentialfieldsincludinghashessocancheckredactedpdus"
    }
  },
  "type": "m.room.message",
  "unsigned": {
    "age": 4612
  }
}

移除 m.room.create 事件的 creator 属性

m.room.create 事件的 content 不再包含 creator 属性,此前它总是与事件的 sender 属性等同。

m.room.redaction 事件的 redacts 属性移入 content

m.room.redaction 事件的 redacts 属性已从事件的顶层属性移到事件的 content 属性下。

为向后兼容旧版客户端,服务端在通过 Client-Server API 提供此类事件时,应在顶层添加 redacts 属性。

为更好兼容新版客户端,服务端在向旧版房间版本提供此类事件时,应在 content 下添加 redacts 属性。

授权规则

事件必须由 sender 属性指定的服务器签名。

影响授权的状态事件类型有:

未显式设置时,权限级别会采用默认值。例如,提及 sender 的权限级别,也可以指代房间内用户的默认权限级别。

m.room.redaction 事件与其它事件一样受到授权规则约束。实际上,除非 m.room.power_levels 事件通过 eventsevents_default 属性对 m.room.redaction 事件设置了权限要求,否则这些事件通常会被允许。特别注意,撤回权限(redact level不会被授权规则考虑。

具有发送撤回事件的能力,并不意味着该撤回一定会被执行。接收服务器必须按撤回处理部分所述进行额外检查。

规则如下:

  1. [Changed in this version] 如果类型为 m.room.create
    1. 如果有任何 prev_events,则拒绝。
    2. 如果 room_id 的域名与 sender 的域名不一致,则拒绝。
    3. 如果 content.room_version 存在且不是已知版本,则拒绝。
    4. 其他情况,允许。
  2. 针对事件的 auth_events
    1. 如果某一对 typestate_key 存在重复项,则拒绝。
    2. 如果有 typestate_key 未按授权事件选择算法选取,拒绝。
    3. 如果有条目在PDU接收时的检查中被拒绝,则拒绝。
    4. 如果没有 m.room.create 事件,被拒绝。
  3. 如果房间状态下 m.room.create 事件的 contentm.federate 属性为 false,且当前事件的 sender 域与创建事件的 sender 域不一致,则拒绝。
  4. 如果类型为 m.room.member
    1. 如果没有 state_key 属性,或 content 中没有 membership 属性,拒绝。
    2. 如果 content 包含 join_authorised_via_users_server
      1. 如果事件未被该属性指定用户的 homeserver 合法签名,则拒绝。
    3. 如果 membershipjoin
      1. [Changed in this version] 若唯一的前序事件是 m.room.createstate_keym.room.create 的 sender,则允许。
      2. 如果 sender 不等于 state_key,拒绝。
      3. 如果 sender 被禁言,拒绝。
      4. join_ruleinviteknock,且会员状态为 invitejoin,则允许。
      5. join_rulerestrictedknock_restricted
        1. 若会员状态为 joininvite,允许。
        2. 如果 content 中的 join_authorised_via_users_server 不是有权邀请用户的用户,拒绝。
        3. 其他情况,允许。
      6. join_rulepublic,允许。
      7. 其他情况,拒绝。
    4. 如果 membershipinvite
      1. 如果 contentthird_party_invite 属性:
        1. 如目标用户已被禁言,拒绝。
        2. 如果 content.third_party_invite 缺少 signed 属性,拒绝。
        3. 如果 signed 不包含 mxidtoken 属性,拒绝。
        4. 如果 mxid 不等于 state_key,拒绝。
        5. 若当前房间状态没有 state_key 匹配 tokenm.room.third_party_invite 事件,拒绝。
        6. 如果 sender 不等于 m.room.third_party_invite 的 sender,拒绝。
        7. signed 中任何签名匹配 m.room.third_party_invite 事件中的公钥,则允许。公钥位于 content 的如下属性中:
          1. public_key 属性内的单个公钥;
          2. public_keys 属性内的公钥列表。
        8. 否则,拒绝。
      2. 如果 sender 当前会员状态不是 join,拒绝。
      3. 如果目标用户当前会员状态为 joinban,拒绝。
      4. 如果 sender 的权限级别大于等于邀请级别,允许。
      5. 否则,拒绝。
    5. 如果 membershipleave
      1. sender 等于 state_key,仅当此用户当前会员状态为 invitejoinknock 时允许。
      2. 如果 sender 的当前会员状态不是 join,拒绝。
      3. 若目标用户当前会员状态为 ban,且 sender 权限小于禁言级别,拒绝。
      4. sender 权限大于等于踢出级别,且目标用户权限低于 sender 权限,允许。
      5. 否则,拒绝。
    6. 如果 membershipban
      1. sender 当前会员状态不是 join,拒绝。
      2. sender 权限大于等于禁言级别,且目标用户权限低于 sender 权限,允许。
      3. 否则,拒绝。
    7. 如果 membershipknock
      1. join_rule 不是 knockknock_restricted,拒绝。
      2. sender 不等于 state_key,拒绝。
      3. sender 当前会员状态不是 baninvitejoin,允许。
      4. 否则,拒绝。
    8. 其他未知会员状态,拒绝。
  5. 如果 sender 当前会员状态不是 join,拒绝。
  6. 若类型为 m.room.third_party_invite
    1. 仅当 sender 当前权限大于等于邀请级别时允许。
  7. 如事件类型所需权限级别大于 sender 权限级别,拒绝。
  8. 如果事件有 state_key@ 开头,且不等于 sender,拒绝。
  9. 若类型为 m.room.power_levels
    1. 如果 content 内的 users_defaultevents_defaultstate_defaultbanredactkickinvite 属性存在且不是整数,拒绝。
    2. 如果 content 内的 eventsnotifications 属性存在,且不是值为整数的对象,拒绝。
    3. 如果 content 内的 users 属性不是键为合法用户ID、值为整数的对象,拒绝。
    4. 如果房间中不存在先前的 m.room.power_levels 事件,允许。
    5. 对于 users_defaultevents_defaultstate_defaultbanredactkickinvite,如有添加、变更或删除,每项需检查:
      1. 当前值大于 sender 当前权限,拒绝。
      2. 新值大于 sender 当前权限,拒绝。
    6. 对于 eventsnotifications 属性中被更改或移除的每一项:
      1. 当前值大于 sender 当前权限,拒绝。
    7. 对于 eventsnotifications 中被添加或更改的每一项:
      1. 新值大于 sender 当前权限,拒绝。
    8. 对于除自身外的 users 属性中被更改或移除的每一项:
      1. 当前值大于等于 sender 当前权限,拒绝。
    9. 对于 users 属性中被添加或更改的每一项:
      1. 新值大于 sender 当前权限,拒绝。
    10. 其他情况,允许。
  10. 其他情况,允许。

这些规则的部分结果:

  • 除非你是房间成员,唯一允许的操作(除初始创建/加入外)只有加入公开房间,以及接受或拒绝房间邀请。
  • 取消禁言某人,你必须同时具有不小于踢出和禁言级别的权限,且权限要高于目标用户。

与 v10 一致

以下章节自 v10 起未修改,为完整性而保留。

撤回处理

本页面的翻译未经核对,可能存在翻译质量不佳、错翻、漏翻等情况。您可以在 Forgejo 存储库 打开 Issue、提交 Pull Request 或邮件联系我们提出改进建议和参与翻译与核对。

在房间版本1和2中,清除操作(redactions)明确属于授权规则第11条。自房间版本3起,这些条件不再适用,如本版本的授权规则所示。

尽管清除操作始终被事件的授权规则接受,但只有在清除事件和被清除的事件都已被接收并且能够被验证后,才能将其发送给客户端。如果这两个事件都有效并已被服务器看到,那么在满足以下任一条件时,服务器会应用清除操作:

  1. 清除事件的 sender 的权限等级大于或等于清除权限等级(redact level)。
  2. 清除事件的 sender 的域名与原事件的 sender 的域名一致。

如果服务器将应用清除操作,则该清除事件也会被发送给客户端。否则,服务器只会等待有效的配对事件到来,届时可以重新检查上述条件。

事件 ID

本页面的翻译未经核对,可能存在翻译质量不佳、错翻、漏翻等情况。您可以在 Forgejo 存储库 打开 Issue、提交 Pull Request 或邮件联系我们提出改进建议和参与翻译与核对。

事件 ID 是事件的参考哈希,其采用一种变体的无填充 Base64 编码,将第 62 和第 63 个字符分别替换为 -_,而不是使用 +/。这与 RFC4648 URL 安全型 base64 的定义一致。

事件 ID 仍以前缀 $ 开头,最终可能类似于 $Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg

状态解析

事件 E 之后的房间状态 S′(E) 由事件 E 之前的房间状态 S(E) 定义,并且依赖于 E 是状态事件还是消息事件:

  • 如果 E 是一条消息事件,则 S′(E) = S(E)
  • 如果 E 是一条状态事件,则 S′(E)S(E) 相同,除了其与 Eevent_typestate_key 对应的项被 Eevent_id 替换。

事件 E 之前的房间状态 S(E)prev_event 集合 {E1, E2, …} 之后的状态集合 {S′(E1), S′(E2), …} 的 合并与决议结果。如何对一组状态进行合并与决议,见下述算法。

定义

版本 2 房间的状态合并算法使用如下定义,并以房间状态集 {S1, S2, …} 为输入:

权限事件(Power events)。
权限事件 指具有类型 m.room.power_levelsm.room.join_rules 的状态事件,或者类型为 m.room.membermembership 字段为 leaveban,且 senderstate_key 不一致的状态事件。其核心思想为:权限事件是那些可能移除某人在房间内某项操作权限的事件。

无冲突状态映射与冲突状态集。
状态映射 Si 的 key 为形如 (event_type, state_key) 的字符串二元组 K,对应的 value V 为一个状态事件。所有 Si 的 (K, V) 键值对可划分为两个集合:如果给定 key K 在所有 Si 出现,且值 V 在每个状态映射都一致,则此 (K, V) 属于 无冲突状态映射;否则,V 属于 冲突状态集

注意,无冲突状态映射每个 key K 只会有一个事件,而冲突状态集可能因同 key 包含多个事件。

鉴权链(Auth chain)。
事件 E鉴权链,是包含 E 的所有鉴权事件(auth events)、所有这些事件的鉴权事件,递归回溯直到房间创建为止的集合。换句话说,就是通过事件的 auth_events 链接遍历可达的所有事件。

鉴权差集(Auth difference)。
鉴权差集 的计算方式如下:首先对每个状态 Si,计算其完全鉴权链,即该状态中每个事件的鉴权链的并集。然后找出那些未在所有鉴权链中都出现的事件。若 Ci 表示 Si 的完全鉴权链,则鉴权差集为 ∪ Ci − ∩ Ci

完整冲突集(Full conflicted set)。
完整冲突集 是冲突状态集与鉴权差集的并集。

逆拓扑权限排序(Reverse topological power ordering)。
一组事件的 逆拓扑权限排序,是按鉴权事件形成的有向无环图(DAG)进行拓扑排序,得到字典序最小的排序,并从最早事件到最晚事件排列。比较两个拓扑排序确定哪一个字典序更小时,事件的比较关系如下:对事件 xy,若

  1. x 的发送者的权限级别 高于 y 的发送者(以各自的 auth_event 查得);或
  2. 发送者权限级别相同,但 xorigin_server_ts 小于 y;或
  3. 权限级别与 origin_server_ts 都相同,但 xevent_id 小于 yevent_id

x < y

逆拓扑权限排序可用 Kahn 算法进行拓扑排序,每步从候选顶点中按上述比较关系选择最小顶点。

主链排序(Mainline ordering)。
P = P0 为某个 m.room.power_levels 事件。从 i = 0 开始,反复获取 Pi+1,即 Piauth_events 中类型为 m.room.power_levels 的事件。每次自增 i,直到 Piauth_events 中没有 m.room.power_levels 事件为止。P0主链 为 [P0 , P1, … , Pn]。

若另有事件 e = e0(可以是另一个 m.room.power_levels 事件),可以构造类似事件链 [e1, …, em],其中 ej+1ejauth_events 中的 m.room.power_levels 事件,em 没有再指向任何 m.room.power_levels 事件。(注意 e0 本身不包含在该列表中,也有可能该列表为空,因为 e 可能没有引用过 m.room.power_levels 事件。)

对这两条列表进行如下比较:

  • 查找最小的 j ≥ 1,使得 ej 属于 P 的主链;
  • 若存在这样的 j,则 ej = Pi,且 i 唯一、i ≥ 0;否则令 i = ∞,其中 ∞ 为一个大于任何整数的特殊标记值;
  • 无论哪种情况,e主链位置 就是 i

P 计算得主链位置后,基于 P 的主链排序,就是将一组事件按以下比较关系(从小到大)排序:对事件 xy,若

  1. x 的主链位置 大于 y(即 x 的鉴权链基于主链上的较早事件);或
  2. 主链位置相同,但 xorigin_server_ts 小于 y;或
  3. 主链位置、origin_server_ts 都相同,但 xevent_id 小于 y

x < y

迭代鉴权检查(Iterative auth checks)。
迭代鉴权检查算法 的输入是初始房间状态和已排序的状态事件列表。它通过遍历事件列表,将符合授权规则的状态事件依次应用到房间状态上。若某状态事件未通过授权规则,则忽略该事件。如果验证授权规则时缺少某个必须的 (event_type, state_key) key,则用事件 auth_events 中相应的状态事件(若未被拒绝)替代。

算法

一组状态的 合并与决议 按如下步骤执行:

  1. 选取出现在 完整冲突集 内的所有权限事件组成集合 X。对于每一个权限事件 P,将 P 的鉴权链中同时属于完整冲突集的事件也加入 X。对 X逆拓扑权限排序 排序为列表。
  2. 无冲突状态映射 作为起点,对上一步得到的事件列表应用迭代鉴权检查算法,得出部分已决议状态。
  3. 将第 1 步未涉及的所有剩余事件按第 2 步已决议状态中的权限等级,用主链排序确定顺序。
  4. 对上述部分已决议状态及新排序的事件列表,再次应用迭代鉴权检查算法
  5. 无冲突状态映射中的相同 key 事件(若存在)替换当前结果中对应事件,得出最终合并决议状态。

被拒绝的事件

由于基于事件当前状态(而非鉴权链)验证授权而被拒绝的事件,除非另有特别说明,在算法中仍按常规方式处理。

注意,那些由于无法通过其鉴权链授权而被拒绝的事件不应出现在此流程中,因为他们不会出现在状态集合之内(本算法只使用状态集中的事件,或状态集中事件的鉴权链中的事件)。

这样做有助于保证不同服务器下房间状态更易收敛,因为事件的被拒绝状态可能不同。如果某服务器在另一个服务器作为中转加入房间时返回了不正确的状态(无论是故障还是恶意),就有可能出现此类差异。状态收敛是重要特性,因为它确保房间中所有用户都看到(基本)一致的房间状态。如果各服务器状态视图分歧,可能导致房间分裂,例如因对成员列表存在分歧。

直观来看,使用被拒绝的事件似乎有风险,但实际上:

  1. 服务器无法随意伪造状态,因为它们仍需通过根据事件鉴权链的鉴权检查(例如,若之前没有权限,不能自授权限)。
  2. 若想使一个已被拒绝的事件通过鉴权,必须存在某个状态集允许该事件。恶意服务器可能构造一个分支,声称状态就是该特定状态集,然后复制被拒绝事件指向该分支并发送该事件。复制的事件将通过鉴权检查。因此,忽略被拒绝事件未必能消除潜在攻击路径。

被拒绝的鉴权事件(auth events)故意不参与迭代鉴权检查,因为检查过程中不会对鉴权事件重新授权(但非鉴权事件则会被检查)。

规范化 JSON

服务器必须严格执行 附录 中规定的 JSON 格式。这意味着在大多数端点会返回 400 M_BAD_JSON 错误,或者在联邦通信中丢弃事件。例如,Federation API 的 /send 端点会丢弃该事件,而 Client Server API 的 /send/{eventType} 端点则会返回 M_BAD_JSON 错误。

签名密钥有效期

在验证事件签名时,服务器必须强制要求密钥请求中的 valid_until_ts 属性至少与所验证事件的 origin_server_ts 相同或更大。缺少签名密钥副本的服务器必须尝试通过 GET /_matrix/key/v2/serverPOST /_matrix/key/v2/query API 获取密钥。当使用 /query 端点时,服务器必须设置 minimum_valid_until_ts 属性,以提示公证服务器在适当情况下尝试刷新密钥。

在确定密钥是否有效时,服务器必须采用 valid_until_ts 和当前时间起未来 7 天中的较小值。这是为了避免攻击者发布一个在较长时间内有效而无法被主服务器所有者撤销的密钥的情况。