Calculate and set event id using reference hash

This commit is contained in:
Pim Kunis 2021-08-18 13:54:18 +02:00
parent 8696b0fb96
commit 1781575e75
6 changed files with 177 additions and 108 deletions

View file

@ -1,5 +1,5 @@
defmodule MatrixServer do defmodule MatrixServer do
alias MatrixServer.OrderedMap alias MatrixServer.EncodableMap
def get_mxid(localpart) when is_binary(localpart) do def get_mxid(localpart) when is_binary(localpart) do
"@#{localpart}:#{server_name()}" "@#{localpart}:#{server_name()}"
@ -76,8 +76,7 @@ defmodule MatrixServer do
def encode_canonical_json(object) do def encode_canonical_json(object) do
object object
|> Map.drop([:signatures, :unsigned]) |> EncodableMap.from_map()
|> OrderedMap.from_map()
|> Jason.encode() |> Jason.encode()
end end
@ -134,4 +133,11 @@ defmodule MatrixServer do
datetime1 datetime1
end end
end end
def encode_url_safe_base64(data) do
data
|> encode_unpadded_base64()
|> String.replace("+", "-")
|> String.replace("/", "_")
end
end end

View file

@ -1,10 +1,10 @@
# https://github.com/michalmuskala/jason/issues/69 # https://github.com/michalmuskala/jason/issues/69
defmodule MatrixServer.OrderedMap do defmodule MatrixServer.EncodableMap do
alias MatrixServer.OrderedMap alias MatrixServer.EncodableMap
defstruct pairs: [] defstruct pairs: []
defimpl Jason.Encoder, for: OrderedMap do defimpl Jason.Encoder, for: EncodableMap do
def encode(%{pairs: pairs}, opts) do def encode(%{pairs: pairs}, opts) do
Jason.Encode.keyword(pairs, opts) Jason.Encode.keyword(pairs, opts)
end end
@ -14,6 +14,9 @@ defmodule MatrixServer.OrderedMap do
pairs = pairs =
map map
|> Enum.map(fn |> Enum.map(fn
{k, v} when is_struct(v, DateTime) ->
{k, DateTime.to_unix(v, :millisecond)}
{k, v} when is_map(v) -> {k, v} when is_map(v) ->
{k, from_map(v)} {k, from_map(v)}
@ -22,6 +25,6 @@ defmodule MatrixServer.OrderedMap do
end) end)
|> Enum.sort() |> Enum.sort()
%OrderedMap{pairs: pairs} %EncodableMap{pairs: pairs}
end end
end end

View file

@ -42,6 +42,8 @@ defmodule MatrixServer.KeyServer do
# https://blog.swwomm.com/2020/09/elixir-ed25519-signatures-with-enacl.html # https://blog.swwomm.com/2020/09/elixir-ed25519-signatures-with-enacl.html
defp sign_object(object, private_key) do defp sign_object(object, private_key) do
object = Map.drop(object, [:signatures, :unsigned])
with {:ok, json} <- MatrixServer.encode_canonical_json(object) do with {:ok, json} <- MatrixServer.encode_canonical_json(object) do
signature = signature =
json json

View file

