diff --git a/lib/pleroma/keys.ex b/lib/pleroma/keys.ex deleted file mode 100644 index 413861b15..000000000 --- a/lib/pleroma/keys.ex +++ /dev/null @@ -1,46 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2021 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Keys do - # Native generation of RSA keys is only available since OTP 20+ and in default build conditions - # We try at compile time to generate natively an RSA key otherwise we fallback on the old way. - try do - _ = :public_key.generate_key({:rsa, 2048, 65_537}) - - def generate_rsa_pem do - key = :public_key.generate_key({:rsa, 2048, 65_537}) - entry = :public_key.pem_entry_encode(:RSAPrivateKey, key) - pem = :public_key.pem_encode([entry]) |> String.trim_trailing() - {:ok, pem} - end - rescue - _ -> - def generate_rsa_pem do - port = Port.open({:spawn, "openssl genrsa"}, [:binary]) - - {:ok, pem} = - receive do - {^port, {:data, pem}} -> {:ok, pem} - end - - Port.close(port) - - if Regex.match?(~r/RSA PRIVATE KEY/, pem) do - {:ok, pem} - else - :error - end - end - end - - def keys_from_pem(pem) do - with [private_key_code] <- :public_key.pem_decode(pem), - private_key <- :public_key.pem_entry_decode(private_key_code), - {:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} <- private_key do - {:ok, private_key, {:RSAPublicKey, modulus, exponent}} - else - error -> {:error, error} - end - end -end diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex index c4ac2c87e..bc3baf433 100644 --- a/lib/pleroma/signature.ex +++ b/lib/pleroma/signature.ex @@ -5,47 +5,25 @@ defmodule Pleroma.Signature do @behaviour HTTPSignatures.Adapter - alias Pleroma.EctoType.ActivityPub.ObjectValidators - alias Pleroma.Keys alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - - @known_suffixes ["/publickey", "/main-key", "#key"] + alias Pleroma.User.SigningKey + require Logger def key_id_to_actor_id(key_id) do - uri = - key_id - |> URI.parse() - |> Map.put(:fragment, nil) - |> Map.put(:query, nil) - |> remove_suffix(@known_suffixes) + case SigningKey.key_id_to_ap_id(key_id) do + nil -> + # hm, we SHOULD have gotten this in the pipeline before we hit here! + Logger.error("Could not figure out who owns the key #{key_id}") + {:error, :key_owner_not_found} - maybe_ap_id = URI.to_string(uri) - - case ObjectValidators.ObjectID.cast(maybe_ap_id) do - {:ok, ap_id} -> - {:ok, ap_id} - - _ -> - case Pleroma.Web.WebFinger.finger(maybe_ap_id) do - {:ok, %{"ap_id" => ap_id}} -> {:ok, ap_id} - _ -> {:error, maybe_ap_id} - end + key -> + {:ok, key} end end - defp remove_suffix(uri, [test | rest]) do - if not is_nil(uri.path) and String.ends_with?(uri.path, test) do - Map.put(uri, :path, String.replace(uri.path, test, "")) - else - remove_suffix(uri, rest) - end - end - - defp remove_suffix(uri, []), do: uri - def fetch_public_key(conn) do with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), + {:ok, %SigningKey{}} <- SigningKey.get_or_fetch_by_key_id(kid), {:ok, actor_id} <- key_id_to_actor_id(kid), {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do {:ok, public_key} @@ -57,8 +35,8 @@ def fetch_public_key(conn) do def refetch_public_key(conn) do with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), + {:ok, %SigningKey{}} <- SigningKey.get_or_fetch_by_key_id(kid), {:ok, actor_id} <- key_id_to_actor_id(kid), - {:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id), {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do {:ok, public_key} else @@ -67,9 +45,9 @@ def refetch_public_key(conn) do end end - def sign(%User{keys: keys} = user, headers) do - with {:ok, private_key, _} <- Keys.keys_from_pem(keys) do - HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers) + def sign(%User{} = user, headers) do + with {:ok, private_key} <- SigningKey.private_key(user) do + HTTPSignatures.sign(private_key, SigningKey.local_key_id(user.ap_id), headers) end end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 2bc3e9ace..bf8717ffb 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -25,7 +25,6 @@ defmodule Pleroma.User do alias Pleroma.Hashtag alias Pleroma.User.HashtagFollow alias Pleroma.HTML - alias Pleroma.Keys alias Pleroma.MFA alias Pleroma.Notification alias Pleroma.Object @@ -43,6 +42,7 @@ defmodule Pleroma.User do alias Pleroma.Web.OAuth alias Pleroma.Web.RelMe alias Pleroma.Workers.BackgroundWorker + alias Pleroma.User.SigningKey use Pleroma.Web, :verified_routes @@ -101,7 +101,6 @@ defmodule Pleroma.User do field(:password, :string, virtual: true) field(:password_confirmation, :string, virtual: true) field(:keys, :string) - field(:public_key, :string) field(:ap_id, :string) field(:avatar, :map, default: %{}) field(:local, :boolean, default: true) @@ -222,6 +221,10 @@ defmodule Pleroma.User do on_replace: :delete ) + # FOR THE FUTURE: We might want to make this a one-to-many relationship + # it's entirely possible right now, but we don't have a use case for it + has_one(:signing_key, SigningKey, foreign_key: :user_id) + timestamps() end @@ -457,6 +460,7 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do |> fix_follower_address() struct + |> Repo.preload(:signing_key) |> cast( params, [ @@ -466,7 +470,6 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do :inbox, :shared_inbox, :nickname, - :public_key, :avatar, :ap_enabled, :banner, @@ -495,6 +498,7 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do |> validate_required([:ap_id]) |> validate_required([:name], trim: false) |> unique_constraint(:nickname) + |> cast_assoc(:signing_key, with: &SigningKey.remote_changeset/2, required: false) |> validate_format(:nickname, @email_regex) |> validate_length(:bio, max: bio_limit) |> validate_length(:name, max: name_limit) @@ -526,7 +530,6 @@ def update_changeset(struct, params \\ %{}) do :name, :emoji, :avatar, - :public_key, :inbox, :shared_inbox, :is_locked, @@ -570,6 +573,7 @@ def update_changeset(struct, params \\ %{}) do :pleroma_settings_store, &{:ok, Map.merge(struct.pleroma_settings_store, &1)} ) + |> cast_assoc(:signing_key, with: &SigningKey.remote_changeset/2, requred: false) |> validate_fields(false, struct) end @@ -828,8 +832,10 @@ def put_following_and_follower_and_featured_address(changeset) do end defp put_private_key(changeset) do - {:ok, pem} = Keys.generate_rsa_pem() - put_change(changeset, :keys, pem) + ap_id = get_field(changeset, :ap_id) + + changeset + |> put_assoc(:signing_key, SigningKey.generate_local_keys(ap_id)) end defp autofollow_users(user) do @@ -1146,7 +1152,8 @@ def update_and_set_cache(%{data: %Pleroma.User{} = user} = changeset) do was_superuser_before_update = User.superuser?(user) with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do - set_cache(user) + user + |> set_cache() end |> maybe_remove_report_notifications(was_superuser_before_update) end @@ -2051,24 +2058,16 @@ defp create_service_actor(uri, nickname) do |> set_cache() end - def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do - key = - public_key_pem - |> :public_key.pem_decode() - |> hd() - |> :public_key.pem_entry_decode() - - {:ok, key} - end - - def public_key(_), do: {:error, "key not found"} + defdelegate public_key(user), to: SigningKey def get_public_key_for_ap_id(ap_id) do with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id), - {:ok, public_key} <- public_key(user) do + {:ok, public_key} <- SigningKey.public_key(user) do {:ok, public_key} else - _ -> :error + e -> + Logger.error("Could not get public key for #{ap_id}.\n#{inspect(e)}") + {:error, e} end end diff --git a/lib/pleroma/user/signing_key.ex b/lib/pleroma/user/signing_key.ex new file mode 100644 index 000000000..f25489068 --- /dev/null +++ b/lib/pleroma/user/signing_key.ex @@ -0,0 +1,262 @@ +defmodule Pleroma.User.SigningKey do + use Ecto.Schema + import Ecto.Query + import Ecto.Changeset + require Pleroma.Constants + alias Pleroma.User + alias Pleroma.Repo + + require Logger + + @primary_key false + schema "signing_keys" do + belongs_to(:user, Pleroma.User, type: FlakeId.Ecto.CompatType) + field :public_key, :string + field :private_key, :string + # This is an arbitrary field given by the remote instance + field :key_id, :string, primary_key: true + timestamps() + end + + def load_key(%User{} = user) do + user + |> Repo.preload(:signing_key) + end + + def key_id_of_local_user(%User{local: true} = user) do + case Repo.preload(user, :signing_key) do + %User{signing_key: %__MODULE__{key_id: key_id}} -> key_id + _ -> nil + end + end + + @spec remote_changeset(__MODULE__, map) :: Changeset.t() + def remote_changeset(%__MODULE__{} = signing_key, attrs) do + signing_key + |> cast(attrs, [:public_key, :key_id]) + |> validate_required([:public_key, :key_id]) + end + + @spec key_id_to_user_id(String.t()) :: String.t() | nil + @doc """ + Given a key ID, return the user ID associated with that key. + Returns nil if the key ID is not found. + """ + def key_id_to_user_id(key_id) do + from(sk in __MODULE__, where: sk.key_id == ^key_id) + |> select([sk], sk.user_id) + |> Repo.one() + end + + @spec key_id_to_ap_id(String.t()) :: String.t() | nil + @doc """ + Given a key ID, return the AP ID associated with that key. + Returns nil if the key ID is not found. + """ + def key_id_to_ap_id(key_id) do + Logger.debug("Looking up key ID: #{key_id}") + + result = + from(sk in __MODULE__, where: sk.key_id == ^key_id) + |> join(:inner, [sk], u in User, on: sk.user_id == u.id) + |> select([sk, u], %{user: u}) + |> Repo.one() + + case result do + %{user: %User{ap_id: ap_id}} -> ap_id + _ -> nil + end + end + + @spec generate_rsa_pem() :: {:ok, binary()} + @doc """ + Generate a new RSA private key and return it as a PEM-encoded string. + """ + def generate_rsa_pem do + key = :public_key.generate_key({:rsa, 2048, 65_537}) + entry = :public_key.pem_entry_encode(:RSAPrivateKey, key) + pem = :public_key.pem_encode([entry]) |> String.trim_trailing() + {:ok, pem} + end + + @spec generate_local_keys(String.t()) :: {:ok, Changeset.t()} | {:error, String.t()} + @doc """ + Generate a new RSA key pair and create a changeset for it + """ + def generate_local_keys(ap_id) do + {:ok, private_pem} = generate_rsa_pem() + {:ok, local_pem} = private_pem_to_public_pem(private_pem) + + %__MODULE__{} + |> change() + |> put_change(:public_key, local_pem) + |> put_change(:private_key, private_pem) + |> put_change(:key_id, local_key_id(ap_id)) + end + + @spec local_key_id(String.t()) :: String.t() + @doc """ + Given an AP ID, return the key ID for the local user. + """ + def local_key_id(ap_id) do + ap_id <> "#main-key" + end + + @spec private_pem_to_public_pem(binary) :: {:ok, binary()} | {:error, String.t()} + @doc """ + Given a private key in PEM format, return the corresponding public key in PEM format. + """ + def private_pem_to_public_pem(private_pem) do + [private_key_code] = :public_key.pem_decode(private_pem) + private_key = :public_key.pem_entry_decode(private_key_code) + {:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} = private_key + public_key = {:RSAPublicKey, modulus, exponent} + public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key) + {:ok, :public_key.pem_encode([public_key])} + end + + @spec public_key(User.t()) :: {:ok, binary()} | {:error, String.t()} + @doc """ + Given a user, return the public key for that user in binary format. + """ + def public_key(%User{} = user) do + case Repo.preload(user, :signing_key) do + %User{signing_key: %__MODULE__{public_key: public_key_pem}} -> + key = + public_key_pem + |> :public_key.pem_decode() + |> hd() + |> :public_key.pem_entry_decode() + + {:ok, key} + + _ -> + {:error, "key not found"} + end + end + + def public_key(_), do: {:error, "key not found"} + + def public_key_pem(%User{} = user) do + case Repo.preload(user, :signing_key) do + %User{signing_key: %__MODULE__{public_key: public_key_pem}} -> {:ok, public_key_pem} + _ -> {:error, "key not found"} + end + end + + def public_key_pem(_e) do + {:error, "key not found"} + end + + @spec private_key(User.t()) :: {:ok, binary()} | {:error, String.t()} + @doc """ + Given a user, return the private key for that user in binary format. + """ + def private_key(%User{} = user) do + case Repo.preload(user, :signing_key) do + %{signing_key: %__MODULE__{private_key: private_key_pem}} -> + key = + private_key_pem + |> :public_key.pem_decode() + |> hd() + |> :public_key.pem_entry_decode() + + {:ok, key} + + _ -> + {:error, "key not found"} + end + end + + @spec get_or_fetch_by_key_id(String.t()) :: {:ok, __MODULE__} | {:error, String.t()} + @doc """ + Given a key ID, return the signing key associated with that key. + Will either return the key if it exists locally, or fetch it from the remote instance. + """ + def get_or_fetch_by_key_id(key_id) do + case key_id_to_user_id(key_id) do + nil -> + fetch_remote_key(key_id) + + user_id -> + {:ok, Repo.get_by(__MODULE__, user_id: user_id)} + end + end + + @spec fetch_remote_key(String.t()) :: {:ok, __MODULE__} | {:error, String.t()} + @doc """ + Fetch a remote key by key ID. + Will send a request to the remote instance to get the key ID. + This request should, at the very least, return a user ID and a public key object. + Though bear in mind that some implementations (looking at you, pleroma) may require a signature for this request. + This has the potential to create an infinite loop if the remote instance requires a signature to fetch the key... + So if we're rejected, we should probably just give up. + """ + def fetch_remote_key(key_id) do + Logger.debug("Fetching remote key: #{key_id}") + resp = Pleroma.Object.Fetcher.fetch_and_contain_remote_object_from_id(key_id) + + case resp do + {:ok, _body} -> + case handle_signature_response(resp) do + {:ok, ap_id, public_key_pem} -> + Logger.debug("Fetched remote key: #{ap_id}") + # fetch the user + {:ok, user} = User.get_or_fetch_by_ap_id(ap_id) + # store the key + key = %__MODULE__{ + user_id: user.id, + public_key: public_key_pem, + key_id: key_id + } + + Repo.insert(key, on_conflict: :replace_all, conflict_target: :key_id) + + e -> + Logger.debug("Failed to fetch remote key: #{inspect(e)}") + {:error, "Could not fetch key"} + end + + _ -> + Logger.debug("Failed to fetch remote key: #{inspect(resp)}") + {:error, "Could not fetch key"} + end + end + + # Take the response from the remote instance and extract the key details + # will check if the key ID matches the owner of the key, if not, error + defp extract_key_details(%{"id" => ap_id, "publicKey" => public_key}) do + if ap_id !== public_key["owner"] do + {:error, "Key ID does not match owner"} + else + %{"publicKeyPem" => public_key_pem} = public_key + {:ok, ap_id, public_key_pem} + end + end + + defp handle_signature_response({:ok, body}) do + case body do + %{ + "type" => "CryptographicKey", + "publicKeyPem" => public_key_pem, + "owner" => ap_id + } -> + {:ok, ap_id, public_key_pem} + + # for when we get a subset of the user object + %{ + "id" => _user_id, + "publicKey" => _public_key, + "type" => actor_type + } + when actor_type in Pleroma.Constants.actor_types() -> + extract_key_details(body) + + %{"error" => error} -> + {:error, error} + end + end + + defp handle_signature_response({:error, e}), do: {:error, e} + defp handle_signature_response(other), do: {:error, "Could not fetch key: #{inspect(other)}"} +end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index c87072300..9b28e64d9 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1547,6 +1547,17 @@ defp normalize_attachment(%{} = attachment), do: [attachment] defp normalize_attachment(attachment) when is_list(attachment), do: attachment defp normalize_attachment(_), do: [] + defp maybe_make_public_key_object(data) do + if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do + %{ + public_key: data["publicKey"]["publicKeyPem"], + key_id: data["publicKey"]["id"] + } + else + nil + end + end + defp object_to_user_data(data, additional) do fields = data @@ -1578,9 +1589,16 @@ defp object_to_user_data(data, additional) do featured_address = data["featured"] {:ok, pinned_objects} = fetch_and_prepare_featured_from_ap_id(featured_address) - public_key = - if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do - data["publicKey"]["publicKeyPem"] + # first, check that the owner is correct + signing_key = + if data["id"] !== data["publicKey"]["owner"] do + Logger.error( + "Owner of the public key is not the same as the actor - not saving the public key." + ) + + nil + else + maybe_make_public_key_object(data) end shared_inbox = @@ -1624,7 +1642,7 @@ defp object_to_user_data(data, additional) do bio: data["summary"] || "", actor_type: actor_type, also_known_as: also_known_as, - public_key: public_key, + signing_key: signing_key, inbox: data["inbox"], shared_inbox: shared_inbox, pinned_objects: pinned_objects, diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 6642f7771..9c6b3655d 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -60,7 +60,26 @@ defp relay_active?(conn, _) do end end - def user(conn, %{"nickname" => nickname}) do + @doc """ + Render the user's AP data + WARNING: we cannot actually check if the request has a fragment! so let's play defensively + - IF we have a valid signature, serve full user + - IF we do not, and authorized_fetch_mode is enabled, serve the key only + - OTHERWISE, serve the full actor (since we don't need to worry about the signature) + """ + def user(%{assigns: %{valid_signature: true}} = conn, params) do + render_full_user(conn, params) + end + + def user(conn, params) do + if Pleroma.Config.get([:activitypub, :authorized_fetch_mode], false) do + render_key_only_user(conn, params) + else + render_full_user(conn, params) + end + end + + defp render_full_user(conn, %{"nickname" => nickname}) do with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do conn |> put_resp_content_type("application/activity+json") @@ -72,6 +91,18 @@ def user(conn, %{"nickname" => nickname}) do end end + def render_key_only_user(conn, %{"nickname" => nickname}) do + with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do + conn + |> put_resp_content_type("application/activity+json") + |> put_view(UserView) + |> render("keys.json", %{user: user}) + else + nil -> {:error, :not_found} + %{local: false} -> {:error, :not_found} + end + end + def object(%{assigns: assigns} = conn, _) do with ap_id <- Endpoint.url() <> conn.request_path, %Object{} = object <- Object.get_cached_by_ap_id(ap_id), diff --git a/lib/pleroma/web/activity_pub/object_validators/user_validator.ex b/lib/pleroma/web/activity_pub/object_validators/user_validator.ex index adb291a55..b80068e37 100644 --- a/lib/pleroma/web/activity_pub/object_validators/user_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/user_validator.ex @@ -14,7 +14,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UserValidator do @behaviour Pleroma.Web.ActivityPub.ObjectValidator.Validating alias Pleroma.Object.Containment - alias Pleroma.Signature require Pleroma.Constants @@ -23,8 +22,7 @@ def validate(object, meta) def validate(%{"type" => type, "id" => _id} = data, meta) when type in Pleroma.Constants.actor_types() do - with :ok <- validate_pubkey(data), - :ok <- validate_inbox(data), + with :ok <- validate_inbox(data), :ok <- contain_collection_origin(data) do {:ok, data, meta} else @@ -35,33 +33,6 @@ def validate(%{"type" => type, "id" => _id} = data, meta) def validate(_, _), do: {:error, "Not a user object"} - defp mabye_validate_owner(nil, _actor), do: :ok - defp mabye_validate_owner(actor, actor), do: :ok - defp mabye_validate_owner(_owner, _actor), do: :error - - defp validate_pubkey( - %{"id" => id, "publicKey" => %{"id" => pk_id, "publicKeyPem" => _key}} = data - ) - when id != nil do - with {_, {:ok, kactor}} <- {:key, Signature.key_id_to_actor_id(pk_id)}, - true <- id == kactor, - :ok <- mabye_validate_owner(Map.get(data, "owner"), id) do - :ok - else - {:key, _} -> - {:error, "Unable to determine actor id from key id"} - - false -> - {:error, "Key id does not relate to user id"} - - _ -> - {:error, "Actor does not own its public key"} - end - end - - # pubkey is optional atm - defp validate_pubkey(_data), do: :ok - defp validate_inbox(%{"id" => id, "inbox" => inbox}) do case Containment.same_origin(id, inbox) do :ok -> :ok diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 47b8e37e5..2ca31fc3c 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -5,7 +5,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do use Pleroma.Web, :view - alias Pleroma.Keys alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User @@ -33,9 +32,7 @@ def render("endpoints.json", %{user: %User{local: true} = _user}) do def render("endpoints.json", _), do: %{} def render("service.json", %{user: user}) do - {:ok, _, public_key} = Keys.keys_from_pem(user.keys) - public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key) - public_key = :public_key.pem_encode([public_key]) + {:ok, public_key} = User.SigningKey.public_key_pem(user) endpoints = render("endpoints.json", %{user: user}) @@ -52,7 +49,7 @@ def render("service.json", %{user: user}) do "url" => user.ap_id, "manuallyApprovesFollowers" => false, "publicKey" => %{ - "id" => "#{user.ap_id}#main-key", + "id" => User.SigningKey.local_key_id(user.ap_id), "owner" => user.ap_id, "publicKeyPem" => public_key }, @@ -70,9 +67,12 @@ def render("user.json", %{user: %User{nickname: "internal." <> _} = user}), do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname) def render("user.json", %{user: user}) do - {:ok, _, public_key} = Keys.keys_from_pem(user.keys) - public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key) - public_key = :public_key.pem_encode([public_key]) + public_key = + case User.SigningKey.public_key_pem(user) do + {:ok, public_key} -> public_key + _ -> nil + end + user = User.sanitize_html(user) endpoints = render("endpoints.json", %{user: user}) @@ -97,7 +97,7 @@ def render("user.json", %{user: user}) do "url" => user.ap_id, "manuallyApprovesFollowers" => user.is_locked, "publicKey" => %{ - "id" => "#{user.ap_id}#main-key", + "id" => User.SigningKey.local_key_id(user.ap_id), "owner" => user.ap_id, "publicKeyPem" => public_key }, @@ -116,6 +116,20 @@ def render("user.json", %{user: user}) do |> Map.merge(Utils.make_json_ld_header()) end + def render("keys.json", %{user: user}) do + {:ok, public_key} = User.SigningKey.public_key_pem(user) + + %{ + "id" => user.ap_id, + "publicKey" => %{ + "id" => User.SigningKey.key_id_of_local_user(user), + "owner" => user.ap_id, + "publicKeyPem" => public_key + } + } + |> Map.merge(Utils.make_json_ld_header()) + end + def render("following.json", %{user: user, page: page} = opts) do showing_items = (opts[:for] && opts[:for] == user) || !user.hide_follows showing_count = showing_items || !user.hide_follows_count diff --git a/lib/pleroma/web/plugs/ensure_user_public_key_plug.ex b/lib/pleroma/web/plugs/ensure_user_public_key_plug.ex new file mode 100644 index 000000000..5b090f289 --- /dev/null +++ b/lib/pleroma/web/plugs/ensure_user_public_key_plug.ex @@ -0,0 +1,32 @@ +defmodule Pleroma.Web.Plugs.EnsureUserPublicKeyPlug do + @moduledoc """ + This plug will attempt to pull in a user's public key if we do not have it. + We _should_ be able to request the URL from the key URL... + """ + + alias Pleroma.User + + def init(options), do: options + + def call(conn, _opts) do + key_id = key_id_from_conn(conn) + + unless is_nil(key_id) do + User.SigningKey.get_or_fetch_by_key_id(key_id) + # now we SHOULD have the user that owns the key locally. maybe. + # if we don't, we'll error out when we try to validate. + end + + conn + end + + defp key_id_from_conn(conn) do + case HTTPSignatures.signature_for_conn(conn) do + %{"keyId" => key_id} when is_binary(key_id) -> + key_id + + _ -> + nil + end + end +end diff --git a/lib/pleroma/web/plugs/http_signature_plug.ex b/lib/pleroma/web/plugs/http_signature_plug.ex index e3ae99636..06527cada 100644 --- a/lib/pleroma/web/plugs/http_signature_plug.ex +++ b/lib/pleroma/web/plugs/http_signature_plug.ex @@ -139,12 +139,17 @@ defp maybe_require_signature( defp maybe_require_signature(conn), do: conn defp signature_host(conn) do - with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), - {:ok, actor_id} <- Signature.key_id_to_actor_id(kid) do + with {:key_id, %{"keyId" => kid}} <- {:key_id, HTTPSignatures.signature_for_conn(conn)}, + {:actor_id, {:ok, actor_id}} <- {:actor_id, Signature.key_id_to_actor_id(kid)} do actor_id else - e -> - {:error, e} + {:key_id, e} -> + Logger.error("Failed to extract key_id from signature: #{inspect(e)}") + nil + + {:actor_id, e} -> + Logger.error("Failed to extract actor_id from signature: #{inspect(e)}") + nil end end end diff --git a/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex b/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex index a73def682..0d5cb9eab 100644 --- a/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex +++ b/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex @@ -4,7 +4,6 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do alias Pleroma.Helpers.AuthHelper - alias Pleroma.Signature alias Pleroma.User alias Pleroma.Web.ActivityPub.Utils @@ -33,7 +32,7 @@ def call(%{assigns: %{valid_signature: true}, params: %{"actor" => actor}} = con |> assign(:valid_signature, false) # remove me once testsuite uses mapped capabilities instead of what we do now - {:user, nil} -> + {:user, _} -> Logger.debug("Failed to map identity from signature (lookup failure)") Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}") @@ -68,7 +67,8 @@ def call(%{assigns: %{valid_signature: true}} = conn, _opts) do Logger.debug("Failed to map identity from signature (lookup failure)") Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}") - only_permit_user_routes(conn) + conn + |> assign(:valid_signature, false) _ -> Logger.debug("Failed to map identity from signature (no payload actor mismatch)") @@ -82,33 +82,34 @@ def call(%{assigns: %{valid_signature: true}} = conn, _opts) do # no signature at all def call(conn, _opts), do: conn - defp only_permit_user_routes(%{path_info: ["users", _]} = conn) do - conn - |> assign(:limited_ap, true) - end - - defp only_permit_user_routes(conn) do - conn - |> assign(:valid_signature, false) - end - defp key_id_from_conn(conn) do - with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn), - {:ok, ap_id} <- Signature.key_id_to_actor_id(key_id) do - ap_id - else + case HTTPSignatures.signature_for_conn(conn) do + %{"keyId" => key_id} when is_binary(key_id) -> + key_id + _ -> nil end end defp user_from_key_id(conn) do - with key_actor_id when is_binary(key_actor_id) <- key_id_from_conn(conn), - {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(key_actor_id) do + with {:key_id, key_id} when is_binary(key_id) <- {:key_id, key_id_from_conn(conn)}, + {:mapped_ap_id, ap_id} when is_binary(ap_id) <- + {:mapped_ap_id, User.SigningKey.key_id_to_ap_id(key_id)}, + {:user_fetch, {:ok, %User{} = user}} <- {:user_fetch, User.get_or_fetch_by_ap_id(ap_id)} do user else - _ -> - nil + {:key_id, nil} -> + Logger.debug("Failed to map identity from signature (no key ID)") + {:key_id, nil} + + {:mapped_ap_id, nil} -> + Logger.debug("Failed to map identity from signature (could not map key ID to AP ID)") + {:mapped_ap_id, nil} + + {:user_fetch, {:error, _}} -> + Logger.debug("Failed to map identity from signature (lookup failure)") + {:user_fetch, nil} end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 49ab3540b..c227d0d4a 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -144,7 +144,14 @@ defmodule Pleroma.Web.Router do }) end + pipeline :optional_http_signature do + plug(Pleroma.Web.Plugs.EnsureUserPublicKeyPlug) + plug(Pleroma.Web.Plugs.HTTPSignaturePlug) + plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug) + end + pipeline :http_signature do + plug(Pleroma.Web.Plugs.EnsureUserPublicKeyPlug) plug(Pleroma.Web.Plugs.HTTPSignaturePlug) plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug) plug(Pleroma.Web.Plugs.EnsureHTTPSignaturePlug) @@ -745,7 +752,7 @@ defmodule Pleroma.Web.Router do scope "/", Pleroma.Web do # Note: html format is supported only if static FE is enabled # Note: http signature is only considered for json requests (no auth for non-json requests) - pipe_through([:accepts_html_xml_json, :http_signature, :static_fe]) + pipe_through([:accepts_html_xml_json, :optional_http_signature, :static_fe]) # Note: returns user _profile_ for json requests, redirects to user _feed_ for non-json ones get("/users/:nickname", Feed.UserController, :feed_redirect, as: :user_feed) diff --git a/mix.exs b/mix.exs index 7ffc450e2..ff888fd35 100644 --- a/mix.exs +++ b/mix.exs @@ -144,7 +144,7 @@ defp deps do {:ex_aws, "~> 2.4"}, {:ex_aws_s3, "~> 2.4"}, {:sweet_xml, "~> 0.7"}, - {:earmark, "~> 1.4"}, + {:earmark, "1.4.46"}, {:bbcode_pleroma, "~> 0.2.0"}, {:argon2_elixir, "~> 3.1"}, {:cors_plug, "~> 3.0"}, @@ -200,7 +200,7 @@ defp deps do ## dev & test {:ex_doc, "~> 0.30", only: :dev, runtime: false}, - {:ex_machina, "~> 2.7", only: :test}, + {:ex_machina, "~> 2.8", only: :test}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:mock, "~> 0.3.8", only: :test}, {:excoveralls, "0.16.1", only: :test}, diff --git a/mix.lock b/mix.lock index 09997f29d..04cc0aa0a 100644 --- a/mix.lock +++ b/mix.lock @@ -8,50 +8,50 @@ "cachex": {:hex, :cachex, "3.6.0", "14a1bfbeee060dd9bec25a5b6f4e4691e3670ebda28c8ba2884b12fe30b36bf8", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "ebf24e373883bc8e0c8d894a63bbe102ae13d918f790121f5cfe6e485cc8e2e2"}, "calendar": {:hex, :calendar, "1.0.0", "f52073a708528482ec33d0a171954ca610fe2bd28f1e871f247dc7f1565fa807", [:mix], [{:tzdata, "~> 0.1.201603 or ~> 0.5.20 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "990e9581920c82912a5ee50e62ff5ef96da6b15949a2ee4734f935fdef0f0a6f"}, "captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "90f6ce7672f70f56708792a98d98bd05176c9176", [ref: "90f6ce7672f70f56708792a98d98bd05176c9176"]}, - "castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"}, + "castore": {:hex, :castore, "1.0.9", "5cc77474afadf02c7c017823f460a17daa7908e991b0cc917febc90e466a375c", [:mix], [], "hexpm", "5ea956504f1ba6f2b4eb707061d8e17870de2bee95fb59d512872c2ef06925e7"}, "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, - "comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"}, + "comeonin": {:hex, :comeonin, "5.5.0", "364d00df52545c44a139bad919d7eacb55abf39e86565878e17cebb787977368", [:mix], [], "hexpm", "6287fc3ba0aad34883cbe3f7949fc1d1e738e5ccdce77165bc99490aa69f47fb"}, "concurrent_limiter": {:git, "https://akkoma.dev/AkkomaGang/concurrent-limiter.git", "a9e0b3d64574bdba761f429bb4fba0cf687b3338", [ref: "a9e0b3d64574bdba761f429bb4fba0cf687b3338"]}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, "cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"}, "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, - "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, + "credo": {:hex, :credo, "1.7.8", "9722ba1681e973025908d542ec3d95db5f9c549251ba5b028e251ad8c24ab8c5", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cb9e87cc64f152f3ed1c6e325e7b894dea8f5ef2e41123bd864e3cd5ceb44968"}, "custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"}, - "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, + "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, - "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, + "dialyxir": {:hex, :dialyxir, "1.4.4", "fb3ce8741edeaea59c9ae84d5cec75da00fa89fe401c72d6e047d11a61f65f70", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "cd6111e8017ccd563e65621a4d9a4a1c5cd333df30cebc7face8029cacb4eff6"}, "earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, "eblurhash": {:hex, :eblurhash, "1.2.2", "7da4255aaea984b31bb71155f673257353b0e0554d0d30dcf859547e74602582", [:rebar3], [], "hexpm", "8c20ca00904de023a835a9dcb7b7762fed32264c85a80c3cafa85288e405044c"}, "ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"}, "ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"}, - "ecto_psql_extras": {:hex, :ecto_psql_extras, "0.8.0", "440719cd74f09b3f01c84455707a2c3972b725c513808e68eb6c5b0ab82bf523", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 0.18.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1 or ~> 4.0.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "f1512812dc196bcb932a96c82e55f69b543dc125e9d39f5e3631a9c4ec65ef12"}, + "ecto_psql_extras": {:hex, :ecto_psql_extras, "0.8.2", "79350a53246ac5ec27326d208496aebceb77fa82a91744f66a9154560f0759d3", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "> 0.16.0 and < 0.20.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1 or ~> 4.0.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "6149c1c4a5ba6602a76cb09ee7a269eb60dab9694a1dbbb797f032555212de75"}, "ecto_sql": {:hex, :ecto_sql, "3.10.2", "6b98b46534b5c2f8b8b5f03f126e75e2a73c64f3c071149d32987a5378b0fdbd", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "68c018debca57cb9235e3889affdaec7a10616a4e3a80c99fa1d01fdafaa9007"}, "elasticsearch": {:git, "https://akkoma.dev/AkkomaGang/elasticsearch-elixir.git", "6cd946f75f6ab9042521a009d1d32d29a90113ca", [ref: "main"]}, "elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"}, "elixir_xml_to_map": {:hex, :elixir_xml_to_map, "3.1.0", "4d6260486a8cce59e4bf3575fe2dd2a24766546ceeef9f93fcec6f7c62a2827a", [:mix], [{:erlsom, "~> 1.4", [hex: :erlsom, repo: "hexpm", optional: false]}], "hexpm", "8fe5f2e75f90bab07ee2161120c2dc038ebcae8135554f5582990f1c8c21f911"}, - "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "erlsom": {:hex, :erlsom, "1.5.1", "c8fe2babd33ff0846403f6522328b8ab676f896b793634cfe7ef181c05316c03", [:rebar3], [], "hexpm", "7965485494c5844dd127656ac40f141aadfa174839ec1be1074e7edf5b4239eb"}, "eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"}, - "ex_aws": {:hex, :ex_aws, "2.5.4", "86c5bb870a49e0ab6f5aa5dd58cf505f09d2624ebe17530db3c1b61c88a673af", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e82bd0091bb9a5bb190139599f922ff3fc7aebcca4374d65c99c4e23aa6d1625"}, - "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.3", "422468e5c3e1a4da5298e66c3468b465cfd354b842e512cb1f6fbbe4e2f5bdaf", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "4f09dd372cc386550e484808c5ac5027766c8d0cd8271ccc578b82ee6ef4f3b8"}, + "ex_aws": {:hex, :ex_aws, "2.5.6", "6f642e0f82eff10a9b470044f084b81a791cf15b393d647ea5f3e65da2794e3d", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.3", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c69eec59e31fdd89d0beeb1d97e16518dd1b23ad95b3d5c9f1dcfec23d97f960"}, + "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.4", "87aaf4a2f24a48f516d7f5aaced9d128dd5d0f655c4431f9037a11a85c71109c", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "c06e7f68b33f7c0acba1361dbd951c79661a28f85aa2e0582990fccca4425355"}, "ex_const": {:hex, :ex_const, "0.3.0", "9d79516679991baf540ef445438eef1455ca91cf1a3c2680d8fb9e5bea2fe4de", [:mix], [], "hexpm", "76546322abb9e40ee4a2f454cf1c8a5b25c3672fa79bed1ea52c31e0d2428ca9"}, - "ex_doc": {:hex, :ex_doc, "0.34.0", "ab95e0775db3df71d30cf8d78728dd9261c355c81382bcd4cefdc74610bef13e", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "60734fb4c1353f270c3286df4a0d51e65a2c1d9fba66af3940847cc65a8066d7"}, - "ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"}, + "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, + "ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"}, "ex_syslogger": {:hex, :ex_syslogger, "2.0.0", "de6de5c5472a9c4fdafb28fa6610e381ae79ebc17da6490b81d785d68bd124c9", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "a52b2fe71764e9e6ecd149ab66635812f68e39279cbeee27c52c0e35e8b8019e"}, "excoveralls": {:hex, :excoveralls, "0.16.1", "0bd42ed05c7d2f4d180331a20113ec537be509da31fed5c8f7047ce59ee5a7c5", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "dae763468e2008cf7075a64cb1249c97cb4bc71e236c5c2b5e5cdf1cfa2bf138"}, "expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"}, "fast_html": {:hex, :fast_html, "2.3.0", "08c1d8ead840dd3060ba02c761bed9f37f456a1ddfe30bcdcfee8f651cec06a6", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "f18e3c7668f82d3ae0b15f48d48feeb257e28aa5ab1b0dbf781c7312e5da029d"}, "fast_sanitize": {:hex, :fast_sanitize, "0.2.3", "67b93dfb34e302bef49fec3aaab74951e0f0602fd9fa99085987af05bd91c7a5", [:mix], [{:fast_html, "~> 2.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "e8ad286d10d0386e15d67d0ee125245ebcfbc7d7290b08712ba9013c8c5e56e2"}, "file_ex": {:git, "https://akkoma.dev/AkkomaGang/file_ex.git", "cc7067c7d446c2526e9ecf91d40896b088851569", [ref: "cc7067c7d446c2526e9ecf91d40896b088851569"]}, - "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, + "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, "flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"}, - "floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"}, + "floki": {:hex, :floki, "0.36.3", "1102f93b16a55bc5383b85ae3ec470f82dee056eaeff9195e8afdf0ef2a43c30", [:mix], [], "hexpm", "fe0158bff509e407735f6d40b3ee0d7deb47f3f3ee7c6c182ad28599f9f6b27a"}, "gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"}, "gettext": {:hex, :gettext, "0.22.3", "c8273e78db4a0bb6fba7e9f0fd881112f349a3117f7f7c598fa18c66c888e524", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "935f23447713954a6866f1bb28c3a878c4c011e802bcd68a726f5e558e4b64bd"}, "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, @@ -61,33 +61,34 @@ "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "inet_cidr": {:hex, :inet_cidr, "1.0.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"}, - "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, - "joken": {:hex, :joken, "2.6.1", "2ca3d8d7f83bf7196296a3d9b2ecda421a404634bfc618159981a960020480a1", [:mix], [{:jose, "~> 1.11.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "ab26122c400b3d254ce7d86ed066d6afad27e70416df947cdcb01e13a7382e68"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"}, "jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"}, "jumper": {:hex, :jumper, "1.0.2", "68cdcd84472a00ac596b4e6459a41b3062d4427cbd4f1e8c8793c5b54f1406a7", [:mix], [], "hexpm", "9b7782409021e01ab3c08270e26f36eb62976a38c1aa64b2eaf6348422f165e1"}, "linkify": {:hex, :linkify, "0.5.3", "5f8143d8f61f5ff08d3aeeff47ef6509492b4948d8f08007fbf66e4d2246a7f2", [:mix], [], "hexpm", "3ef35a1377d47c25506e07c1c005ea9d38d700699d92ee92825f024434258177"}, - "mail": {:hex, :mail, "0.3.1", "cb0a14e4ed8904e4e5a08214e686ccf6f9099346885db17d8c309381f865cc5c", [:mix], [], "hexpm", "1db701e89865c1d5fa296b2b57b1cd587587cca8d8a1a22892b35ef5a8e352a6"}, + "mail": {:hex, :mail, "0.4.2", "959229676ef11dfdfb1269ce4a611622cb70415a1d6f925d79e547848bafc14d", [:mix], [], "hexpm", "08e5b70c72b8d1605cb88ef2df2c7e41d002210a621503ea1c13f1a7916b6bd3"}, "majic": {:git, "https://akkoma.dev/AkkomaGang/majic.git", "80540b36939ec83f48e76c61e5000e0fd67706f0", [ref: "80540b36939ec83f48e76c61e5000e0fd67706f0"]}, "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, - "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mfm_parser": {:git, "https://akkoma.dev/AkkomaGang/mfm-parser.git", "b21ab7754024af096f2d14247574f55f0063295b", [ref: "b21ab7754024af096f2d14247574f55f0063295b"]}, - "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, + "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, "mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"}, "mock": {:hex, :mock, "0.3.8", "7046a306b71db2488ef54395eeb74df0a7f335a7caca4a3d3875d1fc81c884dd", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "7fa82364c97617d79bb7d15571193fc0c4fe5afd0c932cef09426b3ee6fe2022"}, "mogrify": {:hex, :mogrify, "0.9.3", "238c782f00271dace01369ad35ae2e9dd020feee3443b9299ea5ea6bed559841", [:mix], [], "hexpm", "0189b1e1de27455f2b9ae8cf88239cefd23d38de9276eb5add7159aea51731e6"}, - "mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"}, + "mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, + "nimble_ownership": {:hex, :nimble_ownership, "1.0.0", "3f87744d42c21b2042a0aa1d48c83c77e6dd9dd357e425a038dd4b49ba8b79a1", [:mix], [], "hexpm", "7c16cc74f4e952464220a73055b557a273e8b1b7ace8489ec9d86e9ad56cb2cc"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, - "oban": {:hex, :oban, "2.17.10", "c3e5bd739b5c3fdc38eba1d43ab270a8c6ca4463bb779b7705c69400b0d87678", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4afd027b8e2bc3c399b54318b4f46ee8c40251fb55a285cb4e38b5363f0ee7c4"}, - "open_api_spex": {:hex, :open_api_spex, "3.19.1", "65ccb5d06e3d664d1eec7c5ea2af2289bd2f37897094a74d7219fb03fc2b5994", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "392895827ce2984a3459c91a484e70708132d8c2c6c5363972b4b91d6bbac3dd"}, + "oban": {:hex, :oban, "2.17.12", "33fb0cbfb92b910d48dd91a908590fe3698bb85eacec8cd0d9bc6aa13dddd6d6", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7a647d6cd6bb300073db17faabce22d80ae135da3baf3180a064fa7c4fa046e3"}, + "open_api_spex": {:hex, :open_api_spex, "3.21.2", "6a704f3777761feeb5657340250d6d7332c545755116ca98f33d4b875777e1e5", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "f42ae6ed668b895ebba3e02773cfb4b41050df26f803f2ef634c72a7687dc387"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, - "phoenix": {:hex, :phoenix, "1.7.12", "1cc589e0eab99f593a8aa38ec45f15d25297dd6187ee801c8de8947090b5a9d3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "d646192fbade9f485b01bc9920c139bfdd19d0f8df3d73fd8eaf2dfbe0d2837c"}, - "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.1", "96798325fab2fed5a824ca204e877b81f9afd2e480f581e81f7b4b64a5a477f2", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.17", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "0ae544ff99f3c482b0807c5cec2c8289e810ecacabc04959d82c3337f4703391"}, + "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.3", "f686701b0499a07f2e3b122d84d52ff8a31f5def386e03706c916f6feddf69ef", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "909502956916a657a197f94cc1206d9a65247538de8a5e186f7537c895d95764"}, "phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"}, "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"}, "phoenix_live_view": {:hex, :phoenix_live_view, "0.18.18", "1f38fbd7c363723f19aad1a04b5490ff3a178e37daaf6999594d5f34796c47fc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a5810d0472f3189ede6d2a95bda7f31c6113156b91784a3426cb0ab6a6d85214"}, @@ -95,8 +96,8 @@ "phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.1", "b74ccaa8046fbc388a62134360ee7d9742d5a8ae74063f34eb050279de7a99e1", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "4000eeba3f9d7d1a6bf56d2bd56733d5cadf41a7f0d8ffe5bb67e7d667e204a2"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, - "plug": {:hex, :plug, "1.16.0", "1d07d50cb9bb05097fdf187b31cf087c7297aafc3fed8299aac79c128a707e47", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cbf53aa1f5c4d758a7559c0bd6d59e286c2be0c6a1fac8cc3eee2f638243b93e"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.7.1", "87677ffe3b765bc96a89be7960f81703223fe2e21efa42c125fcd0127dd9d6b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "02dbd5f9ab571b864ae39418db7811618506256f6d13b4a45037e5fe78dc5de3"}, + "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.7.2", "fdadb973799ae691bf9ecad99125b16625b1c6039999da5fe544d99218e662e4", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "245d8a11ee2306094840c000e8816f0cbed69a23fc0ac2bcf8d7835ae019bb2f"}, "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, "plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "79fd4fcf34d110605c26560cbae8f23c603ec4158c08298bd4360fdea90bb5cf"}, "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"}, @@ -104,7 +105,7 @@ "postgrex": {:hex, :postgrex, "0.17.5", "0483d054938a8dc069b21bdd636bf56c487404c241ce6c319c1f43588246b281", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "50b8b11afbb2c4095a3ba675b4f055c416d0f3d7de6633a595fc131a828a67eb"}, "pot": {:hex, :pot, "1.0.2", "13abb849139fdc04ab8154986abbcb63bdee5de6ed2ba7e1713527e33df923dd", [:rebar3], [], "hexpm", "78fe127f5a4f5f919d6ea5a2a671827bd53eb9d37e5b4128c0ad3df99856c2e0"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, - "recon": {:hex, :recon, "2.5.5", "c108a4c406fa301a529151a3bb53158cadc4064ec0c5f99b03ddb8c0e4281bdf", [:mix, :rebar3], [], "hexpm", "632a6f447df7ccc1a4a10bdcfce71514412b16660fe59deca0fcf0aa3c054404"}, + "recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"}, "remote_ip": {:hex, :remote_ip, "1.1.0", "cb308841595d15df3f9073b7c39243a1dd6ca56e5020295cb012c76fbec50f2d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "616ffdf66aaad6a72fc546dabf42eed87e2a99e97b09cbd92b10cc180d02ed74"}, "search_parser": {:git, "https://github.com/FloatingGhost/pleroma-contrib-search-parser.git", "08971a81e68686f9ac465cfb6661d51c5e4e1e7f", [ref: "08971a81e68686f9ac465cfb6661d51c5e4e1e7f"]}, "sleeplocks": {:hex, :sleeplocks, "1.1.3", "96a86460cc33b435c7310dbd27ec82ca2c1f24ae38e34f8edde97f756503441a", [:rebar3], [], "hexpm", "d3b3958552e6eb16f463921e70ae7c767519ef8f5be46d7696cc1ed649421321"}, @@ -114,22 +115,22 @@ "swoosh": {:hex, :swoosh, "1.14.4", "94e9dba91f7695a10f49b0172c4a4cb658ef24abef7e8140394521b7f3bbb2d4", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.4 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "081c5a590e4ba85cc89baddf7b2beecf6c13f7f84a958f1cd969290815f0f026"}, "syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"}, "table_rex": {:hex, :table_rex, "4.0.0", "3c613a68ebdc6d4d1e731bc973c233500974ec3993c99fcdabb210407b90959b", [:mix], [], "hexpm", "c35c4d5612ca49ebb0344ea10387da4d2afe278387d4019e4d8111e815df8f55"}, - "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"}, "telemetry_metrics_prometheus": {:hex, :telemetry_metrics_prometheus, "1.1.0", "1cc23e932c1ef9aa3b91db257ead31ea58d53229d407e059b29bb962c1505a13", [:mix], [{:plug_cowboy, "~> 2.1", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}], "hexpm", "d43b3659b3244da44fe0275b717701542365d4519b79d9ce895b9719c1ce4d26"}, "telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.1.0", "4e15f6d7dbedb3a4e3aed2262b7e1407f166fcb9c30ca3f96635dfbbef99965c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0dd10e7fe8070095df063798f82709b0a1224c31b8baf6278b423898d591a069"}, "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"}, "temple": {:git, "https://akkoma.dev/AkkomaGang/temple.git", "066a699ade472d8fa42a9d730b29a61af9bc8b59", [ref: "066a699ade472d8fa42a9d730b29a61af9bc8b59"]}, - "tesla": {:hex, :tesla, "1.9.0", "8c22db6a826e56a087eeb8cdef56889731287f53feeb3f361dec5d4c8efb6f14", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "7c240c67e855f7e63e795bf16d6b3f5115a81d1f44b7fe4eadbf656bae0fef8a"}, + "tesla": {:hex, :tesla, "1.13.0", "24a068a48d107080dd7c943a593997eee265977a38020eb2ab657cca78a12502", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "7b8fc8f6b0640fa0d090af7889d12eb396460e044b6f8688a8e55e30406a2200"}, "timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"}, "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"}, - "tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"}, + "tzdata": {:hex, :tzdata, "1.1.2", "45e5f1fcf8729525ec27c65e163be5b3d247ab1702581a94674e008413eef50b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cec7b286e608371602318c414f344941d5eb0375e14cfdab605cca2fe66cba8b"}, "ueberauth": {:hex, :ueberauth, "0.10.5", "806adb703df87e55b5615cf365e809f84c20c68aa8c08ff8a416a5a6644c4b02", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3efd1f31d490a125c7ed453b926f7c31d78b97b8a854c755f5c40064bf3ac9e1"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"}, "vex": {:hex, :vex, "0.9.2", "fe061acc9e0907d983d46b51bf35d58176f0fe6eb7ba3b33c9336401bf42b6d1", [:mix], [], "hexpm", "76e709a9762e98c6b462dfce92e9b5dfbf712839227f2da8add6dd11549b12cb"}, "web_push_encryption": {:hex, :web_push_encryption, "0.3.1", "76d0e7375142dfee67391e7690e89f92578889cbcf2879377900b5620ee4708d", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.11.1", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "4f82b2e57622fb9337559058e8797cb0df7e7c9790793bdc4e40bc895f70e2a2"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, - "websock_adapter": {:hex, :websock_adapter, "0.5.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.7", "65fa74042530064ef0570b75b43f5c49bb8b235d6515671b3d250022cb8a1f9e", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1"}, "websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"}, } diff --git a/priv/repo/migrations/20240625213637_create_signing_key_table.exs b/priv/repo/migrations/20240625213637_create_signing_key_table.exs new file mode 100644 index 000000000..062edc66f --- /dev/null +++ b/priv/repo/migrations/20240625213637_create_signing_key_table.exs @@ -0,0 +1,13 @@ +defmodule Pleroma.Repo.Migrations.CreateSigningKeyTable do + use Ecto.Migration + + def change do + create table(:signing_keys, primary_key: false) do + add :user_id, references(:users, type: :uuid, on_delete: :delete_all) + add :key_id, :text, primary_key: true + add :public_key, :text + add :private_key, :text + timestamps() + end + end +end diff --git a/priv/repo/migrations/20240625220752_move_signing_keys.exs b/priv/repo/migrations/20240625220752_move_signing_keys.exs new file mode 100644 index 000000000..9104b7c29 --- /dev/null +++ b/priv/repo/migrations/20240625220752_move_signing_keys.exs @@ -0,0 +1,37 @@ +defmodule Pleroma.Repo.Migrations.MoveSigningKeys do + use Ecto.Migration + alias Pleroma.User + alias Pleroma.Repo + import Ecto.Query + + def up do + # we do not handle remote users here! + # because we want to store a key id -> user id mapping, and we don't + # currently store key ids for remote users... + query = + from(u in User) + |> where(local: true) + + Repo.stream(query, timeout: :infinity) + |> Enum.each(fn + %User{id: user_id, keys: private_key, local: true, ap_id: ap_id} -> + IO.puts("Migrating user #{user_id}") + # we can precompute the public key here... + # we do use it on every user view which makes it a bit of a dos attack vector + # so we should probably cache it + {:ok, public_key} = User.SigningKey.private_pem_to_public_pem(private_key) + + key = %User.SigningKey{ + user_id: user_id, + public_key: public_key, + key_id: User.SigningKey.local_key_id(ap_id), + private_key: private_key + } + + {:ok, _} = Repo.insert(key) + end) + end + + # no need to rollback + def down, do: :ok +end diff --git a/test/mix/tasks/pleroma/database_test.exs b/test/mix/tasks/pleroma/database_test.exs index 20f113e6e..4f97a978a 100644 --- a/test/mix/tasks/pleroma/database_test.exs +++ b/test/mix/tasks/pleroma/database_test.exs @@ -353,7 +353,7 @@ test "with the --keep-threads option it keeps old threads with bookmarked posts" test "We don't have unexpected tables which may contain objects that are referenced by activities" do # We can delete orphaned activities. For that we look for the objects they reference in the 'objects', 'activities', and 'users' table. - # If someone adds another table with objects (idk, maybe with separate relations, or collections or w/e), then we need to make sure we + # If someone adds another table with objects (idk, maybe with separate relations, or collections or w/e), then we need to make sure we # add logic for that in the 'prune_objects' task so that we don't wrongly delete their corresponding activities. # So when someone adds (or removes) a table, this test will fail. # Either the table contains objects which can be referenced from the activities table @@ -401,6 +401,7 @@ test "We don't have unexpected tables which may contain objects that are referen ["rich_media_card"], ["scheduled_activities"], ["schema_migrations"], + ["signing_keys"], ["thread_mutes"], ["user_follows_hashtag"], ["user_frontend_setting_profiles"], diff --git a/test/pleroma/emoji/pack_test.exs b/test/pleroma/emoji/pack_test.exs index 3843add13..049f27a92 100644 --- a/test/pleroma/emoji/pack_test.exs +++ b/test/pleroma/emoji/pack_test.exs @@ -64,7 +64,10 @@ test "returns error when zip file is bad", %{pack: pack} do path: Path.absname("test/instance_static/emoji/test_pack/blank.png") } - assert Pack.add_file(pack, nil, nil, file) == {:error, :bad_eocd} + # this varies by erlang OTP + possible_error_codes = [:bad_eocd, :einval] + {:error, code} = Pack.add_file(pack, nil, nil, file) + assert Enum.member?(possible_error_codes, code) end test "returns pack when zip file is empty", %{pack: pack} do diff --git a/test/pleroma/keys_test.exs b/test/pleroma/keys_test.exs deleted file mode 100644 index 9a15bf06e..000000000 --- a/test/pleroma/keys_test.exs +++ /dev/null @@ -1,24 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2021 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.KeysTest do - use Pleroma.DataCase, async: true - - alias Pleroma.Keys - - test "generates an RSA private key pem" do - {:ok, key} = Keys.generate_rsa_pem() - - assert is_binary(key) - assert Regex.match?(~r/RSA/, key) - end - - test "returns a public and private key from a pem" do - pem = File.read!("test/fixtures/private_key.pem") - {:ok, private, public} = Keys.keys_from_pem(pem) - - assert elem(private, 0) == :RSAPrivateKey - assert elem(public, 0) == :RSAPublicKey - end -end diff --git a/test/pleroma/signature_test.exs b/test/pleroma/signature_test.exs index 768c78f21..0e360d58d 100644 --- a/test/pleroma/signature_test.exs +++ b/test/pleroma/signature_test.exs @@ -35,25 +35,23 @@ defp make_fake_conn(key_id), do: %Plug.Conn{req_headers: %{"signature" => make_fake_signature(key_id <> "#main-key")}} describe "fetch_public_key/1" do - test "it returns key" do + test "it returns the key" do expected_result = {:ok, @rsa_public_key} - user = insert(:user, public_key: @public_key) + user = + insert(:user) + |> with_signing_key(public_key: @public_key) assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == expected_result end - test "it returns error when not found user" do - assert capture_log(fn -> - assert Signature.fetch_public_key(make_fake_conn("https://test-ap-id")) == - {:error, :error} - end) =~ "[error] Could not decode user" - end - test "it returns error if public key is nil" do - user = insert(:user, public_key: nil) + # this actually needs the URL to be valid + user = insert(:user) + key_id = user.ap_id <> "#main-key" + Tesla.Mock.mock(fn %{url: ^key_id} -> {:ok, %{status: 404}} end) - assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == {:error, :error} + assert {:error, _} = Signature.fetch_public_key(make_fake_conn(user.ap_id)) end end @@ -63,12 +61,6 @@ test "it returns key" do assert Signature.refetch_public_key(make_fake_conn(ap_id)) == {:ok, @rsa_public_key} end - - test "it returns error when not found user" do - assert capture_log(fn -> - {:error, _} = Signature.refetch_public_key(make_fake_conn("https://test-ap_id")) - end) =~ "[error] Could not decode user" - end end defp split_signature(sig) do @@ -104,9 +96,9 @@ defp assert_part_equal(part_a, part_b) do test "it returns signature headers" do user = insert(:user, %{ - ap_id: "https://mastodon.social/users/lambadalambda", - keys: @private_key + ap_id: "https://mastodon.social/users/lambadalambda" }) + |> with_signing_key(private_key: @private_key) headers = %{ host: "test.test", @@ -121,50 +113,15 @@ test "it returns signature headers" do "keyId=\"https://mastodon.social/users/lambadalambda#main-key\",algorithm=\"rsa-sha256\",headers=\"content-length host\",signature=\"sibUOoqsFfTDerquAkyprxzDjmJm6erYc42W5w1IyyxusWngSinq5ILTjaBxFvfarvc7ci1xAi+5gkBwtshRMWm7S+Uqix24Yg5EYafXRun9P25XVnYBEIH4XQ+wlnnzNIXQkU3PU9e6D8aajDZVp3hPJNeYt1gIPOA81bROI8/glzb1SAwQVGRbqUHHHKcwR8keiR/W2h7BwG3pVRy4JgnIZRSW7fQogKedDg02gzRXwUDFDk0pr2p3q6bUWHUXNV8cZIzlMK+v9NlyFbVYBTHctAR26GIAN6Hz0eV0mAQAePHDY1mXppbA8Gpp6hqaMuYfwifcXmcc+QFm4e+n3A==\"" ) end - - test "it returns error" do - user = insert(:user, %{ap_id: "https://mastodon.social/users/lambadalambda", keys: ""}) - - assert Signature.sign( - user, - %{host: "test.test", "content-length": "100"} - ) == {:error, []} - end end describe "key_id_to_actor_id/1" do - test "it properly deduces the actor id for misskey" do - assert Signature.key_id_to_actor_id("https://example.com/users/1234/publickey") == - {:ok, "https://example.com/users/1234"} - end + test "it reverses the key id to actor id" do + user = + insert(:user) + |> with_signing_key() - test "it properly deduces the actor id for mastodon and pleroma" do - assert Signature.key_id_to_actor_id("https://example.com/users/1234#main-key") == - {:ok, "https://example.com/users/1234"} - end - - test "it deduces the actor id for gotoSocial" do - assert Signature.key_id_to_actor_id("https://example.com/users/1234/main-key") == - {:ok, "https://example.com/users/1234"} - end - - test "it deduces the actor ID for streams" do - assert Signature.key_id_to_actor_id("https://example.com/users/1234?operation=getkey") == - {:ok, "https://example.com/users/1234"} - end - - test "it deduces the actor ID for bridgy" do - assert Signature.key_id_to_actor_id("https://example.com/1234#key") == - {:ok, "https://example.com/1234"} - end - - test "it calls webfinger for 'acct:' accounts" do - with_mock(Pleroma.Web.WebFinger, - finger: fn _ -> {:ok, %{"ap_id" => "https://gensokyo.2hu/users/raymoo"}} end - ) do - assert Signature.key_id_to_actor_id("acct:raymoo@gensokyo.2hu") == - {:ok, "https://gensokyo.2hu/users/raymoo"} - end + assert Signature.key_id_to_actor_id(user.signing_key.key_id) == {:ok, user.ap_id} end end diff --git a/test/pleroma/user_search_test.exs b/test/pleroma/user_search_test.exs index 2af19b6de..b499629c8 100644 --- a/test/pleroma/user_search_test.exs +++ b/test/pleroma/user_search_test.exs @@ -259,7 +259,7 @@ test "works with URIs" do |> Map.put(:multi_factor_authentication_settings, nil) |> Map.put(:notification_settings, nil) - assert user == expected + assert_user_match(user, expected) end test "excludes a blocked users from search result" do diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index e3f0bb415..207e38993 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -639,11 +639,12 @@ test "it sets the password_hash, ap_id, private key and followers collection add changeset = User.register_changeset(%User{}, @full_user_data) assert changeset.valid? - assert is_binary(changeset.changes[:password_hash]) - assert is_binary(changeset.changes[:keys]) assert changeset.changes[:ap_id] == User.ap_id(%User{nickname: @full_user_data.nickname}) - assert is_binary(changeset.changes[:keys]) + assert changeset.changes[:signing_key] + assert changeset.changes[:signing_key].valid? + assert is_binary(changeset.changes[:signing_key].changes.private_key) + assert is_binary(changeset.changes[:signing_key].changes.public_key) assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers" end @@ -1665,7 +1666,6 @@ test "delete/1 purges a user when they wouldn't be fully deleted" do name: "qqqqqqq", password_hash: "pdfk2$1b3n159001", keys: "RSA begin buplic key", - public_key: "--PRIVATE KEYE--", avatar: %{"a" => "b"}, tags: ["qqqqq"], banner: %{"a" => "b"}, @@ -1704,8 +1704,6 @@ test "delete/1 purges a user when they wouldn't be fully deleted" do email: nil, name: nil, password_hash: nil, - keys: "RSA begin buplic key", - public_key: "--PRIVATE KEYE--", avatar: %{}, tags: [], last_refreshed_at: nil, diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index b325bcb9a..c41a19cc7 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -584,6 +584,7 @@ test "it inserts an incoming activity into the database" <> local: false, last_refreshed_at: nil ) + |> with_signing_key() data = File.read!("test/fixtures/mastodon-post-activity.json") @@ -594,7 +595,7 @@ test "it inserts an incoming activity into the database" <> conn = conn |> assign(:valid_signature, true) - |> put_req_header("signature", "keyId=\"#{user.ap_id}/main-key\"") + |> put_req_header("signature", "keyId=\"#{user.signing_key.key_id}\"") |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) @@ -608,7 +609,10 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn} do data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!() sender_url = data["actor"] - sender = insert(:user, ap_id: data["actor"]) + + sender = + insert(:user, ap_id: data["actor"]) + |> with_signing_key() Instances.set_consistently_unreachable(sender_url) refute Instances.reachable?(sender_url) @@ -616,7 +620,7 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn} do conn = conn |> assign(:valid_signature, true) - |> put_req_header("signature", "keyId=\"#{sender.ap_id}/main-key\"") + |> put_req_header("signature", "keyId=\"#{sender.signing_key.key_id}\"") |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) @@ -641,7 +645,7 @@ test "accept follow activity", %{conn: conn} do assert "ok" == conn |> assign(:valid_signature, true) - |> put_req_header("signature", "keyId=\"#{followed_relay.ap_id}/main-key\"") + |> put_req_header("signature", "keyId=\"#{followed_relay.ap_id}#main-key\"") |> put_req_header("content-type", "application/activity+json") |> post("/inbox", accept) |> json_response(200) @@ -678,6 +682,7 @@ test "accepts Add/Remove activities", %{conn: conn} do |> String.replace("{{nickname}}", "lain") actor = "https://example.com/users/lain" + key_id = "#{actor}/main-key" insert(:user, ap_id: actor, @@ -705,6 +710,16 @@ test "accepts Add/Remove activities", %{conn: conn} do headers: [{"content-type", "application/activity+json"}] } + %{ + method: :get, + url: ^key_id + } -> + %Tesla.Env{ + status: 200, + body: user, + headers: [{"content-type", "application/activity+json"}] + } + %{method: :get, url: "https://example.com/users/lain/collections/featured"} -> %Tesla.Env{ status: 200, @@ -778,12 +793,14 @@ test "mastodon pin/unpin", %{conn: conn} do |> String.replace("{{nickname}}", "lain") actor = "https://example.com/users/lain" + key_id = "#{actor}/main-key" sender = insert(:user, ap_id: actor, featured_address: "https://example.com/users/lain/collections/featured" ) + |> with_signing_key() Tesla.Mock.mock(fn %{ @@ -806,6 +823,16 @@ test "mastodon pin/unpin", %{conn: conn} do headers: [{"content-type", "application/activity+json"}] } + %{ + method: :get, + url: ^key_id + } -> + %Tesla.Env{ + status: 200, + body: user, + headers: [{"content-type", "application/activity+json"}] + } + %{method: :get, url: "https://example.com/users/lain/collections/featured"} -> %Tesla.Env{ status: 200, @@ -839,7 +866,7 @@ test "mastodon pin/unpin", %{conn: conn} do assert "ok" == conn |> assign(:valid_signature, true) - |> put_req_header("signature", "keyId=\"#{sender.ap_id}/main-key\"") + |> put_req_header("signature", "keyId=\"#{sender.signing_key.key_id}\"") |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) |> json_response(200) @@ -901,7 +928,9 @@ test "it inserts an incoming activity into the database", %{conn: conn, data: da end test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do - user = insert(:user) + user = + insert(:user) + |> with_signing_key() data = data @@ -946,7 +975,9 @@ test "it accepts messages with cc as string instead of array", %{conn: conn, dat end test "it accepts messages with bcc as string instead of array", %{conn: conn, data: data} do - user = insert(:user) + user = + insert(:user) + |> with_signing_key() data = data @@ -973,7 +1004,10 @@ test "it accepts announces with to as string instead of array", %{conn: conn} do user = insert(:user) {:ok, post} = CommonAPI.post(user, %{status: "hey"}) - announcer = insert(:user, local: false) + + announcer = + insert(:user, local: false) + |> with_signing_key() data = %{ "@context" => "https://www.w3.org/ns/activitystreams", @@ -988,7 +1022,7 @@ test "it accepts announces with to as string instead of array", %{conn: conn} do conn = conn |> assign(:valid_signature, true) - |> put_req_header("signature", "keyId=\"#{announcer.ap_id}/main-key\"") + |> put_req_header("signature", "keyId=\"#{announcer.signing_key.key_id}\"") |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) @@ -1003,7 +1037,10 @@ test "it accepts messages from actors that are followed by the user", %{ data: data } do recipient = insert(:user) - actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"}) + + actor = + insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"}) + |> with_signing_key() {:ok, recipient, actor} = User.follow(recipient, actor) @@ -1019,7 +1056,7 @@ test "it accepts messages from actors that are followed by the user", %{ conn = conn |> assign(:valid_signature, true) - |> put_req_header("signature", "keyId=\"#{actor.ap_id}/main-key\"") + |> put_req_header("signature", "keyId=\"#{actor.signing_key.key_id}\"") |> put_req_header("content-type", "application/activity+json") |> post("/users/#{recipient.nickname}/inbox", data) @@ -1056,7 +1093,10 @@ test "it returns a note activity in a collection", %{conn: conn} do end test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do - user = insert(:user) + user = + insert(:user, ap_id: data["actor"]) + |> with_signing_key() + data = Map.put(data, "bcc", [user.ap_id]) sender_host = URI.parse(data["actor"]).host @@ -1066,7 +1106,7 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn, da conn = conn |> assign(:valid_signature, true) - |> put_req_header("signature", "keyId=\"#{data["actor"]}/main-key\"") + |> put_req_header("signature", "keyId=\"#{user.signing_key.key_id}\"") |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) @@ -1077,6 +1117,7 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn, da @tag capture_log: true test "it removes all follower collections but actor's", %{conn: conn} do [actor, recipient] = insert_pair(:user) + actor = with_signing_key(actor) to = [ recipient.ap_id, @@ -1105,7 +1146,7 @@ test "it removes all follower collections but actor's", %{conn: conn} do conn |> assign(:valid_signature, true) - |> put_req_header("signature", "keyId=\"#{actor.ap_id}/main-key\"") + |> put_req_header("signature", "keyId=\"#{actor.signing_key.key_id}\"") |> put_req_header("content-type", "application/activity+json") |> post("/users/#{recipient.nickname}/inbox", data) |> json_response(200) @@ -1141,7 +1182,11 @@ test "it requires authentication", %{conn: conn} do @tag capture_log: true test "forwarded report", %{conn: conn} do admin = insert(:user, is_admin: true) - actor = insert(:user, local: false) + + actor = + insert(:user, local: false) + |> with_signing_key() + remote_domain = URI.parse(actor.ap_id).host reported_user = insert(:user) @@ -1198,7 +1243,7 @@ test "forwarded report", %{conn: conn} do conn |> assign(:valid_signature, true) - |> put_req_header("signature", "keyId=\"#{actor.ap_id}/main-key\"") + |> put_req_header("signature", "keyId=\"#{actor.signing_key.key_id}\"") |> put_req_header("content-type", "application/activity+json") |> post("/users/#{reported_user.nickname}/inbox", data) |> json_response(200) @@ -1232,12 +1277,22 @@ test "forwarded report from mastodon", %{conn: conn} do |> File.read!() |> String.replace("{{DOMAIN}}", remote_domain) - Tesla.Mock.mock(fn %{url: ^remote_actor} -> - %Tesla.Env{ - status: 200, - body: mock_json_body, - headers: [{"content-type", "application/activity+json"}] - } + key_url = "#{remote_actor}#main-key" + + Tesla.Mock.mock(fn + %{url: ^remote_actor} -> + %Tesla.Env{ + status: 200, + body: mock_json_body, + headers: [{"content-type", "application/activity+json"}] + } + + %{url: ^key_url} -> + %Tesla.Env{ + status: 200, + body: mock_json_body, + headers: [{"content-type", "application/activity+json"}] + } end) data = %{ @@ -1254,7 +1309,7 @@ test "forwarded report from mastodon", %{conn: conn} do conn |> assign(:valid_signature, true) - |> put_req_header("signature", "keyId=\"#{remote_actor}/main-key\"") + |> put_req_header("signature", "keyId=\"#{remote_actor}#main-key\"") |> put_req_header("content-type", "application/activity+json") |> post("/users/#{reported_user.nickname}/inbox", data) |> json_response(200) diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs index 87930b7b1..eeec59cfb 100644 --- a/test/pleroma/web/activity_pub/publisher_test.exs +++ b/test/pleroma/web/activity_pub/publisher_test.exs @@ -140,7 +140,9 @@ test "publish to url with with different ports" do {:ok, %Tesla.Env{status: 200, body: "port 80"}} end) - actor = insert(:user) + actor = + insert(:user) + |> with_signing_key() assert {:ok, %{body: "port 42"}} = Publisher.publish_one(%{ @@ -165,7 +167,10 @@ test "publish to url with with different ports" do Instances, [:passthrough], [] do - actor = insert(:user) + actor = + insert(:user) + |> with_signing_key() + inbox = "http://200.site/users/nick1/inbox" assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) @@ -176,7 +181,10 @@ test "publish to url with with different ports" do Instances, [:passthrough], [] do - actor = insert(:user) + actor = + insert(:user) + |> with_signing_key() + inbox = "http://200.site/users/nick1/inbox" assert {:ok, _} = @@ -195,7 +203,10 @@ test "publish to url with with different ports" do Instances, [:passthrough], [] do - actor = insert(:user) + actor = + insert(:user) + |> with_signing_key() + inbox = "http://200.site/users/nick1/inbox" assert {:ok, _} = @@ -214,7 +225,10 @@ test "publish to url with with different ports" do Instances, [:passthrough], [] do - actor = insert(:user) + actor = + insert(:user) + |> with_signing_key() + inbox = "http://404.site/users/nick1/inbox" assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) @@ -226,7 +240,10 @@ test "publish to url with with different ports" do Instances, [:passthrough], [] do - actor = insert(:user) + actor = + insert(:user) + |> with_signing_key() + inbox = "http://connrefused.site/users/nick1/inbox" assert capture_log(fn -> @@ -241,7 +258,10 @@ test "publish to url with with different ports" do Instances, [:passthrough], [] do - actor = insert(:user) + actor = + insert(:user) + |> with_signing_key() + inbox = "http://200.site/users/nick1/inbox" assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) @@ -253,7 +273,10 @@ test "publish to url with with different ports" do Instances, [:passthrough], [] do - actor = insert(:user) + actor = + insert(:user) + |> with_signing_key() + inbox = "http://connrefused.site/users/nick1/inbox" assert capture_log(fn -> @@ -294,7 +317,9 @@ test "publish to url with with different ports" do ap_enabled: true }) - actor = insert(:user, follower_address: follower.ap_id) + actor = + insert(:user, follower_address: follower.ap_id) + |> with_signing_key() {:ok, follower, actor} = Pleroma.User.follow(follower, actor) {:ok, _another_follower, actor} = Pleroma.User.follow(another_follower, actor) @@ -365,7 +390,9 @@ test "publish to url with with different ports" do ap_enabled: true }) - actor = insert(:user, follower_address: follower.ap_id) + actor = + insert(:user, follower_address: follower.ap_id) + |> with_signing_key() {:ok, follower, actor} = Pleroma.User.follow(follower, actor) actor = refresh_record(actor) diff --git a/test/pleroma/web/activity_pub/views/user_view_test.exs b/test/pleroma/web/activity_pub/views/user_view_test.exs index 2a367b680..7e251e510 100644 --- a/test/pleroma/web/activity_pub/views/user_view_test.exs +++ b/test/pleroma/web/activity_pub/views/user_view_test.exs @@ -11,7 +11,9 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do alias Pleroma.Web.CommonAPI test "Renders a user, including the public key" do - user = insert(:user) + user = + insert(:user) + |> with_signing_key() result = UserView.render("user.json", %{user: user}) @@ -37,7 +39,9 @@ test "Renders profile fields" do end test "Renders with emoji tags" do - user = insert(:user, emoji: %{"bib" => "/test"}) + user = + insert(:user, emoji: %{"bib" => "/test"}) + |> with_signing_key() assert %{ "tag" => [ @@ -74,13 +78,18 @@ test "Does not add an avatar image if the user hasn't set one" do end test "renders an invisible user with the invisible property set to true" do - user = insert(:user, invisible: true) + user = + insert(:user, invisible: true) + |> with_signing_key() assert %{"invisible" => true} = UserView.render("service.json", %{user: user}) end test "service has a few essential fields" do - user = insert(:user) + user = + insert(:user) + |> with_signing_key() + result = UserView.render("service.json", %{user: user}) assert result["id"] assert result["type"] == "Application" @@ -120,7 +129,9 @@ test "remote users have an empty endpoints structure" do end test "instance users do not expose oAuth endpoints" do - user = insert(:user, nickname: nil, local: true) + user = + insert(:user, nickname: nil, local: true) + |> with_signing_key() result = UserView.render("user.json", %{user: user}) diff --git a/test/pleroma/web/plugs/http_signature_plug_test.exs b/test/pleroma/web/plugs/http_signature_plug_test.exs index 0a602424d..5236b519e 100644 --- a/test/pleroma/web/plugs/http_signature_plug_test.exs +++ b/test/pleroma/web/plugs/http_signature_plug_test.exs @@ -14,6 +14,15 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do import Phoenix.Controller, only: [put_format: 2] import Mock + setup do + user = + :user + |> insert(%{ap_id: "http://mastodon.example.org/users/admin"}) + |> with_signing_key(%{key_id: "http://mastodon.example.org/users/admin#main-key"}) + + {:ok, %{user: user}} + end + setup_with_mocks([ {HTTPSignatures, [], [ @@ -46,15 +55,15 @@ defp submit_to_plug(host, method, path) do |> HTTPSignaturePlug.call(%{}) end - test "it call HTTPSignatures to check validity if the actor signed it" do - params = %{"actor" => "http://mastodon.example.org/users/admin"} + test "it call HTTPSignatures to check validity if the actor signed it", %{user: user} do + params = %{"actor" => user.ap_id} conn = build_conn(:get, "/doesntmattter", params) conn = conn |> put_req_header( "signature", - "keyId=\"http://mastodon.example.org/users/admin#main-key" + "keyId=\"#{user.signing_key.key_id}\"" ) |> put_format("activity+json") |> HTTPSignaturePlug.call(%{}) diff --git a/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs index 086a27885..5789cd756 100644 --- a/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs +++ b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs @@ -8,52 +8,63 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlugTest do import Tesla.Mock import Plug.Conn + import Pleroma.Factory import Pleroma.Tests.Helpers, only: [clear_config: 2] setup do mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok + + user = + insert(:user) + |> with_signing_key() + + {:ok, %{user: user}} end - defp set_signature(conn, key_id) do + defp set_signature(conn, ap_id) do conn - |> put_req_header("signature", "keyId=\"#{key_id}\"") + |> put_req_header("signature", "keyId=\"#{ap_id}#main-key\"") |> assign(:valid_signature, true) end - test "it successfully maps a valid identity with a valid signature" do + test "it successfully maps a valid identity with a valid signature", %{user: user} do conn = build_conn(:get, "/doesntmattter") - |> set_signature("http://mastodon.example.org/users/admin") + |> set_signature(user.ap_id) |> MappedSignatureToIdentityPlug.call(%{}) refute is_nil(conn.assigns.user) end - test "it successfully maps a valid identity with a valid signature with payload" do + test "it successfully maps a valid identity with a valid signature with payload", %{user: user} do conn = - build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"}) - |> set_signature("http://mastodon.example.org/users/admin") + build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id}) + |> set_signature(user.ap_id) |> MappedSignatureToIdentityPlug.call(%{}) refute is_nil(conn.assigns.user) end - test "it considers a mapped identity to be invalid when it mismatches a payload" do + test "it considers a mapped identity to be invalid when it mismatches a payload", %{user: user} do conn = - build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"}) + build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id}) |> set_signature("https://niu.moe/users/rye") |> MappedSignatureToIdentityPlug.call(%{}) assert %{valid_signature: false} == conn.assigns end - test "it considers a mapped identity to be invalid when the associated instance is blocked" do + test "it considers a mapped identity to be invalid when the associated instance is blocked", %{ + user: user + } do clear_config([:activitypub, :authorized_fetch_mode], true) + # extract domain from user.ap_id + url = URI.parse(user.ap_id) + clear_config([:mrf_simple, :reject], [ - {"mastodon.example.org", "anime is banned"} + {url.host, "anime is banned"} ]) on_exit(fn -> @@ -62,18 +73,21 @@ test "it considers a mapped identity to be invalid when the associated instance end) conn = - build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"}) - |> set_signature("http://mastodon.example.org/users/admin") + build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id}) + |> set_signature(user.ap_id) |> MappedSignatureToIdentityPlug.call(%{}) assert %{valid_signature: false} == conn.assigns end - test "allowlist federation: it considers a mapped identity to be valid when the associated instance is allowed" do + test "allowlist federation: it considers a mapped identity to be valid when the associated instance is allowed", + %{user: user} do clear_config([:activitypub, :authorized_fetch_mode], true) + url = URI.parse(user.ap_id) + clear_config([:mrf_simple, :accept], [ - {"mastodon.example.org", "anime is allowed"} + {url.host, "anime is allowed"} ]) on_exit(fn -> @@ -82,15 +96,16 @@ test "allowlist federation: it considers a mapped identity to be valid when the end) conn = - build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"}) - |> set_signature("http://mastodon.example.org/users/admin") + build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id}) + |> set_signature(user.ap_id) |> MappedSignatureToIdentityPlug.call(%{}) assert conn.assigns[:valid_signature] refute is_nil(conn.assigns.user) end - test "allowlist federation: it considers a mapped identity to be invalid when the associated instance is not allowed" do + test "allowlist federation: it considers a mapped identity to be invalid when the associated instance is not allowed", + %{user: user} do clear_config([:activitypub, :authorized_fetch_mode], true) clear_config([:mrf_simple, :accept], [ @@ -103,8 +118,8 @@ test "allowlist federation: it considers a mapped identity to be invalid when th end) conn = - build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"}) - |> set_signature("http://mastodon.example.org/users/admin") + build_conn(:post, "/doesntmattter", %{"actor" => user.ap_id}) + |> set_signature(user.ap_id) |> MappedSignatureToIdentityPlug.call(%{}) assert %{valid_signature: false} == conn.assigns diff --git a/test/support/factory.ex b/test/support/factory.ex index 2a73a4ae6..54e5f91b7 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -47,7 +47,6 @@ def instance_factory(attrs \\ %{}) do end def user_factory(attrs \\ %{}) do - pem = Enum.random(@rsa_keys) # Argon2.hash_pwd_salt("test") # it really eats CPU time, so we use a precomputed hash password_hash = @@ -64,8 +63,7 @@ def user_factory(attrs \\ %{}) do last_refreshed_at: NaiveDateTime.utc_now(), notification_settings: %Pleroma.User.NotificationSetting{}, multi_factor_authentication_settings: %Pleroma.MFA.Settings{}, - ap_enabled: true, - keys: pem + ap_enabled: true } urls = @@ -97,6 +95,28 @@ def user_factory(attrs \\ %{}) do |> merge_attributes(attrs) end + def with_signing_key(%User{} = user, attrs \\ %{}) do + signing_key = + build(:signing_key, %{user: user, key_id: "#{user.ap_id}#main-key"}) + |> merge_attributes(attrs) + + insert(signing_key) + %{user | signing_key: signing_key} + end + + def signing_key_factory(attrs \\ %{}) do + pem = Enum.random(@rsa_keys) + user = attrs[:user] || insert(:user) + {:ok, public_key} = Pleroma.User.SigningKey.private_pem_to_public_pem(pem) + + %Pleroma.User.SigningKey{ + user_id: user.id, + public_key: attrs[:public_key] || public_key, + private_key: attrs[:private_key] || pem, + key_id: attrs[:key_id] + } + end + def user_relationship_factory(attrs \\ %{}) do source = attrs[:source] || insert(:user) target = attrs[:target] || insert(:user) diff --git a/test/support/helpers.ex b/test/support/helpers.ex index 2dfff70a2..f0beae8ec 100644 --- a/test/support/helpers.ex +++ b/test/support/helpers.ex @@ -66,6 +66,8 @@ defmacro __using__(_opts) do clear_config: 2 ] + import Pleroma.Test.MatchingHelpers + def time_travel(entity, seconds) do new_time = NaiveDateTime.add(entity.inserted_at, seconds) diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index ea06c4966..d14434333 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -424,6 +424,15 @@ def get("http://mastodon.example.org/users/admin", _, _, _) do }} end + def get("http://mastodon.example.org/users/admin/main-key", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/admin@mastdon.example.org.json"), + headers: activitypub_object_headers() + }} + end + def get( "http://mastodon.example.org/users/admin/statuses/99512778738411822/replies?min_id=99512778738411824&page=true", _, @@ -958,6 +967,15 @@ def get("https://mastodon.social/users/lambadalambda", _, _, _) do }} end + def get("https://mastodon.social/users/lambadalambda#main-key", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/lambadalambda.json"), + headers: activitypub_object_headers() + }} + end + def get("https://mastodon.social/users/lambadalambda/collections/featured", _, _, _) do {:ok, %Tesla.Env{ @@ -1403,6 +1421,15 @@ def get("https://relay.mastodon.host/actor", _, _, _) do }} end + def get("https://relay.mastodon.host/actor#main-key", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/relay/relay.json"), + headers: activitypub_object_headers() + }} + end + def get("http://localhost:4001/", _, "", [{"accept", "text/html"}]) do {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/7369654.html")}} end diff --git a/test/support/matching_helpers.ex b/test/support/matching_helpers.ex new file mode 100644 index 000000000..c8411b907 --- /dev/null +++ b/test/support/matching_helpers.ex @@ -0,0 +1,10 @@ +defmodule Pleroma.Test.MatchingHelpers do + import ExUnit.Assertions + + @assoc_fields [ + :signing_key + ] + def assert_user_match(actor1, actor2) do + assert Ecto.reset_fields(actor1, @assoc_fields) == Ecto.reset_fields(actor2, @assoc_fields) + end +end