Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85827279
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
19 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/pleroma/multi_factor_authentications.ex b/lib/pleroma/multi_factor_authentications.ex
index 60eb49b6b..5cd37f329 100644
--- a/lib/pleroma/multi_factor_authentications.ex
+++ b/lib/pleroma/multi_factor_authentications.ex
@@ -1,115 +1,153 @@
defmodule Pleroma.MultiFactorAuthentications do
@moduledoc """
The MultiFactorAuthentications context.
"""
alias Comeonin.Pbkdf2
alias Pleroma.User
alias Pleroma.Web.Auth.TOTP
alias Pleroma.MultiFactorAuthentications.Changeset
alias Pleroma.MultiFactorAuthentications.Settings
alias Pleroma.Repo
alias Pleroma.Web.CommonAPI.Utils
- def supported_challenge_types(user) do
+ @doc "Returns enabled methods of user"
+ def supported_methods(user) do
settings = fetch_settings(user)
Settings.mfa_methods()
|> Enum.map(fn m -> [m, enable_method?(m, settings)] end)
|> Enum.into(%{}, fn [m, v] -> {m, v} end)
|> Enum.reduce(
[],
fn
{k, true}, acc -> acc ++ [k]
_, acc -> acc
end
)
|> Enum.join(",")
end
+ @doc "Checks that user enabled MFA"
def require?(user) do
fetch_settings(user).enabled
end
+ @doc """
+ Display MFA settings of user
+ """
def mfa_settings(user) do
settings = fetch_settings(user)
Settings.mfa_methods()
|> Enum.map(fn m -> [m, enable_method?(m, settings)] end)
|> Enum.into(%{enabled: settings.enabled}, fn [a, b] -> {a, b} end)
end
+ @doc false
def fetch_settings(%User{} = user) do
user.multi_factor_authentication_settings || %Settings{}
end
@doc "clears backup codes"
def invalidate_backup_code(%User{} = user, hash_code) do
%{backup_codes: codes} = fetch_settings(user)
user
|> Changeset.cast_backup_codes(codes -- [hash_code])
|> Repo.update()
end
+ @doc "generates backup codes"
+ @spec generate_backup_codes(User.t()) :: {:ok, list(binary)} | {:error, String.t()}
def generate_backup_codes(%User{} = user) do
with codes <- TOTP.generate_backup_codes(),
hashed_codes <- Enum.map(codes, fn code -> Pbkdf2.hashpwsalt(code) end),
%Ecto.Changeset{valid?: true} = changeset <-
Changeset.cast_backup_codes(user, hashed_codes),
{:ok, _} <- Repo.update(changeset) do
{:ok, codes}
else
{:error, msg} ->
%{error: msg}
end
end
+ @doc """
+ Generates secret key and set delivery_type to 'app' for TOTP method.
+ """
+ @spec setup_totp(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def setup_totp(user) do
user
|> Changeset.setup_totp(%{secret: TOTP.generate_secret(), delivery_type: "app"})
|> Repo.update()
end
+ @doc """
+ Confirms the TOTP method for user.
+
+ `attrs`:
+ `password` - current user password
+ `code` - TOTP token
+ """
+ @spec confirm_totp(User.t(), map()) ::
+ {:ok, User.t()} | {:error, Ecto.Changeset.t() | String.t() | atom()}
def confirm_totp(%User{} = user, attrs) do
with settings <- user.multi_factor_authentication_settings.totp,
{:ok, _user} <- Utils.confirm_current_password(user, attrs["password"]),
{:ok, :pass} <- TOTP.validate_token(settings.secret, attrs["code"]) do
user
|> Changeset.confirm_totp()
|> Repo.update()
end
end
+ @doc """
+ Disables the TOTP method for user.
+
+ `attrs`:
+ `password` - current user password
+ """
+ @spec disable_totp(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t() | String.t()}
def disable_totp(%User{} = user, attrs) do
with {:ok, user} <- Utils.confirm_current_password(user, attrs["password"]) do
user
|> Changeset.disable_totp()
|> Changeset.disable()
|> Repo.update()
end
end
+ @doc """
+ Force disable MFA for user. (for admin).
+ """
+ @spec disable(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def disable(%User{} = user) do
user
|> Changeset.disable_totp()
|> Changeset.disable(true)
|> Repo.update()
end
+ @doc """
+ Checks that user enabled method MFA.
+ """
def enable_method?(method, settings) do
with {:ok, %{confirmed: true} = _} <- Map.fetch(settings, method) do
true
else
_ -> false
end
end
+ @doc """
+ Checks that user has enabled at least one method
+ """
def has_confirmed_method?(settings) do
Settings.mfa_methods()
|> Enum.map(fn m -> enable_method?(m, settings) end)
|> Enum.any?()
end
end
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index d93c0d46e..d69ea5a27 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -1,424 +1,425 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPI.Utils do
alias Calendar.Strftime
alias Comeonin.Pbkdf2
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Formatter
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Endpoint
alias Pleroma.Web.MediaProxy
require Logger
# This is a hack for twidere.
def get_by_id_or_ap_id(id) do
activity =
Activity.get_by_id_with_object(id) || Activity.get_create_by_object_ap_id_with_object(id)
activity &&
if activity.data["type"] == "Create" do
activity
else
Activity.get_create_by_object_ap_id_with_object(activity.data["object"])
end
end
def get_replied_to_activity(""), do: nil
def get_replied_to_activity(id) when not is_nil(id) do
Activity.get_by_id(id)
end
def get_replied_to_activity(_), do: nil
def attachments_from_ids(data) do
if Map.has_key?(data, "descriptions") do
attachments_from_ids_descs(data["media_ids"], data["descriptions"])
else
attachments_from_ids_no_descs(data["media_ids"])
end
end
def attachments_from_ids_no_descs(ids) do
Enum.map(ids || [], fn media_id ->
Repo.get(Object, media_id).data
end)
end
def attachments_from_ids_descs(ids, descs_str) do
{_, descs} = Jason.decode(descs_str)
Enum.map(ids || [], fn media_id ->
Map.put(Repo.get(Object, media_id).data, "name", descs[media_id])
end)
end
def to_for_user_and_mentions(user, mentions, inReplyTo, "public") do
mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)
to = ["https://www.w3.org/ns/activitystreams#Public" | mentioned_users]
cc = [user.follower_address]
if inReplyTo do
{Enum.uniq([inReplyTo.data["actor"] | to]), cc}
else
{to, cc}
end
end
def to_for_user_and_mentions(user, mentions, inReplyTo, "unlisted") do
mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)
to = [user.follower_address | mentioned_users]
cc = ["https://www.w3.org/ns/activitystreams#Public"]
if inReplyTo do
{Enum.uniq([inReplyTo.data["actor"] | to]), cc}
else
{to, cc}
end
end
def to_for_user_and_mentions(user, mentions, inReplyTo, "private") do
{to, cc} = to_for_user_and_mentions(user, mentions, inReplyTo, "direct")
{[user.follower_address | to], cc}
end
def to_for_user_and_mentions(_user, mentions, inReplyTo, "direct") do
mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)
if inReplyTo do
{Enum.uniq([inReplyTo.data["actor"] | mentioned_users]), []}
else
{mentioned_users, []}
end
end
def make_content_html(
status,
attachments,
data,
visibility
) do
no_attachment_links =
data
|> Map.get("no_attachment_links", Config.get([:instance, :no_attachment_links]))
|> Kernel.in([true, "true"])
content_type = get_content_type(data["content_type"])
options =
if visibility == "direct" && Config.get([:instance, :safe_dm_mentions]) do
[safe_mention: true]
else
[]
end
status
|> format_input(content_type, options)
|> maybe_add_attachments(attachments, no_attachment_links)
|> maybe_add_nsfw_tag(data)
end
defp get_content_type(content_type) do
if Enum.member?(Config.get([:instance, :allowed_post_formats]), content_type) do
content_type
else
"text/plain"
end
end
defp maybe_add_nsfw_tag({text, mentions, tags}, %{"sensitive" => sensitive})
when sensitive in [true, "True", "true", "1"] do
{text, mentions, [{"#nsfw", "nsfw"} | tags]}
end
defp maybe_add_nsfw_tag(data, _), do: data
def make_context(%Activity{data: %{"context" => context}}), do: context
def make_context(_), do: Utils.generate_context_id()
def maybe_add_attachments(parsed, _attachments, true = _no_links), do: parsed
def maybe_add_attachments({text, mentions, tags}, attachments, _no_links) do
text = add_attachments(text, attachments)
{text, mentions, tags}
end
def add_attachments(text, attachments) do
attachment_text =
Enum.map(attachments, fn
%{"url" => [%{"href" => href} | _]} = attachment ->
name = attachment["name"] || URI.decode(Path.basename(href))
href = MediaProxy.url(href)
"<a href=\"#{href}\" class='attachment'>#{shortname(name)}</a>"
_ ->
""
end)
Enum.join([text | attachment_text], "<br>")
end
def format_input(text, format, options \\ [])
@doc """
Formatting text to plain text.
"""
def format_input(text, "text/plain", options) do
text
|> Formatter.html_escape("text/plain")
|> Formatter.linkify(options)
|> (fn {text, mentions, tags} ->
{String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
end).()
end
@doc """
Formatting text as BBCode.
"""
def format_input(text, "text/bbcode", options) do
text
|> String.replace(~r/\r/, "")
|> Formatter.html_escape("text/plain")
|> BBCode.to_html()
|> (fn {:ok, html} -> html end).()
|> Formatter.linkify(options)
end
@doc """
Formatting text to html.
"""
def format_input(text, "text/html", options) do
text
|> Formatter.html_escape("text/html")
|> Formatter.linkify(options)
end
@doc """
Formatting text to markdown.
"""
def format_input(text, "text/markdown", options) do
text
|> Formatter.mentions_escape(options)
|> Earmark.as_html!()
|> Formatter.linkify(options)
|> Formatter.html_escape("text/html")
end
def make_note_data(
actor,
to,
context,
content_html,
attachments,
in_reply_to,
tags,
cw \\ nil,
cc \\ [],
sensitive \\ false
) do
object = %{
"type" => "Note",
"to" => to,
"cc" => cc,
"content" => content_html,
"summary" => cw,
"sensitive" => !Enum.member?(["false", "False", "0", false], sensitive),
"context" => context,
"attachment" => attachments,
"actor" => actor,
"tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq()
}
with false <- is_nil(in_reply_to),
%Object{} = in_reply_to_object <- Object.normalize(in_reply_to) do
Map.put(object, "inReplyTo", in_reply_to_object.data["id"])
else
_ -> object
end
end
def format_naive_asctime(date) do
date |> DateTime.from_naive!("Etc/UTC") |> format_asctime
end
def format_asctime(date) do
Strftime.strftime!(date, "%a %b %d %H:%M:%S %z %Y")
end
def date_to_asctime(date) when is_binary(date) do
with {:ok, date, _offset} <- DateTime.from_iso8601(date) do
format_asctime(date)
else
_e ->
Logger.warn("Date #{date} in wrong format, must be ISO 8601")
""
end
end
def date_to_asctime(date) do
Logger.warn("Date #{date} in wrong format, must be ISO 8601")
""
end
def to_masto_date(%NaiveDateTime{} = date) do
date
|> NaiveDateTime.to_iso8601()
|> String.replace(~r/(\.\d+)?$/, ".000Z", global: false)
end
def to_masto_date(date) do
try do
date
|> NaiveDateTime.from_iso8601!()
|> NaiveDateTime.to_iso8601()
|> String.replace(~r/(\.\d+)?$/, ".000Z", global: false)
rescue
_e -> ""
end
end
defp shortname(name) do
if String.length(name) < 30 do
name
else
String.slice(name, 0..30) <> "…"
end
end
+ @spec confirm_current_password(User.t(), String.t()) :: {:ok, User.t()} | {:error, String.t()}
def confirm_current_password(user, password) do
with %User{local: true} = db_user <- User.get_cached_by_id(user.id),
true <- Pbkdf2.checkpw(password, db_user.password_hash) do
{:ok, db_user}
else
_ -> {:error, "Invalid password."}
end
end
def emoji_from_profile(%{info: _info} = user) do
(Formatter.get_emoji(user.bio) ++ Formatter.get_emoji(user.name))
|> Enum.map(fn {shortcode, url, _} ->
%{
"type" => "Emoji",
"icon" => %{"type" => "Image", "url" => "#{Endpoint.url()}#{url}"},
"name" => ":#{shortcode}:"
}
end)
end
def maybe_notify_to_recipients(
recipients,
%Activity{data: %{"to" => to, "type" => _type}} = _activity
) do
recipients ++ to
end
def maybe_notify_mentioned_recipients(
recipients,
%Activity{data: %{"to" => _to, "type" => type} = data} = activity
)
when type == "Create" do
object = Object.normalize(activity)
object_data =
cond do
!is_nil(object) ->
object.data
is_map(data["object"]) ->
data["object"]
true ->
%{}
end
tagged_mentions = maybe_extract_mentions(object_data)
recipients ++ tagged_mentions
end
def maybe_notify_mentioned_recipients(recipients, _), do: recipients
def maybe_notify_subscribers(
recipients,
%Activity{data: %{"actor" => actor, "type" => type}} = activity
)
when type == "Create" do
with %User{} = user <- User.get_cached_by_ap_id(actor) do
subscriber_ids =
user
|> User.subscribers()
|> Enum.filter(&Visibility.visible_for_user?(activity, &1))
|> Enum.map(& &1.ap_id)
recipients ++ subscriber_ids
end
end
def maybe_notify_subscribers(recipients, _), do: recipients
def maybe_extract_mentions(%{"tag" => tag}) do
tag
|> Enum.filter(fn x -> is_map(x) end)
|> Enum.filter(fn x -> x["type"] == "Mention" end)
|> Enum.map(fn x -> x["href"] end)
end
def maybe_extract_mentions(_), do: []
def make_report_content_html(nil), do: {:ok, {nil, [], []}}
def make_report_content_html(comment) do
max_size = Pleroma.Config.get([:instance, :max_report_comment_size], 1000)
if String.length(comment) <= max_size do
{:ok, format_input(comment, "text/plain")}
else
{:error, "Comment must be up to #{max_size} characters"}
end
end
def get_report_statuses(%User{ap_id: actor}, %{"status_ids" => status_ids}) do
{:ok, Activity.all_by_actor_and_id(actor, status_ids)}
end
def get_report_statuses(_, _), do: {:ok, nil}
# DEPRECATED mostly, context objects are now created at insertion time.
def context_to_conversation_id(context) do
with %Object{id: id} <- Object.get_cached_by_ap_id(context) do
id
else
_e ->
changeset = Object.context_mapping(context)
case Repo.insert(changeset) do
{:ok, %{id: id}} ->
id
# This should be solved by an upsert, but it seems ecto
# has problems accessing the constraint inside the jsonb.
{:error, _} ->
Object.get_cached_by_ap_id(context).id
end
end
end
def conversation_id_to_context(id) do
with %Object{data: %{"id" => context}} <- Repo.get(Object, id) do
context
else
_e ->
{:error, "No such conversation"}
end
end
end
diff --git a/lib/pleroma/web/oauth/token/response.ex b/lib/pleroma/web/oauth/token/response.ex
index ad99e6433..2c5501ef8 100644
--- a/lib/pleroma/web/oauth/token/response.ex
+++ b/lib/pleroma/web/oauth/token/response.ex
@@ -1,41 +1,41 @@
defmodule Pleroma.Web.OAuth.Token.Response do
@moduledoc false
alias Pleroma.User
alias Pleroma.Web.OAuth.Token.Utils
alias Pleroma.MultiFactorAuthentications, as: MFA
@expires_in Pleroma.Config.get([:oauth2, :token_expires_in], 600)
@doc false
def build(%User{} = user, token, opts \\ %{}) do
%{
token_type: "Bearer",
access_token: token.token,
refresh_token: token.refresh_token,
expires_in: @expires_in,
scope: Enum.join(token.scopes, " "),
me: user.ap_id
}
|> Map.merge(opts)
end
def build_for_client_credentials(token) do
%{
token_type: "Bearer",
access_token: token.token,
refresh_token: token.refresh_token,
created_at: Utils.format_created_at(token),
expires_in: @expires_in,
scope: Enum.join(token.scopes, " ")
}
end
def build_for_mfa_token(user, mfa_token) do
%{
error: "mfa_required",
mfa_token: mfa_token.token,
- supported_challenge_types: MFA.supported_challenge_types(user)
+ supported_challenge_types: MFA.supported_methods(user)
}
end
end
diff --git a/test/multi_factor_authentications_test.exs b/test/multi_factor_authentications_test.exs
new file mode 100644
index 000000000..43d6da159
--- /dev/null
+++ b/test/multi_factor_authentications_test.exs
@@ -0,0 +1,53 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.MultiFactorAuthenticationsTest do
+ use Pleroma.DataCase
+
+ import Pleroma.Factory
+ alias Pleroma.MultiFactorAuthentications, as: MFA
+ alias Comeonin.Pbkdf2
+
+ describe "mfa_settings" do
+ test "returns settings user's" do
+ user =
+ insert(:user,
+ multi_factor_authentication_settings: %MFA.Settings{
+ enabled: true,
+ totp: %MFA.Settings.TOTP{secret: "xx", confirmed: true}
+ }
+ )
+
+ settings = MFA.mfa_settings(user)
+ assert match?(^settings, %{enabled: true, totp: true})
+ end
+ end
+
+ describe "generate backup codes" do
+ test "returns backup codes" do
+ user = insert(:user)
+
+ {:ok, [code1, code2]} = MFA.generate_backup_codes(user)
+ updated_user = refresh_record(user)
+ [hash1, hash2] = updated_user.multi_factor_authentication_settings.backup_codes
+ assert Pbkdf2.checkpw(code1, hash1)
+ assert Pbkdf2.checkpw(code2, hash2)
+ end
+ end
+
+ describe "invalidate_backup_code" do
+ test "invalid used code" do
+ user = insert(:user)
+
+ {:ok, _} = MFA.generate_backup_codes(user)
+ user = refresh_record(user)
+ assert length(user.multi_factor_authentication_settings.backup_codes) == 2
+ [hash_code | _] = user.multi_factor_authentication_settings.backup_codes
+
+ {:ok, user} = MFA.invalidate_backup_code(user, hash_code)
+
+ assert length(user.multi_factor_authentication_settings.backup_codes) == 1
+ end
+ end
+end
diff --git a/test/pleroma/multi_factor_authentications/multi_factor_authentications_test.exs b/test/pleroma/multi_factor_authentications/multi_factor_authentications_test.exs
deleted file mode 100644
index 471d97413..000000000
--- a/test/pleroma/multi_factor_authentications/multi_factor_authentications_test.exs
+++ /dev/null
@@ -1,2 +0,0 @@
-defmodule Pleroma.MultiFactorAuthenticationsTest do
-end
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Oct 30, 5:19 PM (14 h, 29 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1800354
Default Alt Text
(19 KB)
Attached To
Mode
rPUBE pleroma-upstream
Attached
Detach File
Event Timeline