80c3a3190831de92121a4cd86e3ab609d02ed10e
[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(%User{id: user_id}, %{data: %{"to" => to} = data}) do
166 cc = Map.get(data, "cc", [])
167
168 bcc =
169 data
170 |> Map.get("bcc", [])
171 |> Enum.reduce([], fn ap_id, bcc ->
172 case Pleroma.List.get_by_ap_id(ap_id) do
173 %Pleroma.List{user_id: ^user_id} = list ->
174 {:ok, following} = Pleroma.List.get_following(list)
175 bcc ++ Enum.map(following, & &1.ap_id)
176
177 _ ->
178 bcc
179 end
180 end)
181
182 [to, cc, bcc]
183 |> Enum.concat()
184 |> Enum.map(&User.get_cached_by_ap_id/1)
185 |> Enum.filter(fn user -> user && !user.local end)
186 end
187
188 @doc "Pushes an activity to remote account."
189 def publish_one(%{recipient: %{info: %{salmon: salmon}}} = params),
190 do: publish_one(Map.put(params, :recipient, salmon))
191
192 def publish_one(%{recipient: url, feed: feed} = params) when is_binary(url) do
193 with {:ok, %{status: code}} when code in 200..299 <-
194 @httpoison.post(
195 url,
196 feed,
197 [{"Content-Type", "application/magic-envelope+xml"}]
198 ) do
199 if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
200 do: Instances.set_reachable(url)
201
202 Logger.debug(fn -> "Pushed to #{url}, code #{code}" end)
203 :ok
204 else
205 e ->
206 unless params[:unreachable_since], do: Instances.set_reachable(url)
207 Logger.debug(fn -> "Pushing Salmon to #{url} failed, #{inspect(e)}" end)
208 {:error, "Unreachable instance"}
209 end
210 end
211
212 def publish_one(_), do: :noop
213
214 @supported_activities [
215 "Create",
216 "Follow",
217 "Like",
218 "Announce",
219 "Undo",
220 "Delete"
221 ]
222
223 def is_representable?(%Activity{data: %{"type" => type}} = activity)
224 when type in @supported_activities,
225 do: Visibility.is_public?(activity)
226
227 def is_representable?(_), do: false
228
229 @doc """
230 Publishes an activity to remote accounts
231 """
232 @spec publish(User.t(), Pleroma.Activity.t()) :: none
233 def publish(user, activity)
234
235 def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity)
236 when type in @supported_activities do
237 feed = ActivityRepresenter.to_simple_form(activity, user, true)
238
239 if feed do
240 feed =
241 ActivityRepresenter.wrap_with_entry(feed)
242 |> :xmerl.export_simple(:xmerl_xml)
243 |> to_string
244
245 {:ok, private, _} = keys_from_pem(keys)
246 {:ok, feed} = encode(private, feed)
247
248 remote_users = remote_users(user, activity)
249
250 salmon_urls = Enum.map(remote_users, & &1.info.salmon)
251 reachable_urls_metadata = Instances.filter_reachable(salmon_urls)
252 reachable_urls = Map.keys(reachable_urls_metadata)
253
254 remote_users
255 |> Enum.filter(&(&1.info.salmon in reachable_urls))
256 |> Enum.each(fn remote_user ->
257 Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end)
258
259 Publisher.enqueue_one(__MODULE__, %{
260 recipient: remote_user,
261 feed: feed,
262 unreachable_since: reachable_urls_metadata[remote_user.info.salmon]
263 })
264 end)
265 end
266 end
267
268 def publish(%{id: id}, _), do: Logger.debug(fn -> "Keys missing for user #{id}" end)
269
270 def gather_webfinger_links(%User{} = user) do
271 {:ok, _private, public} = keys_from_pem(user.info.keys)
272 magic_key = encode_key(public)
273
274 [
275 %{"rel" => "salmon", "href" => OStatus.salmon_path(user)},
276 %{
277 "rel" => "magic-public-key",
278 "href" => "data:application/magic-public-key,#{magic_key}"
279 }
280 ]
281 end
282
283 def gather_nodeinfo_protocol_names, do: []
284 end