summaryrefslogtreecommitdiff
path: root/lib/pleroma/mfa/totp.ex
blob: f33e3a379a6254b99e0201ee2baa8252c0c2afbd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.MFA.TOTP do
  @moduledoc """
  This module represents functions to create secrets for
  TOTP Application as well as validate them with a time based token.
  """
  alias Pleroma.Config

  @config_ns [:instance, :multi_factor_authentication, :totp]

  @doc """
  https://github.com/google/google-authenticator/wiki/Key-Uri-Format
  """
  def provisioning_uri(secret, label, opts \\ []) do
    query =
      %{
        secret: secret,
        issuer: Keyword.get(opts, :issuer, default_issuer()),
        digits: Keyword.get(opts, :digits, default_digits()),
        period: Keyword.get(opts, :period, default_period())
      }
      |> Enum.filter(fn {_, v} -> not is_nil(v) end)
      |> Enum.into(%{})
      |> URI.encode_query()

    %URI{scheme: "otpauth", host: "totp", path: "/" <> label, query: query}
    |> URI.to_string()
  end

  defp default_period, do: Config.get(@config_ns ++ [:period])
  defp default_digits, do: Config.get(@config_ns ++ [:digits])

  defp default_issuer,
    do: Config.get(@config_ns ++ [:issuer], Config.get([:instance, :name]))

  @doc "Creates a random Base 32 encoded string"
  def generate_secret do
    Base.encode32(:crypto.strong_rand_bytes(10))
  end

  @doc "Generates a valid token based on a secret"
  def generate_token(secret) do
    :pot.totp(secret)
  end

  @doc """
  Validates a given token based on a secret.

  optional parameters:
  `token_length` default `6`
  `interval_length` default `30`
  `window` default 0

  Returns {:ok, :pass} if the token is valid and
  {:error, :invalid_token} if it is not.
  """
  @spec validate_token(String.t(), String.t()) ::
          {:ok, :pass} | {:error, :invalid_token | :invalid_secret_and_token}
  def validate_token(secret, token)
      when is_binary(secret) and is_binary(token) do
    opts = [
      token_length: default_digits(),
      interval_length: default_period()
    ]

    validate_token(secret, token, opts)
  end

  def validate_token(_, _), do: {:error, :invalid_secret_and_token}

  @doc "See `validate_token/2`"
  @spec validate_token(String.t(), String.t(), Keyword.t()) ::
          {:ok, :pass} | {:error, :invalid_token | :invalid_secret_and_token}
  def validate_token(secret, token, options)
      when is_binary(secret) and is_binary(token) do
    case :pot.valid_totp(token, secret, options) do
      true -> {:ok, :pass}
      false -> {:error, :invalid_token}
    end
  end

  def validate_token(_, _, _), do: {:error, :invalid_secret_and_token}
end