First
[anni] / lib / pleroma / mfa / totp.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.TOTP do
6   @moduledoc """
7   This module represents functions to create secrets for
8   TOTP Application as well as validate them with a time based token.
9   """
10   alias Pleroma.Config
11
12   @config_ns [:instance, :multi_factor_authentication, :totp]
13
14   @doc """
15   https://github.com/google/google-authenticator/wiki/Key-Uri-Format
16   """
17   def provisioning_uri(secret, label, opts \\ []) do
18     query =
19       %{
20         secret: secret,
21         issuer: Keyword.get(opts, :issuer, default_issuer()),
22         digits: Keyword.get(opts, :digits, default_digits()),
23         period: Keyword.get(opts, :period, default_period())
24       }
25       |> Enum.filter(fn {_, v} -> not is_nil(v) end)
26       |> Enum.into(%{})
27       |> URI.encode_query()
28
29     %URI{scheme: "otpauth", host: "totp", path: "/" <> label, query: query}
30     |> URI.to_string()
31   end
32
33   defp default_period, do: Config.get(@config_ns ++ [:period])
34   defp default_digits, do: Config.get(@config_ns ++ [:digits])
35
36   defp default_issuer,
37     do: Config.get(@config_ns ++ [:issuer], Config.get([:instance, :name]))
38
39   @doc "Creates a random Base 32 encoded string"
40   def generate_secret do
41     Base.encode32(:crypto.strong_rand_bytes(10))
42   end
43
44   @doc "Generates a valid token based on a secret"
45   def generate_token(secret) do
46     :pot.totp(secret)
47   end
48
49   @doc """
50   Validates a given token based on a secret.
51
52   optional parameters:
53   `token_length` default `6`
54   `interval_length` default `30`
55   `window` default 0
56
57   Returns {:ok, :pass} if the token is valid and
58   {:error, :invalid_token} if it is not.
59   """
60   @spec validate_token(String.t(), String.t()) ::
61           {:ok, :pass} | {:error, :invalid_token | :invalid_secret_and_token}
62   def validate_token(secret, token)
63       when is_binary(secret) and is_binary(token) do
64     opts = [
65       token_length: default_digits(),
66       interval_length: default_period()
67     ]
68
69     validate_token(secret, token, opts)
70   end
71
72   def validate_token(_, _), do: {:error, :invalid_secret_and_token}
73
74   @doc "See `validate_token/2`"
75   @spec validate_token(String.t(), String.t(), Keyword.t()) ::
76           {:ok, :pass} | {:error, :invalid_token | :invalid_secret_and_token}
77   def validate_token(secret, token, options)
78       when is_binary(secret) and is_binary(token) do
79     case :pot.valid_totp(token, secret, options) do
80       true -> {:ok, :pass}
81       false -> {:error, :invalid_token}
82     end
83   end
84
85   def validate_token(_, _, _), do: {:error, :invalid_secret_and_token}
86 end