@ -70,72 +70,85 @@ defmodule MatrixServer.RoomServer do
end end
@impl true @impl true
def handle_call( def handle_call({:create_room, account, input}, _from, %{room_id: room_id} = state) do
{:create_room, account, # TODO: power_level_content_override, initial_state, invite, invite_3pid
%CreateRoom{room_version: room_version, name: name, topic: topic, preset: preset}}, room = Repo.one!(from r in Room, where: r.id == ^room_id)
_from,
%{room_id: room_id} = state
) do
result =
Repo.transaction(fn ->
room = Repo.one!(from r in Room, where: r.id == ^room_id)
create_room = Event.create_room(room, account, room_version)
join_creator = Event.join(room, account, [create_room.event_id])
pls = Event.power_levels(room, account, [create_room.event_id, join_creator.event_id])
auth_events = [create_room.event_id, join_creator.event_id, pls.event_id]
name_event = if name, do: Event.name(room, account, name, auth_events)
topic_event = if topic, do: Event.topic(room, account, topic, auth_events)
# TODO: power_level_content_override, initial_state, invite, invite_3pid case create_room_events(room, account, input) do
events = events when is_list(events) ->
[create_room, join_creator, pls] ++ case Repo.transaction(create_room_insert_events(room, events)) do
room_creation_preset(account, preset, room, auth_events) ++ {:ok, state_set} -> {:reply, {:ok, room_id}, %{state | state_set: state_set}}
[name_event, topic_event] {:error, reason} -> {:reply, {:error, reason}, state}
_ -> {:reply, {:error, :unknown}, state}
result =
events
|> Enum.reject(&Kernel.is_nil/1)
|> Enum.reduce_while({%{}, room}, fn event, {state_set, room} ->
case verify_and_insert_event(event, state_set, room) do
{:ok, state_set, room} -> {:cont, {state_set, room}}
{:error, reason} -> {:halt, {:error, reason}}
end
end)
case result do
{:error, reason} ->
Repo.rollback(reason)
{state_set, room} ->
serialized_state_set =
Enum.map(state_set, fn {{type, state_key}, event} ->
[type, state_key, event.event_id]
end)
Repo.update!(change(room, state: serialized_state_set))
state_set
end end
end)
case result do :error ->
{:ok, state_set} -> {:reply, {:ok, room_id}, %{state | state_set: state_set}} {:reply, {:error, :event_creation}, state}
{:error, reason} -> {:reply, {:error, reason}, state}
_ -> {:reply, {:error, :unknown}, state}
end end
end end
def handle_call({:server_in_room, domain}, _from, %{state_set: state_set} = state) do def handle_call({:server_in_room, domain}, _from, %{state_set: state_set} = state) do
result = Enum.any?(state_set, fn result =
{{"m.room.member", user_id}, %Event{content: %{"membership" => "join"}}} -> Enum.any?(state_set, fn
MatrixServer.get_domain(user_id) == domain {{"m.room.member", user_id}, %Event{content: %{"membership" => "join"}}} ->
MatrixServer.get_domain(user_id) == domain
_ -> _ ->
false false
end) end)
{:reply, result, state} {:reply, result, state}
end end
defp create_room_events(room, account, %CreateRoom{
room_version: room_version,
name: name,
topic: topic,
preset: preset
}) do
with {:ok, create_room} <- Event.create_room(room, account, room_version),
{:ok, join_creator} <- Event.join(room, account, [create_room]),
{:ok, pls} <- Event.power_levels(room, account, [create_room, join_creator]),
auth_events <- [create_room, join_creator, pls],
{:ok, preset_events} <- room_creation_preset(account, preset, room, auth_events),
{:ok, name_event} <-
if(name, do: Event.name(room, account, name, auth_events), else: {:ok, nil}),
{:ok, topic_event} <-
if(topic, do: Event.topic(room, account, topic, auth_events), else: {:ok, nil}) do
events = [create_room, join_creator, pls] ++ preset_events ++ [name_event, topic_event]
Enum.reject(events, &Kernel.is_nil/1)
else
_ -> :error
end
end
defp create_room_insert_events(room, events) do
fn ->
result =
Enum.reduce_while(events, {%{}, room}, fn event, {state_set, room} ->
case verify_and_insert_event(event, state_set, room) do
{:ok, state_set, room} -> {:cont, {state_set, room}}
{:error, reason} -> {:halt, {:error, reason}}
end
end)
case result do
{:error, reason} ->
Repo.rollback(reason)
{state_set, room} ->
serialized_state_set =
Enum.map(state_set, fn {{type, state_key}, event} ->
[type, state_key, event.event_id]
end)
Repo.update!(change(room, state: serialized_state_set))
state_set
end
end
end
# TODO: trusted_private_chat: # TODO: trusted_private_chat:
# All invitees are given the same power level as the room creator. # All invitees are given the same power level as the room creator.
defp room_creation_preset(account, nil, %Room{visibility: visibility} = room, auth_events) do defp room_creation_preset(account, nil, %Room{visibility: visibility} = room, auth_events) do
@ -156,11 +169,11 @@ defmodule MatrixServer.RoomServer do
"public_chat" -> {"public", "shared", "forbidden"} "public_chat" -> {"public", "shared", "forbidden"}
end end
[ with {:ok, join_rules} <- Event.join_rules(room, account, join_rule, auth_events),
Event.join_rules(room, account, join_rule, auth_events), {:ok, his_vis} <- Event.history_visibility(room, account, his_vis, auth_events),
Event.history_visibility(room, account, his_vis, auth_events), {:ok, guest_access} <- Event.guest_access(room, account, guest_access, auth_events) do
Event.guest_access(room, account, guest_access, auth_events) {:ok, [join_rules, his_vis, guest_access]}
] end
end end
defp verify_and_insert_event( defp verify_and_insert_event(

View file

@ -3,7 +3,7 @@ defmodule MatrixServer.Event do
import Ecto.Query import Ecto.Query
alias MatrixServer.{Repo, Room, Event, Account, OrderedMap, KeyServer} alias MatrixServer.{Repo, Room, Event, Account, EncodableMap, KeyServer}
@primary_key {:event_id, :string, []} @primary_key {:event_id, :string, []}
schema "events" do schema "events" do
@ -26,46 +26,58 @@ defmodule MatrixServer.Event do
%Event{ %Event{
room_id: room_id, room_id: room_id,
sender: MatrixServer.get_mxid(localpart), sender: MatrixServer.get_mxid(localpart),
event_id: generate_event_id(),
origin_server_ts: DateTime.utc_now(), origin_server_ts: DateTime.utc_now(),
prev_events: [], prev_events: [],
auth_events: [] auth_events: []
} }
end end
def create_room(room, %Account{localpart: localpart} = creator, room_version, auth_events \\ []) do def create_room(
room,
%Account{localpart: localpart} = creator,
room_version,
generate_id \\ true
) do
mxid = MatrixServer.get_mxid(localpart) mxid = MatrixServer.get_mxid(localpart)
%Event{ event = %Event{
new(room, creator) new(room, creator)
| type: "m.room.create", | type: "m.room.create",
state_key: "", state_key: "",
content: %{ content: %{
"creator" => mxid, "creator" => mxid,
"room_version" => room_version || MatrixServer.default_room_version() "room_version" => room_version || MatrixServer.default_room_version()
}, }
auth_events: auth_events
} }
if generate_id, do: set_event_id(event), else: event
end end
def join(room, %Account{localpart: localpart} = sender, auth_events \\ []) do def join(room, %Account{localpart: localpart} = sender, auth_events, generate_id \\ true) do
mxid = MatrixServer.get_mxid(localpart) mxid = MatrixServer.get_mxid(localpart)
%Event{ event = %Event{
new(room, sender) new(room, sender)
| type: "m.room.member", | type: "m.room.member",
state_key: mxid, state_key: mxid,
content: %{ content: %{
"membership" => "join" "membership" => "join"
}, },
auth_events: auth_events auth_events: Enum.map(auth_events, & &1.event_id)
} }
if generate_id, do: set_event_id(event), else: event
end end
def power_levels(room, %Account{localpart: localpart} = sender, auth_events \\ []) do def power_levels(
room,
%Account{localpart: localpart} = sender,
auth_events,
generate_id \\ true
) do
mxid = MatrixServer.get_mxid(localpart) mxid = MatrixServer.get_mxid(localpart)
%Event{ event = %Event{
new(room, sender) new(room, sender)
| type: "m.room.power_levels", | type: "m.room.power_levels",
state_key: "", state_key: "",
@ -85,72 +97,80 @@ defmodule MatrixServer.Event do
"room" => 50 "room" => 50
} }
}, },
auth_events: auth_events auth_events: Enum.map(auth_events, & &1.event_id)
} }
if generate_id, do: set_event_id(event), else: event
end end
def name(room, sender, name, auth_events \\ []) do def name(room, sender, name, auth_events, generate_id \\ true) do
%Event{ event = %Event{
new(room, sender) new(room, sender)
| type: "m.room.name", | type: "m.room.name",
state_key: "", state_key: "",
content: %{ content: %{
"name" => name "name" => name
}, },
auth_events: auth_events auth_events: Enum.map(auth_events, & &1.event_id)
} }
if generate_id, do: set_event_id(event), else: event
end end
def topic(room, sender, topic, auth_events \\ []) do def topic(room, sender, topic, auth_events, generate_id \\ true) do
%Event{ event = %Event{
new(room, sender) new(room, sender)
| type: "m.room.topic", | type: "m.room.topic",
state_key: "", state_key: "",
content: %{ content: %{
"topic" => topic "topic" => topic
}, },
auth_events: auth_events auth_events: Enum.map(auth_events, & &1.event_id)
} }
if generate_id, do: set_event_id(event), else: event
end end
def join_rules(room, sender, join_rule, auth_events \\ []) do def join_rules(room, sender, join_rule, auth_events, generate_id \\ true) do
%Event{ event = %Event{
new(room, sender) new(room, sender)
| type: "m.room.join_rules", | type: "m.room.join_rules",
state_key: "", state_key: "",
content: %{ content: %{
"join_rule" => join_rule "join_rule" => join_rule
}, },
auth_events: auth_events auth_events: Enum.map(auth_events, & &1.event_id)
} }
if generate_id, do: set_event_id(event), else: event
end end
def history_visibility(room, sender, history_visibility, auth_events \\ []) do def history_visibility(room, sender, history_visibility, auth_events, generate_id \\ true) do
%Event{ event = %Event{
new(room, sender) new(room, sender)
| type: "m.room.history_visibility", | type: "m.room.history_visibility",
state_key: "", state_key: "",
content: %{ content: %{
"history_visibility" => history_visibility "history_visibility" => history_visibility
}, },
auth_events: auth_events auth_events: Enum.map(auth_events, & &1.event_id)
} }
if generate_id, do: set_event_id(event), else: event
end end
def guest_access(room, sender, guest_access, auth_events \\ []) do def guest_access(room, sender, guest_access, auth_events, generate_id \\ true) do
%Event{ event = %Event{
new(room, sender) new(room, sender)
| type: "m.room.guest_access", | type: "m.room.guest_access",
state_key: "", state_key: "",
content: %{ content: %{
"guest_access" => guest_access "guest_access" => guest_access
}, },
auth_events: auth_events auth_events: Enum.map(auth_events, & &1.event_id)
} }
end
def generate_event_id do if generate_id, do: set_event_id(event), else: event
"$" <> MatrixServer.random_string(17) <> ":" <> MatrixServer.server_name()
end end
def is_control_event(%Event{type: "m.room.power_levels", state_key: ""}), do: true def is_control_event(%Event{type: "m.room.power_levels", state_key: ""}), do: true
@ -275,7 +295,10 @@ defmodule MatrixServer.Event do
end end
def sign(event) do def sign(event) do
content_hash = calculate_content_hash(event) content_hash =
event
|> calculate_content_hash()
|> MatrixServer.encode_unpadded_base64()
event event
|> Map.put(:hashes, %{"sha256" => content_hash}) |> Map.put(:hashes, %{"sha256" => content_hash})
@ -284,20 +307,15 @@ defmodule MatrixServer.Event do
end end
defp calculate_content_hash(event) do defp calculate_content_hash(event) do
result = m =
event event
|> MatrixServer.to_serializable_map() |> MatrixServer.to_serializable_map()
|> Map.drop([:unsigned, :signature, :hashes]) |> Map.drop([:unsigned, :signature, :hashes])
|> OrderedMap.from_map() |> EncodableMap.from_map()
|> Jason.encode() |> Jason.encode()
case result do with {:ok, json} <- Jason.encode(m) do
{:ok, json} -> :crypto.hash(:sha256, json)
:crypto.hash(:sha256, json)
|> MatrixServer.encode_unpadded_base64()
error ->
error
end end
end end
@ -346,4 +364,29 @@ defmodule MatrixServer.Event do
"users", "users",
"users_default" "users_default"
]) ])
defp redact_content(_, _), do: %{}
def set_event_id(event) do
with {:ok, event_id} <- generate_event_id(event) do
{:ok, %Event{event | event_id: event_id}}
end
end
defp generate_event_id(event) do
with {:ok, hash} <- calculate_reference_hash(event) do
{:ok, "$" <> MatrixServer.encode_url_safe_base64(hash)}
end
end
defp calculate_reference_hash(event) do
redacted_event =
event
|> redact()
|> Map.drop([:unsigned, :signature, :age_ts])
with {:ok, json} <- MatrixServer.encode_canonical_json(redacted_event) do
{:ok, :crypto.hash(:sha256, json)}
end
end
end end

View file

@ -2,7 +2,9 @@ defmodule MatrixServerWeb.Federation.EventController do
use MatrixServerWeb, :controller use MatrixServerWeb, :controller
use MatrixServerWeb.Federation.AuthenticateServer use MatrixServerWeb.Federation.AuthenticateServer
def event(conn, %{"event_id" => event_id}) do def event(conn, %{"event_id" => _event_id}) do
conn
|> put_status(200)
|> json(%{})
end end
end end