This commit is contained in:
Jussi 2024-11-09 14:06:34 +02:00
commit abb3eab559
16 changed files with 1035 additions and 0 deletions

95
lib/argon2_elixir.ex Normal file
View file

@ -0,0 +1,95 @@
defmodule Argon2 do
@moduledoc """
Argon2 password hashing for Elixir using Rust NIFs.
This module provides a secure way to hash passwords using the Argon2i algorithm
with configuration presets following security best practices.
## Security Presets
* `:owasp` (default) - OWASP recommended settings (m=19456, t=2, p=1)
* `:strong` - Higher security settings (m=65540, t=3, p=4)
* `:test_unsafe` - Fast settings for testing only (m=1024, t=1, p=1)
## Examples
# Hash with default OWASP settings
iex> hash = Argon2.hash_password("secure_password123")
iex> String.starts_with?(hash, "$argon2i$v=19$m=19456,t=2,p=1$")
true
# Hash with strong settings
iex> hash = Argon2.hash_password("secure_password123", "strong")
iex> String.starts_with?(hash, "$argon2i$v=19$m=65540,t=3,p=4$")
true
# Verify password
iex> hash = Argon2.hash_password("secure_password123")
iex> Argon2.verify_password("secure_password123", hash)
true
iex> Argon2.verify_password("wrong_password", hash)
false
## Security Notes
* Passwords must be at least 8 characters long
* Each hash uses a unique random salt
* The `:test_unsafe` preset should never be used in production
"""
@type password :: String.t()
@type hash :: String.t()
@type config :: String.t()
@doc """
Hashes a password using Argon2i.
## Options
* `config` - One of `"owasp"` (default), `"strong"`, or `"test_unsafe"`
## Examples
iex> hash = Argon2.hash_password("secure_password123")
iex> is_binary(hash)
true
## Security Notes
* Passwords must be at least 8 characters
* A unique random salt is used for each hash
* The default OWASP preset is recommended for most use cases
Raises `ArgumentError` if the password is less than 8 characters long.
"""
@spec hash_password(password :: password, config :: config | nil) :: hash
def hash_password(password, config \\ nil) do
case Argon2.Native.hash_password(password, config) do
{:error, message} -> raise ArgumentError, message
result -> result
end
end
@doc """
Verifies a password against a hash.
Takes constant time regardless of whether the password matches or not.
## Examples
iex> hash = Argon2.hash_password("secure_password123")
iex> Argon2.verify_password("secure_password123", hash)
true
Raises `ArgumentError` if:
* The password is less than 8 characters long
* The hash format is invalid
"""
@spec verify_password(password :: password, hash :: hash) :: boolean
def verify_password(password, hash) do
case Argon2.Native.verify_password(password, hash) do
{:error, message} -> raise ArgumentError, message
result -> result
end
end
end

View file

@ -0,0 +1,13 @@
defmodule Argon2.Native do
@moduledoc """
Native implementation of the Argon2 password hashing algorithm.
"""
use Rustler,
otp_app: :argon2id_elixir,
crate: "argon2"
def hash_password(_password, _config \\ nil), do: error()
def verify_password(_password, _hash), do: error()
defp error, do: :erlang.nif_error(:nif_not_loaded)
end

50
lib/benchmark.ex Normal file
View file

@ -0,0 +1,50 @@
defmodule Argon2.Benchmark do
@moduledoc """
Benchmarking utilities for Argon2 password hashing.
"""
def run(rounds \\ 5) do
configs = [nil, "strong", "test_unsafe"]
password = "benchmark_password123"
IO.puts("Configuration Benchmarks (averaged over #{rounds} runs):\n")
for config <- configs do
{hash_times, verify_times} = measure_times(password, config, rounds)
print_results(config || "owasp", hash_times, verify_times)
end
end
defp measure_times(password, config, rounds) do
hash_times =
for _ <- 1..rounds do
{time, hash} = :timer.tc(fn -> Argon2.hash_password(password, config) end)
{verify_time, _} = :timer.tc(fn -> Argon2.verify_password(password, hash) end)
# Convert to milliseconds
{time / 1000, verify_time / 1000}
end
{hash_avg, verify_avg} =
Enum.reduce(hash_times, {0, 0}, fn {h, v}, {ha, va} ->
{ha + h / rounds, va + v / rounds}
end)
{hash_avg, verify_avg}
end
defp print_results(config, hash_avg, verify_avg) do
memory =
case config do
"owasp" -> 19
"strong" -> 65
"test_unsafe" -> 1
end
IO.puts("""
#{String.upcase(config)}:
Hash time: #{round(hash_avg)}ms
Verify time: #{round(verify_avg)}ms
Memory: #{memory}MB
""")
end
end