initial
This commit is contained in:
commit
abb3eab559
16 changed files with 1035 additions and 0 deletions
95
lib/argon2_elixir.ex
Normal file
95
lib/argon2_elixir.ex
Normal 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
|
13
lib/argon2_elixir/native.ex
Normal file
13
lib/argon2_elixir/native.ex
Normal 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
50
lib/benchmark.ex
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue