X-Git-Url: https://git.squeep.com/?a=blobdiff_plain;f=lib%2Fpleroma%2Fcaptcha%2Fcaptcha.ex;h=6ab754b6f1dc333930471555b5c09c5e593144a0;hb=38425ebdbf157377ccb0402f78dc3d02f81c55f5;hp=f80946c8bb5dd27abaf075652fdcb608d1005bb5;hpb=2791ce9a1ff2365ac7256f5e1dc2324dee2f82c9;p=akkoma diff --git a/lib/pleroma/captcha/captcha.ex b/lib/pleroma/captcha/captcha.ex index f80946c8b..6ab754b6f 100644 --- a/lib/pleroma/captcha/captcha.ex +++ b/lib/pleroma/captcha/captcha.ex @@ -1,69 +1,101 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2018 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Captcha do - use GenServer + alias Calendar.DateTime + alias Plug.Crypto.KeyGenerator + alias Plug.Crypto.MessageEncryptor - @ets_options [:ordered_set, :private, :named_table, {:read_concurrency, true}] - - @doc false - def start_link() do - GenServer.start_link(__MODULE__, [], name: __MODULE__) - end + @doc """ + Ask the configured captcha service for a new captcha + """ + def new do + if not enabled?() do + %{type: :none} + else + new_captcha = method().new() - @doc false - def init(_) do - # Create a ETS table to store captchas - ets_name = Module.concat(method(), Ets) - ^ets_name = :ets.new(Module.concat(method(), Ets), @ets_options) + # This make salt a little different for two keys + {secret, sign_secret} = secret_pair(new_captcha[:token]) - # Clean up old captchas every few minutes - seconds_retained = Pleroma.Config.get!([__MODULE__, :seconds_retained]) - Process.send_after(self(), :cleanup, 1000 * seconds_retained) + # Basically 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) - {:ok, nil} + # Replace the answer with the encrypted answer + %{new_captcha | answer_data: encrypted_captcha_answer} + end end @doc """ - Ask the configured captcha service for a new captcha + Ask the configured captcha service to validate the captcha """ - def new() do - GenServer.call(__MODULE__, :new) + def validate(token, captcha, answer_data) do + with {:ok, %{at: at, answer_data: answer_md5}} <- validate_answer_data(token, answer_data), + :ok <- validate_expiration(at), + :ok <- validate_usage(token), + :ok <- method().validate(token, captcha, answer_md5), + {:ok, _} <- mark_captcha_as_used(token) do + :ok + end end - @doc """ - Ask the configured captcha service to validate the captcha - """ - def validate(token, captcha) do - GenServer.call(__MODULE__, {:validate, token, captcha}) + def enabled?, do: Pleroma.Config.get([__MODULE__, :enabled], false) + + defp seconds_valid, do: Pleroma.Config.get!([__MODULE__, :seconds_valid]) + + defp secret_pair(token) 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") + + {secret, sign_secret} end - @doc false - def handle_call(:new, _from, state) do - enabled = Pleroma.Config.get([__MODULE__, :enabled]) + defp validate_answer_data(token, answer_data) do + {secret, sign_secret} = secret_pair(token) - if !enabled do - {:reply, %{type: :none}, state} + with false <- is_nil(answer_data), + {:ok, data} <- MessageEncryptor.decrypt(answer_data, secret, sign_secret), + %{at: at, answer_data: answer_md5} <- :erlang.binary_to_term(data) do + {:ok, %{at: at, answer_data: answer_md5}} else - {:reply, method().new(), state} + _ -> {:error, :invalid_answer_data} end end - @doc false - def handle_call({:validate, token, captcha}, _from, state) do - {:reply, method().validate(token, captcha), state} - end + defp validate_expiration(created_at) do + # 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 + + valid_if_after = DateTime.subtract!(DateTime.now_utc(), seconds_valid()) - @doc false - def handle_info(:cleanup, state) do - :ok = method().cleanup() + if DateTime.before?(created_at, valid_if_after) do + {:error, :expired} + else + :ok + end + end - seconds_retained = Pleroma.Config.get!([__MODULE__, :seconds_retained]) - # Schedule the next clenup - Process.send_after(self(), :cleanup, 1000 * seconds_retained) + defp validate_usage(token) do + if is_nil(Cachex.get!(:used_captcha_cache, token)) do + :ok + else + {:error, :already_used} + end + end - {:noreply, state} + defp mark_captcha_as_used(token) do + ttl = seconds_valid() |> :timer.seconds() + Cachex.put(:used_captcha_cache, token, true, ttl: ttl) end defp method, do: Pleroma.Config.get!([__MODULE__, :method])