X-Git-Url: https://git.squeep.com/?a=blobdiff_plain;f=lib%2Fpleroma%2Fcaptcha.ex;h=bad7b3a66e3e65db3b529943990e4e3865825826;hb=565ead8397f01a57acd35142d66a5ee3985f68cf;hp=31f3bc797e02eae73718541f03c23a52611a851b;hpb=a2399c1c7c17ee1c8e85ae0b6095405c7cb9f6f1;p=akkoma diff --git a/lib/pleroma/captcha.ex b/lib/pleroma/captcha.ex index 31f3bc797..bad7b3a66 100644 --- a/lib/pleroma/captcha.ex +++ b/lib/pleroma/captcha.ex @@ -1,68 +1,104 @@ -defmodule Pleroma.Captcha do - use GenServer +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only - @ets __MODULE__.Ets - @ets_options [:ordered_set, :private, :named_table, {:read_concurrency, true}] +defmodule Pleroma.Captcha do + alias Calendar.DateTime + alias Plug.Crypto.KeyGenerator + alias Plug.Crypto.MessageEncryptor + @cachex Pleroma.Config.get([:cachex, :provider], Cachex) - @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() + # This make salt a little different for two keys + {secret, sign_secret} = secret_pair(new_captcha[:token]) - @doc false - def init(_) do - @ets = :ets.new(@ets, @ets_options) + # 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 - def new() do - GenServer.call(__MODULE__, :new) + @doc """ + Ask the configured captcha service to validate the captcha + """ + 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 - 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 - method = Pleroma.Config.get!([__MODULE__, :method]) - - case method do - __MODULE__.Kocaptcha -> - endpoint = Pleroma.Config.get!([method, :endpoint]) - case HTTPoison.get(endpoint <> "/new") do - {:error, _} -> - %{error: "Kocaptcha service unavailable"} - {:ok, res} -> - json_resp = Poison.decode!(res.body) - - token = json_resp["token"] - - true = :ets.insert(@ets, {token, json_resp["md5"]}) - - { - :reply, - %{type: :kocaptcha, token: token, url: endpoint <> json_resp["url"]}, - state - } - end + defp validate_answer_data(token, answer_data) do + {secret, sign_secret} = secret_pair(token) + + 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 + _ -> {:error, :invalid_answer_data} end end - @doc false - def handle_call({:validate, token, captcha}, _from, state) do - with false <- is_nil(captcha), - [{^token, saved_md5}] <- :ets.lookup(@ets, token), - true <- (:crypto.hash(:md5, captcha) |> Base.encode16) == String.upcase(saved_md5) do - # Clear the saved value - :ets.delete(@ets, token) + 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()) - {:reply, true, state} + if DateTime.before?(created_at, valid_if_after) do + {:error, :expired} else - e -> IO.inspect(e); {:reply, false, state} + :ok end end + + defp validate_usage(token) do + if is_nil(@cachex.get!(:used_captcha_cache, token)) do + :ok + else + {:error, :already_used} + end + end + + 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]) end