01b730c7617e17309a76783958574d9d08201e84
[anni] / lib / pleroma / mfa.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.MFA do
6   @moduledoc """
7   The MFA context.
8   """
9
10   alias Pleroma.User
11
12   alias Pleroma.MFA.BackupCodes
13   alias Pleroma.MFA.Changeset
14   alias Pleroma.MFA.Settings
15   alias Pleroma.MFA.TOTP
16
17   @doc """
18   Returns MFA methods the user has enabled.
19
20   ## Examples
21
22     iex> Pleroma.MFA.supported_method(User)
23     "totp, u2f"
24   """
25   @spec supported_methods(User.t()) :: String.t()
26   def supported_methods(user) do
27     settings = fetch_settings(user)
28
29     Settings.mfa_methods()
30     |> Enum.reduce([], fn m, acc ->
31       if method_enabled?(m, settings) do
32         acc ++ [m]
33       else
34         acc
35       end
36     end)
37     |> Enum.join(",")
38   end
39
40   @doc "Checks that user enabled MFA"
41   def require?(user) do
42     fetch_settings(user).enabled
43   end
44
45   @doc """
46   Display MFA settings of user
47   """
48   def mfa_settings(user) do
49     settings = fetch_settings(user)
50
51     Settings.mfa_methods()
52     |> Enum.map(fn m -> [m, method_enabled?(m, settings)] end)
53     |> Enum.into(%{enabled: settings.enabled}, fn [a, b] -> {a, b} end)
54   end
55
56   @doc false
57   def fetch_settings(%User{} = user) do
58     user.multi_factor_authentication_settings || %Settings{}
59   end
60
61   @doc "clears backup codes"
62   def invalidate_backup_code(%User{} = user, hash_code) do
63     %{backup_codes: codes} = fetch_settings(user)
64
65     user
66     |> Changeset.cast_backup_codes(codes -- [hash_code])
67     |> User.update_and_set_cache()
68   end
69
70   @doc "generates backup codes"
71   @spec generate_backup_codes(User.t()) :: {:ok, list(binary)} | {:error, String.t()}
72   def generate_backup_codes(%User{} = user) do
73     with codes <- BackupCodes.generate(),
74          hashed_codes <- Enum.map(codes, &Pleroma.Password.Pbkdf2.hash_pwd_salt/1),
75          changeset <- Changeset.cast_backup_codes(user, hashed_codes),
76          {:ok, _} <- User.update_and_set_cache(changeset) do
77       {:ok, codes}
78     else
79       {:error, msg} ->
80         %{error: msg}
81     end
82   end
83
84   @doc """
85   Generates secret key and set delivery_type to 'app' for TOTP method.
86   """
87   @spec setup_totp(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
88   def setup_totp(user) do
89     user
90     |> Changeset.setup_totp(%{secret: TOTP.generate_secret(), delivery_type: "app"})
91     |> User.update_and_set_cache()
92   end
93
94   @doc """
95   Confirms the TOTP method for user.
96
97   `attrs`:
98     `password` - current user password
99     `code` - TOTP token
100   """
101   @spec confirm_totp(User.t(), map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t() | atom()}
102   def confirm_totp(%User{} = user, attrs) do
103     with settings <- user.multi_factor_authentication_settings.totp,
104          {:ok, :pass} <- TOTP.validate_token(settings.secret, attrs["code"]) do
105       user
106       |> Changeset.confirm_totp()
107       |> User.update_and_set_cache()
108     end
109   end
110
111   @doc """
112   Disables the TOTP method for user.
113
114   `attrs`:
115     `password` - current user password
116   """
117   @spec disable_totp(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
118   def disable_totp(%User{} = user) do
119     user
120     |> Changeset.disable_totp()
121     |> Changeset.disable()
122     |> User.update_and_set_cache()
123   end
124
125   @doc """
126   Force disables all MFA methods for user.
127   """
128   @spec disable(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
129   def disable(%User{} = user) do
130     user
131     |> Changeset.disable_totp()
132     |> Changeset.disable(true)
133     |> User.update_and_set_cache()
134   end
135
136   @doc """
137   Checks if the user has MFA method enabled.
138   """
139   def method_enabled?(method, settings) do
140     with {:ok, %{confirmed: true} = _} <- Map.fetch(settings, method) do
141       true
142     else
143       _ -> false
144     end
145   end
146
147   @doc """
148   Checks if the user has enabled at least one MFA method.
149   """
150   def enabled?(settings) do
151     Settings.mfa_methods()
152     |> Enum.map(fn m -> method_enabled?(m, settings) end)
153     |> Enum.any?()
154   end
155 end