User: generate private keys on user creation
authorHélène <pleroma-dev@helene.moe>
Fri, 26 Aug 2022 16:30:43 +0000 (18:30 +0200)
committerFloatingGhost <hannah@coffee-and-dreams.uk>
Sun, 11 Sep 2022 18:54:37 +0000 (19:54 +0100)
This fixes a race condition bug where keys could be regenerated
post-federation, causing activities and HTTP signatures from an user to
be dropped due to key differences.

lib/pleroma/signature.ex
lib/pleroma/user.ex
lib/pleroma/web/activity_pub/activity_pub_controller.ex
lib/pleroma/web/activity_pub/views/user_view.ex
lib/pleroma/web/federator.ex
lib/pleroma/web/web_finger.ex
test/pleroma/user_test.exs
test/pleroma/web/activity_pub/views/user_view_test.exs
test/support/factory.ex

index a71a504b3b5597bd4b3b1f431210b33b89c8b2b6..043a0643e34e492a2d06f3ffb4413e6bfbb9147c 100644 (file)
@@ -66,9 +66,8 @@ defmodule Pleroma.Signature do
     end
   end
 
-  def sign(%User{} = user, headers) do
-    with {:ok, %{keys: keys}} <- User.ensure_keys_present(user),
-         {:ok, private_key, _} <- Keys.keys_from_pem(keys) do
+  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)
     end
   end
index 4383f8f536fd69036cbae00558f3fc58bd3b5bf7..138f3c851d84bfb1424468f317949c20be5e40dd 100644 (file)
@@ -681,9 +681,9 @@ defmodule Pleroma.User do
     |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
     |> validate_format(:nickname, local_nickname_regex())
     |> put_ap_id()
-    |> put_keys()
     |> unique_constraint(:ap_id)
     |> put_following_and_follower_and_featured_address()
+    |> put_private_key()
   end
 
   def register_changeset(struct, params \\ %{}, opts \\ []) do
@@ -741,10 +741,10 @@ defmodule Pleroma.User do
     |> validate_length(:registration_reason, max: reason_limit)
     |> maybe_validate_required_email(opts[:external])
     |> put_password_hash
-    |> put_keys()
     |> put_ap_id()
     |> unique_constraint(:ap_id)
     |> put_following_and_follower_and_featured_address()
+    |> put_private_key()
   end
 
   def maybe_validate_required_email(changeset, true), do: changeset
@@ -757,11 +757,6 @@ defmodule Pleroma.User do
     end
   end
 
-  def put_keys(changeset) do
-    {:ok, pem} = Keys.generate_rsa_pem()
-    put_change(changeset, :keys, pem)
-  end
-
   def put_ap_id(changeset) do
     ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
     put_change(changeset, :ap_id, ap_id)
@@ -779,6 +774,11 @@ defmodule Pleroma.User do
     |> put_change(:featured_address, featured)
   end
 
+  defp put_private_key(changeset) do
+    {:ok, pem} = Keys.generate_rsa_pem()
+    put_change(changeset, :keys, pem)
+  end
+
   defp autofollow_users(user) do
     candidates = Config.get([:instance, :autofollowed_nicknames])
 
@@ -1955,6 +1955,7 @@ defmodule Pleroma.User do
       follower_address: uri <> "/followers"
     }
     |> change
+    |> put_private_key()
     |> unique_constraint(:nickname)
     |> Repo.insert()
     |> set_cache()
@@ -2220,17 +2221,6 @@ defmodule Pleroma.User do
     }
   end
 
