diff --git a/lib/pleroma/captcha/captcha.ex b/lib/pleroma/captcha/captcha.ex index 61a0f907f..04769d4b2 100644 --- a/lib/pleroma/captcha/captcha.ex +++ b/lib/pleroma/captcha/captcha.ex @@ -1,4 +1,8 @@ defmodule Pleroma.Captcha do + alias Plug.Crypto.KeyGenerator + alias Plug.Crypto.MessageEncryptor + alias Calendar.DateTime + use GenServer @doc false @@ -32,13 +36,58 @@ defmodule Pleroma.Captcha do if !enabled do {:reply, %{type: :none}, state} else - {:reply, method().new(), state} + new_captcha = method().new() + + secret_key_base = Pleroma.Config.get!([Pleroma.Web.Endpoint, :secret_key_base]) + + # This make salt a little different for two keys + token = new_captcha[:token] + secret = KeyGenerator.generate(secret_key_base, token <> "_encrypt") + sign_secret = KeyGenerator.generate(secret_key_base, token <> "_sign") + # Basicallty copy what Phoenix.Token does here, add the time to + # the actual data and make it a binary to then encrypt it + encrypted_captcha_answer = + %{ + at: DateTime.now_utc(), + answer_data: new_captcha[:answer_data] + } + |> :erlang.term_to_binary() + |> MessageEncryptor.encrypt(secret, sign_secret) + + IO.inspect(%{new_captcha | answer_data: encrypted_captcha_answer}) + + { + :reply, + # Repalce the answer with the encrypted answer + %{new_captcha | answer_data: encrypted_captcha_answer}, + state + } end end @doc false def handle_call({:validate, token, captcha, answer_data}, _from, state) do - {:reply, method().validate(token, captcha, answer_data), state} + secret_key_base = Pleroma.Config.get!([Pleroma.Web.Endpoint, :secret_key_base]) + secret = KeyGenerator.generate(secret_key_base, token <> "_encrypt") + sign_secret = KeyGenerator.generate(secret_key_base, token <> "_sign") + + # If the time found is less than (current_time - seconds_valid), then the time has already passed. + # Later we check that the time found is more than the presumed invalidatation time, that means + # that the data is still valid and the captcha can be checked + seconds_valid = Pleroma.Config.get!([Pleroma.Captcha, :seconds_valid]) + valid_if_after = DateTime.subtract!(DateTime.now_utc(), seconds_valid) + + result = + with {:ok, data} <- MessageEncryptor.decrypt(answer_data, secret, sign_secret), + %{at: at, answer_data: answer_md5} <- :erlang.binary_to_term(data) do + if DateTime.after?(at, valid_if_after), + do: method().validate(token, captcha, answer_md5), + else: {:error, "CAPTCHA expired"} + else + _ -> {:error, "Invalid answer data"} + end + + {:reply, result, state} end defp method, do: Pleroma.Config.get!([__MODULE__, :method]) diff --git a/lib/pleroma/captcha/captcha_service.ex b/lib/pleroma/captcha/captcha_service.ex index 6f36d29b0..6c5ab6c36 100644 --- a/lib/pleroma/captcha/captcha_service.ex +++ b/lib/pleroma/captcha/captcha_service.ex @@ -4,9 +4,14 @@ defmodule Pleroma.Captcha.Service do Returns: - Service-specific data for using the newly created captcha + Type/Name of the service, the token to identify the captcha, + the data of the answer and service-specific data to use the newly created captcha """ - @callback new() :: map + @callback new() :: %{ + type: atom(), + token: String.t(), + answer_data: any() + } @doc """ Validated the provided captcha solution. @@ -23,6 +28,6 @@ defmodule Pleroma.Captcha.Service do @callback validate( token :: String.t(), captcha :: String.t(), - answer_data :: String.t() + answer_data :: any() ) :: :ok | {:error, String.t()} end diff --git a/lib/pleroma/captcha/kocaptcha.ex b/lib/pleroma/captcha/kocaptcha.ex index f881c7b65..cd0eb6f21 100644 --- a/lib/pleroma/captcha/kocaptcha.ex +++ b/lib/pleroma/captcha/kocaptcha.ex @@ -1,8 +1,4 @@ defmodule Pleroma.Captcha.Kocaptcha do - alias Plug.Crypto.KeyGenerator - alias Plug.Crypto.MessageEncryptor - alias Calendar.DateTime - alias Pleroma.Captcha.Service @behaviour Service @@ -17,57 +13,21 @@ defmodule Pleroma.Captcha.Kocaptcha do {:ok, res} -> json_resp = Poison.decode!(res.body) - token = json_resp["token"] - answer_md5 = json_resp["md5"] - - secret_key_base = Pleroma.Config.get!([Pleroma.Web.Endpoint, :secret_key_base]) - - # This make salt a little different for two keys - secret = KeyGenerator.generate(secret_key_base, token <> "_encrypt") - sign_secret = KeyGenerator.generate(secret_key_base, token <> "_sign") - # Basicallty copy what Phoenix.Token does here, add the time to - # the actual data and make it a binary to then encrypt it - encrypted_captcha_answer = - %{ - at: DateTime.now_utc(), - answer_md5: answer_md5 - } - |> :erlang.term_to_binary() - |> MessageEncryptor.encrypt(secret, sign_secret) - %{ type: :kocaptcha, - token: token, + token: json_resp["token"], url: endpoint <> json_resp["url"], - answer_data: encrypted_captcha_answer + answer_data: json_resp["md5"] } end end @impl Service - def validate(token, captcha, answer_data) do - secret_key_base = Pleroma.Config.get!([Pleroma.Web.Endpoint, :secret_key_base]) - secret = KeyGenerator.generate(secret_key_base, token <> "_encrypt") - sign_secret = KeyGenerator.generate(secret_key_base, token <> "_sign") - - # If the time found is less than (current_time - seconds_valid), then the time has already passed. - # Later we check that the time found is more than the presumed invalidatation time, that means - # that the data is still valid and the captcha can be checked - seconds_valid = Pleroma.Config.get!([Pleroma.Captcha, :seconds_valid]) - valid_if_after = DateTime.subtract!(DateTime.now_utc(), seconds_valid) - - with {:ok, data} <- MessageEncryptor.decrypt(answer_data, secret, sign_secret), - %{at: at, answer_md5: answer_md5} <- :erlang.binary_to_term(data) do - if DateTime.after?(at, valid_if_after) do - if not is_nil(captcha) and - :crypto.hash(:md5, captcha) |> Base.encode16() == String.upcase(answer_md5), - do: :ok, - else: {:error, "Invalid CAPTCHA"} - else - {:error, "CAPTCHA expired"} - end - else - _ -> {:error, "Invalid answer data"} - end + def validate(_token, captcha, answer_data) do + # Here the token is unsed, because the unencrypted captcha answer is just passed to method + if not is_nil(captcha) and + :crypto.hash(:md5, captcha) |> Base.encode16() == String.upcase(answer_data), + do: :ok, + else: {:error, "Invalid CAPTCHA"} end end