Current section
Files
Jump to
Current section
Files
lib/pwned.ex
defmodule Pwned do
@moduledoc """
Check if your password has been pwned.
"""
@range_client Application.get_env(:pwned, :range_client)
@doc """
It uses have i been pwned? 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_response(response),
{:ok, answer} <- do_check(range, rest) do
{:ok, answer}
else
:error -> :error
end
end
defp hash(password) do
:crypto.hash(:sha, password)
|> Base.encode16()
|> String.split_at(5)
end
defp parse_response(response) do
parsed_response =
response
|> String.split("\r\n")
|> Enum.map(fn line -> String.split(line, ":") end)
{: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}
end