Packages

A simple application to check if an email or password has been pwned using the HaveIBeenPwned? API. It requires a purchased hibp-api-key in order to use the email checking functions.

Current section

Files

Jump to
pwned_coretheory lib pwned.ex
Raw

lib/pwned.ex

defmodule Pwned do
@moduledoc """
Check if your password has been pwned.
"""
alias Pwned.Utils.EmailFlattener
alias Pwned.Utils.EmailReducer
@doc """
It uses [have i been pwned?](https://haveibeenpwned.com) to verify if a password has appeared in a data breach. In order to protect the value of the source password being searched the value is not sended through the network.
## Examples
iex> Pwned.check_password("P@ssw0rd")
{:ok, 47205}
iex> Pwned.check_password("Z76okiy2X1m5PFud8iPUQGqusShCJhg")
{:ok, false}
"""
@spec check_password(String.t()) :: {:ok, integer} | {:ok, false} | :error
def check_password(password) do
with {head, rest} <- hash(password),
{:ok, response} <- range_client().get(head),
{:ok, range} <- parse_password_response(response),
{:ok, answer} <- do_check(range, rest) do
{:ok, answer}
else
:error -> :error
end
end
@doc """
This uses API v3 of the [have i been pwned?](https://haveibeenpwned.com) to check
if an email has appeared in a data breach. It returns the total count of appearances.
It also requires a purchased hibp-api-key, and implements a 12-factor methodology by
accessing the hibp-api-key from the system's environment variables.
## Examples
iex> Pwned.check_email("test123@example.com")
{:pwned_email, 4893554722}
iex Pwned.check_email("Z76okiy2X1m5PFud8iPUQGqusShCJhg@example.com")
{:safe_email, "email not pwned"}
"""
def check_email(email) do
with head <- email,
{:pwned_email, response} <- api_client().get(head),
{:ok, response} <- parse_email_response(response),
email_list <- EmailFlattener.flatten(response),
pwned_count <- EmailReducer.reduce_email_list(email_list) do
{:pwned_email, pwned_count}
else
{:safe_email, message} -> {:safe_email, message}
{:error, message} -> {:error, message}
_ -> :error
end
end
defp hash(password) do
:crypto.hash(:sha, password)
|> Base.encode16()
|> String.split_at(5)
end
defp parse_password_response(response) do
parsed_response =
response
|> String.split("\r\n")
|> Enum.map(fn line -> String.split(line, ":") end)
{:ok, parsed_response}
end
defp parse_email_response(response) do
parsed_response = Regex.scan(~r/"PwnCount":\d+/, response)
{:ok, parsed_response}
end
defp do_check(range, rest) do
case find_hash(range, rest) do
{:ok, false} -> {:ok, false}
{:ok, count} -> parse_count(count)
end
end
defp find_hash(range, hash) do
range
|> Enum.find(fn item -> List.first(item) == hash end)
|> handle_hash()
end
defp handle_hash(nil), do: {:ok, false}
defp handle_hash([_hash, count]), do: {:ok, count}
defp parse_count(count) do
Integer.parse(count)
|> handle_count()
end
defp handle_count(:error), do: :error
defp handle_count({count, _rest}), do: {:ok, count}
defp range_client, do: Application.get_env(:ct_pwned, :range_client, Pwned.Range.HTTPClient)
defp api_client, do: Application.get_env(:ct_pwned, :api_client, Pwned.Utils.APIClient)
end