From 038491d2ecee6a41ad824956f1dae0e448ff0e25 Mon Sep 17 00:00:00 2001 From: Alexey Shchepin Date: Mon, 26 May 2025 18:11:45 +0300 Subject: [PATCH] Support older Matrix rooms versions starting from version 4 --- include/mod_matrix_gw.hrl | 7 ++ src/mod_matrix_gw.erl | 47 +++++++++-- src/mod_matrix_gw_room.erl | 164 +++++++++++++++++++++++++++++++++---- src/mod_matrix_gw_s2s.erl | 25 ++++-- 4 files changed, 210 insertions(+), 33 deletions(-) diff --git a/include/mod_matrix_gw.hrl b/include/mod_matrix_gw.hrl index 18efbf252..3b4b6a41f 100644 --- a/include/mod_matrix_gw.hrl +++ b/include/mod_matrix_gw.hrl @@ -21,6 +21,13 @@ -record(room_version, {id :: binary(), %% use the same field names as in Synapse + enforce_key_validity :: boolean(), + special_case_aliases_auth :: boolean(), + strict_canonicaljson :: boolean(), + limit_notifications_power_levels :: boolean(), + knock_join_rule :: boolean(), + restricted_join_rule :: boolean(), + restricted_join_rule_fix :: boolean(), knock_restricted_join_rule :: boolean(), enforce_int_power_levels :: boolean(), implicit_room_creator :: boolean(), diff --git a/src/mod_matrix_gw.erl b/src/mod_matrix_gw.erl index f8e208ebe..bfd3951b8 100644 --- a/src/mod_matrix_gw.erl +++ b/src/mod_matrix_gw.erl @@ -41,6 +41,7 @@ handle_info/2, terminate/2, code_change/3, depends/2, mod_opt_type/1, mod_options/1, mod_doc/0]). -export([parse_auth/1, encode_canonical_json/1, + is_canonical_json/1, get_id_domain_exn/1, base64_decode/1, base64_encode/1, prune_event/2, get_event_id/2, content_hash/1, @@ -155,8 +156,8 @@ process([<<"federation">>, <<"v2">>, <<"invite">>, RoomID, EventID], %% TODO: check type and userid Host = ejabberd_config:get_myname(), PrunedEvent = prune_event(Event, RoomVersion), - ?DEBUG("invite ~p~n", [{RoomID, EventID, Event, RoomVer, catch mod_matrix_gw_s2s:check_signature(Host, PrunedEvent), get_pruned_event_id(PrunedEvent)}]), - case mod_matrix_gw_s2s:check_signature(Host, PrunedEvent) of + %?DEBUG("invite ~p~n", [{RoomID, EventID, Event, RoomVer, catch mod_matrix_gw_s2s:check_signature(Host, PrunedEvent, RoomVersion), get_pruned_event_id(PrunedEvent)}]), + case mod_matrix_gw_s2s:check_signature(Host, PrunedEvent, RoomVersion) of true -> case get_pruned_event_id(PrunedEvent) of EventID -> @@ -629,9 +630,15 @@ prune_event(#{<<"type">> := Type, <<"content">> := Content} = Event, Content2 = case Type of <<"m.room.member">> -> - C3 = maps:with([<<"membership">>, - <<"join_authorised_via_users_server">>], - Content), + C3 = + case RoomVersion#room_version.restricted_join_rule_fix of + true -> + maps:with([<<"membership">>, + <<"join_authorised_via_users_server">>], + Content); + false -> + maps:with([<<"membership">>], Content) + end, case RoomVersion#room_version.updated_redaction_rules of false -> C3; @@ -653,7 +660,12 @@ prune_event(#{<<"type">> := Type, <<"content">> := Content} = Event, Content end; <<"m.room.join_rules">> -> - maps:with([<<"join_rule">>, <<"allow">>], Content); + case RoomVersion#room_version.restricted_join_rule of + false -> + maps:with([<<"join_rule">>], Content); + true -> + maps:with([<<"join_rule">>, <<"allow">>], Content) + end; <<"m.room.power_levels">> -> case RoomVersion#room_version.updated_redaction_rules of false -> @@ -677,6 +689,8 @@ prune_event(#{<<"type">> := Type, <<"content">> := Content} = Event, true -> maps:with([<<"redacts">>], Content) end; + <<"m.room.aliases">> when RoomVersion#room_version.special_case_aliases_auth -> + maps:with([<<"aliases">>], Content); _ -> #{} end, Event2#{<<"content">> := Content2}. @@ -716,6 +730,27 @@ sort_json(List) when is_list(List) -> sort_json(JSON) -> JSON. +is_canonical_json(N) when is_integer(N), + -16#1FFFFFFFFFFFFF =< N, + N =< 16#1FFFFFFFFFFFFF -> + true; +is_canonical_json(B) when is_binary(B) -> + true; +is_canonical_json(B) when is_boolean(B) -> + true; +is_canonical_json(Map) when is_map(Map) -> + maps:fold( + fun(_K, V, true) -> + is_canonical_json(V); + (_K, _V, false) -> + false + end, true, Map); +is_canonical_json(List) when is_list(List) -> + lists:all(fun is_canonical_json/1, List); +is_canonical_json(_) -> + false. + + base64_decode(B) -> Fixed = case size(B) rem 4 of diff --git a/src/mod_matrix_gw_room.erl b/src/mod_matrix_gw_room.erl index 002704f69..1d409716f 100644 --- a/src/mod_matrix_gw_room.erl +++ b/src/mod_matrix_gw_room.erl @@ -103,6 +103,7 @@ -define(ROOM_MESSAGE, <<"m.room.message">>). -define(ROOM_HISTORY_VISIBILITY, <<"m.room.history_visibility">>). -define(ROOM_TOPIC, <<"m.room.topic">>). +-define(ROOM_ALIASES, <<"m.room.aliases">>). -define(MAX_DEPTH, 16#7FFFFFFFFFFFFFFF). -define(MAX_TXN_RETRIES, 5). @@ -650,9 +651,7 @@ handle_event(cast, {join_direct, MatrixServer, RoomID, Sender, UserID}, State, D Host, get, MatrixServer, [<<"_matrix">>, <<"federation">>, <<"v1">>, <<"make_join">>, RoomID, UserID], - [{<<"ver">>, <<"9">>}, - {<<"ver">>, <<"10">>}, - {<<"ver">>, <<"11">>}], + [{<<"ver">>, V} || V <- supported_versions()], none, [{timeout, 5000}], [{sync, true}, @@ -770,9 +769,7 @@ handle_event(cast, {join, UserJID, Packet}, _State, Data) -> Host, get, MatrixServer, [<<"_matrix">>, <<"federation">>, <<"v1">>, <<"make_join">>, RoomID, UserID], - [{<<"ver">>, <<"9">>}, - {<<"ver">>, <<"10">>}, - {<<"ver">>, <<"11">>}], + [{<<"ver">>, V} || V <- supported_versions()], none, [{timeout, 5000}], [{sync, true}, @@ -1280,7 +1277,7 @@ check_event_auth(Event, StateMap, Data) -> <<"ban">> -> check_event_auth_ban( Event, StateMap, Data); - <<"knock">> -> + <<"knock">> when (Data#data.room_version)#room_version.knock_join_rule -> check_event_auth_knock( Event, StateMap, Data); _ -> @@ -1289,6 +1286,18 @@ check_event_auth(Event, StateMap, Data) -> _ -> false end; + ?ROOM_ALIASES when (Data#data.room_version)#room_version.special_case_aliases_auth -> + case Event#event.state_key of + undefined -> + false; + StateKey -> + case mod_matrix_gw:get_id_domain_exn(Event#event.sender) of + StateKey -> + true; + _ -> + false + end + end; _ -> Sender = Event#event.sender, case maps:find({?ROOM_MEMBER, Sender}, StateMap) of @@ -1372,8 +1381,11 @@ check_event_auth_join(Event, StateMap, Data) -> case {JoinRule, SenderMembership} of {<<"public">>, _} -> true; {<<"invite">>, <<"invite">>} -> true; - {<<"knock">>, <<"invite">>} -> true; - {<<"restricted">>, <<"invite">>} -> + {<<"knock">>, <<"invite">>} + when (Data#data.room_version)#room_version.knock_join_rule -> + true; + {<<"restricted">>, <<"invite">>} + when (Data#data.room_version)#room_version.restricted_join_rule -> %% TODO true; {<<"knock_restricted">>, <<"invite">>} @@ -1442,7 +1454,7 @@ check_event_auth_leave(Event, StateMap, Data) -> case SenderMembership of <<"invite">> -> true; <<"join">> -> true; - <<"knock">> -> true; + <<"knock">> when (Data#data.room_version)#room_version.knock_join_rule -> true; _ -> false end; _ -> @@ -1609,6 +1621,13 @@ check_event_auth_power_levels(Event, StateMap, Data) -> try case Event#event.json of #{<<"content">> := NewPL = #{<<"users">> := Users}} when is_map(Users) -> + CheckKeys = + case (Data#data.room_version)#room_version.limit_notifications_power_levels of + false -> + [<<"events">>, <<"users">>]; + true -> + [<<"events">>, <<"users">>, <<"notifications">>] + end, case (Data#data.room_version)#room_version.enforce_int_power_levels of true -> lists:foreach( @@ -1632,7 +1651,7 @@ check_event_auth_power_levels(Event, StateMap, Data) -> end end, [], NewMap) end, - [<<"events">>, <<"users">>, <<"notifications">>]); + CheckKeys); false -> ok end, @@ -1677,7 +1696,7 @@ check_event_auth_power_levels(Event, StateMap, Data) -> end end, [], maps:merge(OldMap, NewMap)) end, - [<<"events">>, <<"users">>, <<"notifications">>]), + CheckKeys), true; _ -> true @@ -1772,7 +1791,7 @@ fill_event(JSON, Data) -> _ -> [] end end, - compute_event_auth_keys(JSON))), + compute_event_auth_keys(JSON, Data#data.room_version))), {JSON#{<<"auth_events">> => AuthEvents, <<"depth">> => Depth2, <<"origin">> => MatrixServer, @@ -1923,7 +1942,8 @@ get_latest_events(Pid) -> check_event_signature(Host, Event) -> PrunedEvent = mod_matrix_gw:prune_event(Event#event.json, Event#event.room_version), - mod_matrix_gw_s2s:check_signature(Host, PrunedEvent). + mod_matrix_gw_s2s:check_signature(Host, PrunedEvent, + Event#event.room_version). find_event(Pid, EventID) -> gen_statem:call(Pid, {find_event, EventID}). @@ -2526,8 +2546,85 @@ find_power_level_event(EventID, Data) -> end, undefined, Event#event.auth_events). +binary_to_room_version(<<"4">>) -> + #room_version{id = <<"4">>, + enforce_key_validity = false, + special_case_aliases_auth = true, + strict_canonicaljson = false, + limit_notifications_power_levels = false, + knock_join_rule = false, + restricted_join_rule = false, + restricted_join_rule_fix = false, + knock_restricted_join_rule = false, + enforce_int_power_levels = false, + implicit_room_creator = false, + updated_redaction_rules = false + }; +binary_to_room_version(<<"5">>) -> + #room_version{id = <<"5">>, + enforce_key_validity = true, + special_case_aliases_auth = true, + strict_canonicaljson = false, + limit_notifications_power_levels = false, + knock_join_rule = false, + restricted_join_rule = false, + restricted_join_rule_fix = false, + knock_restricted_join_rule = false, + enforce_int_power_levels = false, + implicit_room_creator = false, + updated_redaction_rules = false + }; +binary_to_room_version(<<"6">>) -> + #room_version{id = <<"6">>, + enforce_key_validity = true, + special_case_aliases_auth = false, + strict_canonicaljson = true, + limit_notifications_power_levels = true, + knock_join_rule = false, + restricted_join_rule = false, + restricted_join_rule_fix = false, + knock_restricted_join_rule = false, + enforce_int_power_levels = false, + implicit_room_creator = false, + updated_redaction_rules = false + }; +binary_to_room_version(<<"7">>) -> + #room_version{id = <<"7">>, + enforce_key_validity = true, + special_case_aliases_auth = false, + strict_canonicaljson = true, + limit_notifications_power_levels = true, + knock_join_rule = true, + restricted_join_rule = false, + restricted_join_rule_fix = false, + knock_restricted_join_rule = false, + enforce_int_power_levels = false, + implicit_room_creator = false, + updated_redaction_rules = false + }; +binary_to_room_version(<<"8">>) -> + #room_version{id = <<"8">>, + enforce_key_validity = true, + special_case_aliases_auth = false, + strict_canonicaljson = true, + limit_notifications_power_levels = true, + knock_join_rule = true, + restricted_join_rule = true, + restricted_join_rule_fix = false, + knock_restricted_join_rule = false, + enforce_int_power_levels = false, + implicit_room_creator = false, + updated_redaction_rules = false + }; binary_to_room_version(<<"9">>) -> #room_version{id = <<"9">>, + enforce_key_validity = true, + special_case_aliases_auth = false, + strict_canonicaljson = true, + limit_notifications_power_levels = true, + knock_join_rule = true, + restricted_join_rule = true, + restricted_join_rule_fix = true, knock_restricted_join_rule = false, enforce_int_power_levels = false, implicit_room_creator = false, @@ -2535,6 +2632,13 @@ binary_to_room_version(<<"9">>) -> }; binary_to_room_version(<<"10">>) -> #room_version{id = <<"10">>, + enforce_key_validity = true, + special_case_aliases_auth = false, + strict_canonicaljson = true, + limit_notifications_power_levels = true, + knock_join_rule = true, + restricted_join_rule = true, + restricted_join_rule_fix = true, knock_restricted_join_rule = true, enforce_int_power_levels = true, implicit_room_creator = false, @@ -2542,6 +2646,13 @@ binary_to_room_version(<<"10">>) -> }; binary_to_room_version(<<"11">>) -> #room_version{id = <<"11">>, + enforce_key_validity = true, + special_case_aliases_auth = false, + strict_canonicaljson = true, + limit_notifications_power_levels = true, + knock_join_rule = true, + restricted_join_rule = true, + restricted_join_rule_fix = true, knock_restricted_join_rule = true, enforce_int_power_levels = true, implicit_room_creator = true, @@ -2550,6 +2661,10 @@ binary_to_room_version(<<"11">>) -> binary_to_room_version(_) -> false. +supported_versions() -> + [<<"4">>, <<"5">>, <<"6">>, <<"7">>, <<"8">>, <<"9">>, + <<"10">>, <<"11">>]. + json_to_event(#{<<"type">> := Type, <<"room_id">> := RoomID, <<"depth">> := Depth, @@ -2562,6 +2677,17 @@ json_to_event(#{<<"type">> := Type, is_list(AuthEvents) -> StateKey = maps:get(<<"state_key">>, JSON, undefined), EventID = mod_matrix_gw:get_event_id(JSON, RoomVersion), + case RoomVersion#room_version.strict_canonicaljson of + true -> + case mod_matrix_gw:is_canonical_json(JSON) of + true -> + ok; + false -> + throw(non_canonical_json) + end; + false -> + ok + end, #event{id = EventID, room_version = RoomVersion, room_id = RoomID, @@ -3162,12 +3288,13 @@ new_room_id() -> MatrixServer = mod_matrix_gw_opt:matrix_domain(Host), <<$!, S/binary, $:, MatrixServer/binary>>. -compute_event_auth_keys(#{<<"type">> := ?ROOM_CREATE}) -> +compute_event_auth_keys(#{<<"type">> := ?ROOM_CREATE}, _RoomVersion) -> []; compute_event_auth_keys(#{<<"type">> := ?ROOM_MEMBER, <<"sender">> := Sender, <<"content">> := #{<<"membership">> := Membership} = Content, - <<"state_key">> := StateKey}) -> + <<"state_key">> := StateKey}, + RoomVersion) -> Common = [{?ROOM_CREATE, <<"">>}, {?ROOM_POWER_LEVELS, <<"">>}, {?ROOM_MEMBER, Sender}, @@ -3175,7 +3302,8 @@ compute_event_auth_keys(#{<<"type">> := ?ROOM_MEMBER, case Membership of <<"join">> -> case Content of - #{<<"join_authorised_via_users_server">> := AuthUser} -> + #{<<"join_authorised_via_users_server">> := AuthUser} + when RoomVersion#room_version.restricted_join_rule -> [{?ROOM_MEMBER, AuthUser}, {?ROOM_JOIN_RULES, <<"">>} | Common]; _ -> [{?ROOM_JOIN_RULES, <<"">>} | Common] @@ -3192,7 +3320,7 @@ compute_event_auth_keys(#{<<"type">> := ?ROOM_MEMBER, _ -> Common end; -compute_event_auth_keys(#{<<"type">> := _, <<"sender">> := Sender}) -> +compute_event_auth_keys(#{<<"type">> := _, <<"sender">> := Sender}, _RoomVersion) -> [{?ROOM_CREATE, <<"">>}, {?ROOM_POWER_LEVELS, <<"">>}, {?ROOM_MEMBER, Sender}]. diff --git a/src/mod_matrix_gw_s2s.erl b/src/mod_matrix_gw_s2s.erl index 5065eba8d..d450c541a 100644 --- a/src/mod_matrix_gw_s2s.erl +++ b/src/mod_matrix_gw_s2s.erl @@ -28,7 +28,7 @@ %% API -export([start_link/2, supervisor/1, create_db/0, - get_connection/2, check_auth/5, check_signature/2, + get_connection/2, check_auth/5, check_signature/3, get_matrix_host_port/2]). %% gen_statem callbacks @@ -38,6 +38,7 @@ -include("logger.hrl"). -include("ejabberd_http.hrl"). -include_lib("kernel/include/inet.hrl"). +-include("mod_matrix_gw.hrl"). -record(matrix_s2s, {to :: binary(), @@ -169,23 +170,29 @@ check_auth(Host, MatrixServer, AuthParams, Content, Request) -> false end. -check_signature(Host, JSON) -> +check_signature(Host, JSON, RoomVersion) -> case JSON of #{<<"sender">> := Sender, - <<"signatures">> := Sigs} -> + <<"signatures">> := Sigs, + <<"origin_server_ts">> := OriginServerTS} -> MatrixServer = mod_matrix_gw:get_id_domain_exn(Sender), case Sigs of #{MatrixServer := #{} = KeySig} -> case maps:next(maps:iterator(KeySig)) of {KeyID, _Sig, _} -> case catch get_key(Host, MatrixServer, KeyID) of - {ok, VerifyKey, _ValidUntil} -> - %% TODO: check ValidUntil - case check_signature(JSON, MatrixServer, KeyID, VerifyKey) of + {ok, VerifyKey, ValidUntil} -> + if + not RoomVersion#room_version.enforce_key_validity or + OriginServerTS =< ValidUntil -> + case check_signature(JSON, MatrixServer, KeyID, VerifyKey) of + true -> + true; + false -> + ?WARNING_MSG("Failed authentication: ~p", [JSON]), + false + end; true -> - true; - false -> - ?WARNING_MSG("Failed authentication: ~p", [JSON]), false end; _ ->