-  def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
-
-  def ensure_keys_present(%User{} = user) do
-    with {:ok, pem} <- Keys.generate_rsa_pem() do
-      user
-      |> cast(%{keys: pem}, [:keys])
-      |> validate_required([:keys])
-      |> update_and_set_cache()
-    end
-  end
-
   def get_ap_ids_by_nicknames(nicknames) do
     from(u in User,
       where: u.nickname in ^nicknames,
index 1eb0a362093da6826b60652ee22c832a537007e9..c07f91b2e9cb0e31dd85071d0608f1869a20683c 100644 (file)
@@ -66,8 +66,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
   end
 
   def user(conn, %{"nickname" => nickname}) do
-    with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
-         {:ok, user} <- User.ensure_keys_present(user) do
+    with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
       conn
       |> put_resp_content_type("application/activity+json")
       |> put_view(UserView)
@@ -174,7 +173,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
 
   def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
     with %User{} = user <- User.get_cached_by_nickname(nickname),
-         {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
          {:show_follows, true} <-
            {:show_follows, (for_user && for_user == user) || !user.hide_follows} do
       {page, _} = Integer.parse(page)
@@ -192,8 +190,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
   end
 
   def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
-    with %User{} = user <- User.get_cached_by_nickname(nickname),
-         {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
+    with %User{} = user <- User.get_cached_by_nickname(nickname) do
       conn
       |> put_resp_content_type("application/activity+json")
       |> put_view(UserView)
@@ -213,7 +210,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
 
   def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
     with %User{} = user <- User.get_cached_by_nickname(nickname),
-         {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
          {:show_followers, true} <-
            {:show_followers, (for_user && for_user == user) || !user.hide_followers} do
       {page, _} = Integer.parse(page)
@@ -231,8 +227,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
   end
 
   def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
-    with %User{} = user <- User.get_cached_by_nickname(nickname),
-         {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
+    with %User{} = user <- User.get_cached_by_nickname(nickname) do
       conn
       |> put_resp_content_type("application/activity+json")
       |> put_view(UserView)
@@ -245,8 +240,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
         %{"nickname" => nickname, "page" => page?} = params
       )
       when page? in [true, "true"] do
-    with %User{} = user <- User.get_cached_by_nickname(nickname),
-         {:ok, user} <- User.ensure_keys_present(user) do
+    with %User{} = user <- User.get_cached_by_nickname(nickname) do
       # "include_poll_votes" is a hack because postgres generates inefficient
       # queries when filtering by 'Answer', poll votes will be hidden by the
       # visibility filter in this case anyway
@@ -270,8 +264,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
   end
 
   def outbox(conn, %{"nickname" => nickname}) do
-    with %User{} = user <- User.get_cached_by_nickname(nickname),
-         {:ok, user} <- User.ensure_keys_present(user) do
+    with %User{} = user <- User.get_cached_by_nickname(nickname) do
       conn
       |> put_resp_content_type("application/activity+json")
       |> put_view(UserView)
@@ -328,14 +321,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
   end
 
   defp represent_service_actor(%User{} = user, conn) do
-    with {:ok, user} <- User.ensure_keys_present(user) do
-      conn
-      |> put_resp_content_type("application/activity+json")
-      |> put_view(UserView)
-      |> render("user.json", %{user: user})
-    else
-      nil -> {:error, :not_found}
-    end
+    conn
+    |> put_resp_content_type("application/activity+json")
+    |> put_view(UserView)
+    |> render("user.json", %{user: user})
   end
 
   defp represent_service_actor(nil, _), do: {:error, :not_found}
@@ -388,12 +377,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
   def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
         "nickname" => nickname
       }) do
-    with {:ok, user} <- User.ensure_keys_present(user) do
-      conn
-      |> put_resp_content_type("application/activity+json")
-      |> put_view(UserView)
-      |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
-    end
+    conn
+    |> put_resp_content_type("application/activity+json")
+    |> put_view(UserView)
+    |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
   end
 
   def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
@@ -530,19 +517,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
     conn
   end
 
-  defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
-    {:ok, new_user} = User.ensure_keys_present(user)
-
-    for_user =
-      if new_user != user and match?(%User{}, for_user) do
-        User.get_cached_by_nickname(for_user.nickname)
-      else
-        for_user
-      end
-
-    {new_user, for_user}
-  end
-
   def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
     with {:ok, object} <-
            ActivityPub.upload(
index 760515f3422de1d19fbc696c2127acdfbb30194d..310f3ce3e345a80afc0b4e8effe959c4f84ecebd 100644 (file)
@@ -34,7 +34,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do
   def render("endpoints.json", _), do: %{}
 
   def render("service.json", %{user: user}) do
-    {:ok, user} = User.ensure_keys_present(user)
     {: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])
@@ -71,7 +70,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do
     do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname)
 
   def render("user.json", %{user: user}) do
-    {:ok, user} = User.ensure_keys_present(user)
     {: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])
index 82fb9e4e005b70a92523b328529ae7dff1d281b7..bc61130f1ef7aa322ddc7dca8a02af2493ec442b 100644 (file)
@@ -69,10 +69,8 @@ defmodule Pleroma.Web.Federator do
   def perform(:publish, activity) do
     Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end)
 
-    with %User{} = actor <- User.get_cached_by_ap_id(activity.data["actor"]),
-         {:ok, actor} <- User.ensure_keys_present(actor) do
-      Publisher.publish(actor, activity)
-    end
+    %User{} = actor = User.get_cached_by_ap_id(activity.data["actor"])
+    Publisher.publish(actor, activity)
   end
 
   def perform(:incoming_ap_doc, params) do
index b5f2cb72ec88b74821227441068839c75774053f..f5a46ce2525ba00522d7ea15deaf4bfcef59c649 100644 (file)
@@ -69,8 +69,6 @@ defmodule Pleroma.Web.WebFinger do
   end
 
   def represent_user(user, "JSON") do
