42709ab47251531d594cdf7938b68d089a7a729e
[akkoma] / lib / pleroma / web / salmon / salmon.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.Salmon do
6 @behaviour Pleroma.Web.Federator.Publisher
7
8 @httpoison Application.get_env(:pleroma, :httpoison)
9
10 use Bitwise
11
12 alias Pleroma.Activity
13 alias Pleroma.Instances
14 alias Pleroma.User
15 alias Pleroma.Web.ActivityPub.Visibility
16 alias Pleroma.Web.Federator.Publisher
17 alias Pleroma.Web.OStatus
18 alias Pleroma.Web.OStatus.ActivityRepresenter
19 alias Pleroma.Web.XML
20
21 require Logger
22
23 def decode(salmon) do
24 doc = XML.parse_document(salmon)
25
26 {:xmlObj, :string, data} = :xmerl_xpath.string('string(//me:data[1])', doc)
27 {:xmlObj, :string, sig} = :xmerl_xpath.string('string(//me:sig[1])', doc)
28 {:xmlObj, :string, alg} = :xmerl_xpath.string('string(//me:alg[1])', doc)
29 {:xmlObj, :string, encoding} = :xmerl_xpath.string('string(//me:encoding[1])', doc)
30 {:xmlObj, :string, type} = :xmerl_xpath.string('string(//me:data[1]/@type)', doc)
31
32 {:ok, data} = Base.url_decode64(to_string(data), ignore: :whitespace)
33 {:ok, sig} = Base.url_decode64(to_string(sig), ignore: :whitespace)
34 alg = to_string(alg)
35 encoding = to_string(encoding)
36 type = to_string(type)
37
38 [data, type, encoding, alg, sig]
39 end
40
41 def fetch_magic_key(salmon) do
42 with [data, _, _, _, _] <- decode(salmon),
43 doc <- XML.parse_document(data),
44 uri when not is_nil(uri) <- XML.string_from_xpath("/entry/author[1]/uri", doc),
45 {:ok, public_key} <- User.get_public_key_for_ap_id(uri),
46 magic_key <- encode_key(public_key) do
47 {:ok, magic_key}
48 end
49 end
50
51 def decode_and_validate(magickey, salmon) do
52 [data, type, encoding, alg, sig] = decode(salmon)
53
54 signed_text =
55 [data, type, encoding, alg]
56 |> Enum.map(&Base.url_encode64/1)
57 |> Enum.join(".")
58
59 key = decode_key(magickey)
60
61 verify = :public_key.verify(signed_text, :sha256, sig, key)
62
63 if verify do
64 {:ok, data}
65 else
66 :error
67 end
68 end
69
70 def decode_key("RSA." <> magickey) do
71 make_integer = fn bin ->
72 list = :erlang.binary_to_list(bin)
73 Enum.reduce(list, 0, fn el, acc -> acc <<< 8 ||| el end)
74 end
75
76 [modulus, exponent] =
77 magickey
78 |> String.split(".")
79 |> Enum.map(fn n -> Base.url_decode64!(n, padding: false) end)
80 |> Enum.map(make_integer)
81
82 {:RSAPublicKey, modulus, exponent}
83 end
84
85 def encode_key({:RSAPublicKey, modulus, exponent}) do
86 modulus_enc = :binary.encode_unsigned(modulus) |> Base.url_encode64()
87 exponent_enc = :binary.encode_unsigned(exponent) |> Base.url_encode64()
88
89 "RSA.#{modulus_enc}.#{exponent_enc}"
90 end
91
92 # Native generation of RSA keys is only available since OTP 20+ and in default build conditions
93 # We try at compile time to generate natively an RSA key otherwise we fallback on the old way.
94 try do
95 _ = :public_key.generate_key({:rsa, 2048, 65_537})
96
97 def generate_rsa_pem do
98 key = :public_key.generate_key({:rsa, 2048, 65_537})
99 entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
100 pem = :public_key.pem_encode([entry]) |> String.trim_trailing()
101 {:ok, pem}
102 end
103 rescue
104 _ ->
105 def generate_rsa_pem do
106 port = Port.open({:spawn, "openssl genrsa"}, [:binary])
107
108 {:ok, pem} =
109 receive do
110 {^port, {:data, pem}} -> {:ok, pem}
111 end
112
113 Port.close(port)
114
115 if Regex.match?(~r/RSA PRIVATE KEY/, pem) do
116 {:ok, pem}
117 else
118 :error
119 end
120 end
121 end
122
123 def keys_from_pem(pem) do
124 [private_key_code] = :public_key.pem_decode(pem)
125 private_key = :public_key.pem_entry_decode(private_key_code)
126 {:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} = private_key
127 public_key = {:RSAPublicKey, modulus, exponent}
128 {:ok, private_key, public_key}
129 end
130
131 def encode(private_key, doc) do
132 type = "application/atom+xml"
133 encoding = "base64url"
134 alg = "RSA-SHA256"
135
136 signed_text =
137 [doc, type, encoding, alg]
138 |> Enum.map(&Base.url_encode64/1)
139 |> Enum.join(".")
140
141 signature =
142 signed_text
143 |> :public_key.sign(:sha256, private_key)
144 |> to_string
145 |> Base.url_encode64()
146
147 doc_base64 =
148 doc
149 |> Base.url_encode64()
150
151 # Don't need proper xml building, these strings are safe to leave unescaped
152 salmon = """
153 <?xml version="1.0" encoding="UTF-8"?>
154 <me:env xmlns:me="http://salmon-protocol.org/ns/magic-env">
155 <me:data type="application/atom+xml">#{doc_base64}</me:data>
156 <me:encoding>#{encoding}</me:encoding>
157 <me:alg>#{alg}</me:alg>
158 <me:sig>#{signature}</me:sig>
159 </me:env>
160 """
161
162 {:ok, salmon}
163 end
164
165 def remote_users(%{data: %{"to" => to} = data}) do
166 to = to ++ (data["cc"] || [])
167
168 to
169 |> Enum.map(fn id -> User.get_cached_by_ap_id(id) end)
170 |> Enum.filter(fn user -> user && !user.local end)
171 end
172
173 @doc "Pushes an activity to remote account."
174 def publish_one(%{recipient: %{info: %{salmon: salmon}}} = params),
175 do: publish_one(Map.put(params, :recipient, salmon))
176
177 def publish_one(%{recipient: url, feed: feed} = params) when is_binary(url) do
178 with {:ok, %{status: code}} when code in 200..299 <-
179 @httpoison.post(
180 url,
181 feed,
182 [{"Content-Type", "application/magic-envelope+xml"}]
183 ) do
184 if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
185 do: Instances.set_reachable(url)
186
187 Logger.debug(fn -> "Pushed to #{url}, code #{code}" end)
188 :ok
189 else
190 e ->
191 unless params[:unreachable_since], do: Instances.set_reachable(url)
192 Logger.debug(fn -> "Pushing Salmon to #{url} failed, #{inspect(e)}" end)
193 {:error, "Unreachable instance"}
194 end
195 end
196
197 def publish_one(_), do: :noop
198
199 @supported_activities [
200 "Create",
201 "Follow",
202 "Like",
203 "Announce",
204 "Undo",
205 "Delete"
206 ]
207
208 def is_representable?(%Activity{data: %{"type" => type}} = activity)
209 when type in @supported_activities,
210 do: Visibility.is_public?(activity)
211
212 def is_representable?(_), do: false
213
214 @doc """
215 Publishes an activity to remote accounts
216 """
217 @spec publish(User.t(), Pleroma.Activity.t()) :: none
218 def publish(user, activity)
219
220 def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity)
221 when type in @supported_activities do
222 feed = ActivityRepresenter.to_simple_form(activity, user, true)
223
224 if feed do
225 feed =
226 ActivityRepresenter.wrap_with_entry(feed)
227 |> :xmerl.export_simple(:xmerl_xml)
228 |> to_string
229
230 {:ok, private, _} = keys_from_pem(keys)
231 {:ok, feed} = encode(private, feed)
232
233 remote_users = remote_users(activity)
234
235 salmon_urls = Enum.map(remote_users, & &1.info.salmon)
236 reachable_urls_metadata = Instances.filter_reachable(salmon_urls)
237 reachable_urls = Map.keys(reachable_urls_metadata)
238
239 remote_users
240 |> Enum.filter(&(&1.info.salmon in reachable_urls))
241 |> Enum.each(fn remote_user ->
242 Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end)
243
244 Publisher.enqueue_one(__MODULE__, %{
245 recipient: remote_user,
246 feed: feed,
247 unreachable_since: reachable_urls_metadata[remote_user.info.salmon]
248 })
249 end)
250 end
251 end
252
253 def publish(%{id: id}, _), do: Logger.debug(fn -> "Keys missing for user #{id}" end)
254
255 def gather_webfinger_links(%User{} = user) do
256 {:ok, _private, public} = keys_from_pem(user.info.keys)
257 magic_key = encode_key(public)
258
259 [
260 %{"rel" => "salmon", "href" => OStatus.salmon_path(user)},
261 %{
262 "rel" => "magic-public-key",
263 "href" => "data:application/magic-public-key,#{magic_key}"
264 }
265 ]
266 end
267
268 def gather_nodeinfo_protocol_names, do: []
269 end