defmodule ExOtp do @moduledoc """ ExOtp allows you to create One-Time-Passwords, which can be either Time-based One-Time Passwords(TOTP) and Counter based OTPs(via HMAC-based One Time Passwords(HOTP)). The library is primarily based on the [PyOTP](https://github.com/pyauth/pyotp), and almost exactly mirrors their API as well. It follows the MFA standards defined in [RFC4226](https://datatracker.ietf.org/doc/html/rfc4226)(HOTP: an Hmac-Based One-Time Password Algorithm) and [RFC6238](https://datatracker.ietf.org/doc/html/rfc6238)(TOTP: Time-Based One-Time Password Algorithm). The library provides the following features: - Generate TOTP or Counter based HOTP - Generate QR codes for OTPs - Generate random secret values(if required) ## Usage ### TOTP - Create a TOTP object using either a random secret or a user-provided secret as follows: Example: secret = ExOtp.random_secret() #=> "uapgaiacdaptafbu" totp = ExOtp.create_totp(secret, 30) # Specify interval for which the OTP will be valid. #=> %ExOtp.Totp{ # base: %ExOtp.Base{ # digest: :sha, # digits: 6, # secret: "OVQXAZ3BNFQWGZDBOB2GCZTCOU======" # }, # interval: 30 #} - Generate an otp using the `totp` object, for a given datetime value: Example: otp = ExOtp.generate_totp(totp, DateTime.utc_now()) #=> "967372" - Finally, you can check if the otp is valid using the `otp` and `totp` objects: Example: ExOtp.valid_totp?(totp, otp, DateTime.utc_now()) #=> true ### HOTP - Create a HOTP object using either a random secret or a user-provided secret as follows: Example: secret = ExOtp.random_secret() #=> "uapgaiacdaptafbu" hotp = ExOtp.create_hotp(secret, 30) # Specify initial count #=> %ExOtp.Hotp{ # base: %ExOtp.Base{ # digest: :sha, # digits: 6, # secret: "OVQXAZ3BNFQWGZDBOB2GCZTCOU======" # }, # initial_count: 0 #} - Generate an otp using the `hotp` object, for a given datetime value: Example: otp = ExOtp.generate_hotp(hotp, DateTime.utc_now()) #=> "268374" - Finally, you can check if the otp is valid using the `otp` and `hotp` objects: Example: ExOtp.valid_hotp?(hotp, otp, 0) # Specify counter value #=> true ### QR Code(Optional) - Generate the provision URI for use with a QR code scanner built into MFA apps such as Google Authenticator. - Generate the QR code using the `EQRCode` libraby, which is an optional dependency. - Given a `totp` object and an `otp`, you can generate the QR code using: Example: totp |> ExOtp.provision_uri_totp(otp, "test:shubham@google.com",issuer_name: "test") |> ExOtp.generate_qr_code("code.svg") #=> 09:51:17.102 [info] QR code written to file: code.svg """ alias ExOtp.{Errors, Hotp, Totp} require Logger @spec random_secret(integer()) :: no_return() | String.t() def random_secret(length \\ 16) do if length < 16 do raise Errors.InvalidParam, "secret length should be atleast 16 characters" end for _ <- 1..length, into: "", do: <> end @spec create_totp(String.t(), integer()) :: Totp.t() def create_totp(secret, interval \\ 30) do Totp.new(interval, secret) |> Totp.validate() end @spec create_hotp(String.t(), integer()) :: Hotp.t() def create_hotp(secret, initial_count \\ 0) do Hotp.new(initial_count, secret) |> Hotp.validate() end @spec generate_totp(Totp.t(), DateTime.t(), integer) :: String.t() def generate_totp(%Totp{} = totp, for_time, counter \\ 0) do Totp.at(totp, for_time, counter) end @spec generate_hotp(Hotp.t(), integer) :: String.t() def generate_hotp(%Hotp{} = hotp, count) do Hotp.at(hotp, count) end @spec valid_totp?(Totp.t(), String.t(), DateTime.t(), integer) :: boolean def valid_totp?(%Totp{} = totp, otp, for_time \\ DateTime.utc_now(), valid_window \\ 0) do Totp.valid?(totp, otp, for_time, valid_window) end @spec valid_hotp?(ExOtp.Hotp.t(), String.t(), integer) :: boolean def valid_hotp?(%Hotp{} = hotp, otp, counter) do Hotp.valid?(hotp, otp, counter) end @spec provision_uri_totp(Totp.t(), String.t(), keyword) :: String.t() def provision_uri_totp(totp, label, opts \\ []) do Totp.provision_uri(totp, label, opts) end @spec provision_uri_hotp(Hotp.t(), String.t(), keyword) :: String.t() def provision_uri_hotp(hotp, label, opts \\ []) do Hotp.provision_uri(hotp, label, opts) end def generate_qr_code(input, filename \\ "code.svg") do unless Code.ensure_compiled(EQRCode) do raise Errors.MissingDependency, "Please install the optional depndency EQRCode to generate the QR code" end input |> EQRCode.encode() |> EQRCode.svg() |> (&File.write!(filename, &1)).() Logger.info("QR code written to file: #{filename}") end end