aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorEkaterina Vaartis <vaartis@cock.li>2018-12-21 00:32:37 +0300
committerEkaterina Vaartis <vaartis@cock.li>2018-12-21 09:55:47 +0300
commit336e37d98f1b86c0332c9f260e27455a14714fa6 (patch)
treed4c0a116eb5f0ca44766fa9419a6a984bea4fd8b /lib
parent61a88a6757bc557a370888d288f93681cff9668b (diff)
downloadpleroma-336e37d98f1b86c0332c9f260e27455a14714fa6.tar.gz
Make captcha (kocaptcha) stateless
Also rename seconds_retained to seconds_valid since that's how it is now. Put it down from 180 to 20 seconds. The answer data is now stored in an encrypted text transfered to the client and back, so no ETS is needed
Diffstat (limited to 'lib')
-rw-r--r--lib/pleroma/captcha/captcha.ex29
-rw-r--r--lib/pleroma/captcha/captcha_service.ex12
-rw-r--r--lib/pleroma/captcha/kocaptcha.ex82
-rw-r--r--lib/pleroma/web/twitter_api/twitter_api.ex16
4 files changed, 65 insertions, 74 deletions
diff --git a/lib/pleroma/captcha/captcha.ex b/lib/pleroma/captcha/captcha.ex
index 5630f6b57..61a0f907f 100644
--- a/lib/pleroma/captcha/captcha.ex
+++ b/lib/pleroma/captcha/captcha.ex
@@ -1,8 +1,6 @@
defmodule Pleroma.Captcha do
use GenServer
- @ets_options [:ordered_set, :private, :named_table, {:read_concurrency, true}]
-
@doc false
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
@@ -10,14 +8,6 @@ defmodule Pleroma.Captcha do
@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)
-
- # Clean up old captchas every few minutes
- seconds_retained = Pleroma.Config.get!([__MODULE__, :seconds_retained])
- Process.send_after(self(), :cleanup, 1000 * seconds_retained)
-
{:ok, nil}
end
@@ -31,8 +21,8 @@ defmodule Pleroma.Captcha do
@doc """
Ask the configured captcha service to validate the captcha
"""
- def validate(token, captcha) do
- GenServer.call(__MODULE__, {:validate, token, captcha})
+ def validate(token, captcha, answer_data) do
+ GenServer.call(__MODULE__, {:validate, token, captcha, answer_data})
end
@doc false
@@ -47,19 +37,8 @@ defmodule Pleroma.Captcha do
end
@doc false
- def handle_call({:validate, token, captcha}, _from, state) do
- {:reply, method().validate(token, captcha), state}
- end
-
- @doc false
- def handle_info(:cleanup, state) do
- :ok = method().cleanup()
-
- seconds_retained = Pleroma.Config.get!([__MODULE__, :seconds_retained])
- # Schedule the next clenup
- Process.send_after(self(), :cleanup, 1000 * seconds_retained)
-
- {:noreply, state}
+ def handle_call({:validate, token, captcha, answer_data}, _from, state) do
+ {:reply, method().validate(token, captcha, answer_data), 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 8d0b76f86..6f36d29b0 100644
--- a/lib/pleroma/captcha/captcha_service.ex
+++ b/lib/pleroma/captcha/captcha_service.ex
@@ -14,15 +14,15 @@ defmodule Pleroma.Captcha.Service do
Arguments:
* `token` the captcha is associated with
* `captcha` solution of the captcha to validate
+ * `answer_data` is the data needed to validate the answer (presumably encrypted)
Returns:
`true` if captcha is valid, `false` if not
"""
- @callback validate(token :: String.t(), captcha :: String.t()) :: boolean
-
- @doc """
- This function is called periodically to clean up old captchas
- """
- @callback cleanup() :: :ok
+ @callback validate(
+ token :: String.t(),
+ captcha :: String.t(),
+ answer_data :: String.t()
+ ) :: :ok | {:error, String.t()}
end
diff --git a/lib/pleroma/captcha/kocaptcha.ex b/lib/pleroma/captcha/kocaptcha.ex
index 51900d123..f881c7b65 100644
--- a/lib/pleroma/captcha/kocaptcha.ex
+++ b/lib/pleroma/captcha/kocaptcha.ex
@@ -1,11 +1,11 @@
defmodule Pleroma.Captcha.Kocaptcha do
+ alias Plug.Crypto.KeyGenerator
+ alias Plug.Crypto.MessageEncryptor
alias Calendar.DateTime
alias Pleroma.Captcha.Service
@behaviour Service
- @ets __MODULE__.Ets
-
@impl Service
def new() do
endpoint = Pleroma.Config.get!([__MODULE__, :endpoint])
@@ -18,50 +18,56 @@ defmodule Pleroma.Captcha.Kocaptcha do
json_resp = Poison.decode!(res.body)
token = json_resp["token"]
+ answer_md5 = json_resp["md5"]
- true =
- :ets.insert(
- @ets,
- {token, json_resp["md5"], DateTime.now_utc() |> DateTime.Format.unix()}
- )
+ secret_key_base = Pleroma.Config.get!([Pleroma.Web.Endpoint, :secret_key_base])
- %{type: :kocaptcha, token: token, url: endpoint <> json_resp["url"]}
- end
- end
-
- @impl Service
- def validate(token, captcha) 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)
+ # 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)
- true
- else
- _ -> false
+ %{
+ type: :kocaptcha,
+ token: token,
+ url: endpoint <> json_resp["url"],
+ answer_data: encrypted_captcha_answer
+ }
end
end
@impl Service
- def cleanup() do
- seconds_retained = Pleroma.Config.get!([Pleroma.Captcha, :seconds_retained])
- # If the time in ETS is less than current_time - seconds_retained, then the time has
- # already passed
- delete_after =
- DateTime.subtract!(DateTime.now_utc(), seconds_retained) |> DateTime.Format.unix()
+ 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")
- :ets.select_delete(
- @ets,
- [
- {
- {:_, :_, :"$1"},
- [{:<, :"$1", {:const, delete_after}}],
- [true]
- }
- ]
- )
+ # 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)
- :ok
+ 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
end
end
diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex
index d816dc3bc..9e15f2c33 100644
--- a/lib/pleroma/web/twitter_api/twitter_api.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api.ex
@@ -136,22 +136,28 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
password: params["password"],
password_confirmation: params["confirm"],
captcha_solution: params["captcha_solution"],
- captcha_token: params["captcha_token"]
+ captcha_token: params["captcha_token"],
+ captcha_answer_data: params["captcha_answer_data"]
}
captcha_enabled = Pleroma.Config.get([Pleroma.Captcha, :enabled])
# true if captcha is disabled or enabled and valid, false otherwise
captcha_ok =
if !captcha_enabled do
- true
+ :ok
else
- Pleroma.Captcha.validate(params[:captcha_token], params[:captcha_solution])
+ Pleroma.Captcha.validate(
+ params[:captcha_token],
+ params[:captcha_solution],
+ params[:captcha_answer_data]
+ )
end
# Captcha invalid
- if not captcha_ok do
+ if captcha_ok != :ok do
+ {:error, error} = captcha_ok
# I have no idea how this error handling works
- {:error, %{error: Jason.encode!(%{captcha: ["Invalid CAPTCHA"]})}}
+ {:error, %{error: Jason.encode!(%{captcha: [error]})}}
else
registrations_open = Pleroma.Config.get([:instance, :registrations_open])