-    {:ok, user} = User.ensure_keys_present(user)
-
     %{
       "subject" => "acct:#{user.nickname}@#{domain()}",
       "aliases" => gather_aliases(user),
@@ -79,8 +77,6 @@ defmodule Pleroma.Web.WebFinger do
   end
 
   def represent_user(user, "XML") do
-    {:ok, user} = User.ensure_keys_present(user)
-
     aliases =
       user
       |> gather_aliases()
index 645622e43a7430f8ce219b1ce9f64b1b368b6af1..9d35f9780c5974ff7e21d157234c63b6c9150efc 100644 (file)
@@ -620,15 +620,15 @@ defmodule Pleroma.UserTest do
       assert changeset.valid?
     end
 
-    test "it sets the password_hash, ap_id and PEM key" do
+    test "it sets the password_hash, ap_id, private key and followers collection address" do
       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.follower_address == "#{changeset.changes.ap_id}/followers"
     end
 
@@ -2130,21 +2130,6 @@ defmodule Pleroma.UserTest do
     end
   end
 
-  describe "ensure_keys_present" do
-    test "it creates keys for a user and stores them in info" do
-      user = insert(:user)
-      refute is_binary(user.keys)
-      {:ok, user} = User.ensure_keys_present(user)
-      assert is_binary(user.keys)
-    end
-
-    test "it doesn't create keys if there already are some" do
-      user = insert(:user, keys: "xxx")
-      {:ok, user} = User.ensure_keys_present(user)
-      assert user.keys == "xxx"
-    end
-  end
-
   describe "get_ap_ids_by_nicknames" do
     test "it returns a list of AP ids for a given set of nicknames" do
       user = insert(:user)
index e49cb99d3d396b768dbc0fce46d1ec573511c3ac..5501e64d66c7d896da53353a2532ed661440d83a 100644 (file)
@@ -12,7 +12,6 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
 
   test "Renders a user, including the public key" do
     user = insert(:user)
-    {:ok, user} = User.ensure_keys_present(user)
 
     result = UserView.render("user.json", %{user: user})
 
@@ -55,7 +54,6 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
 
   test "Does not add an avatar image if the user hasn't set one" do
     user = insert(:user)
-    {:ok, user} = User.ensure_keys_present(user)
 
     result = UserView.render("user.json", %{user: user})
     refute result["icon"]
@@ -67,8 +65,6 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
         banner: %{"url" => [%{"href" => "https://somebanner"}]}
       )
 
-    {:ok, user} = User.ensure_keys_present(user)
-
     result = UserView.render("user.json", %{user: user})
     assert result["icon"]["url"] == "https://someurl"
     assert result["image"]["url"] == "https://somebanner"
@@ -89,7 +85,6 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
   describe "endpoints" do
     test "local users have a usable endpoints structure" do
       user = insert(:user)
-      {:ok, user} = User.ensure_keys_present(user)
 
       result = UserView.render("user.json", %{user: user})
 
@@ -105,7 +100,6 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
 
     test "remote users have an empty endpoints structure" do
       user = insert(:user, local: false)
-      {:ok, user} = User.ensure_keys_present(user)
 
       result = UserView.render("user.json", %{user: user})
 
@@ -115,7 +109,6 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
 
     test "instance users do not expose oAuth endpoints" do
       user = insert(:user, nickname: nil, local: true)
-      {:ok, user} = User.ensure_keys_present(user)
 
       result = UserView.render("user.json", %{user: user})
 
index 6695886dc4f237f7ffceef53c2f476679bd61d0a..2b0426bb7841d9f82ed5dbd2fafa243127cc3322 100644 (file)
@@ -7,6 +7,7 @@ defmodule Pleroma.Factory do
 
   require Pleroma.Constants
 
+  alias Pleroma.Keys
   alias Pleroma.Object
   alias Pleroma.User
 
@@ -28,6 +29,8 @@ defmodule Pleroma.Factory do
   end
 
   def user_factory(attrs \\ %{}) do
+    {:ok, pem} = Keys.generate_rsa_pem()
+
     user = %User{
       name: sequence(:name, &"Test テスト User #{&1}"),
       email: sequence(:email, &"user#{&1}@example.com"),
@@ -39,7 +42,8 @@ defmodule Pleroma.Factory do
       last_refreshed_at: NaiveDateTime.utc_now(),
       notification_settings: %Pleroma.User.NotificationSetting{},
       multi_factor_authentication_settings: %Pleroma.MFA.Settings{},
-      ap_enabled: true
+      ap_enabled: true,
+      keys: pem
     }
 
     urls =