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

16
.editorconfig Normal file
View file

@ -0,0 +1,16 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.{md, markdown, eex}]
trim_trailing_whitespace = false
[*.rs]
indent_size = 4
max_line_length = 100

4
.formatter.exs Normal file
View file

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

13
.gitignore vendored Normal file
View file

@ -0,0 +1,13 @@
/_build/
/cover/
/deps/
/doc/
/.fetch
erl_crash.dump
*.ez
argon2id_elixir-*.tar
/tmp/
/priv/
.DS_Store
/.elixir_ls/
*.xml

13
LICENSE-APACHE Normal file
View file

@ -0,0 +1,13 @@
Copyright 2024 Jussi Räsänen
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

7
LICENSE-MIT Normal file
View file

@ -0,0 +1,7 @@
Copyright 2024 Jussi Räsänen
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

131
README.md Normal file
View file

@ -0,0 +1,131 @@
# Argon2id for Elixir
Fast and secure Argon2 password hashing library for Elixir.
## Features
- Uses the pure Rust implementation of Argon2
- Only Argon2i implementation (version 0x13) at the moment
- Built-in security presets (OWASP, Strong, Test/Unsafe)
## Installation
Add `argon2id_elixir` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:argon2id_elixir, "~> 0.1.0"}
]
end
```
Ensure you have Rust installed, as it's required for compilation:
```bash
# On Windows
winget install Rust.Rust
# On Unix-like systems (https://rustup.rs/)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
## Usage
### Basic Password Hashing
```elixir
# Hash a password with default OWASP settings
hash = Argon2.hash_password("secure_password123")
# Verify a password
if Argon2.verify_password("secure_password123", hash) do
# Password matches
else
# Password is incorrect
end
```
### Configuration Presets
Three security presets are available:
```elixir
# OWASP (default) - Recommended for most use cases
hash = Argon2.hash_password("secure_password123")
# Strong - Higher security for sensitive applications
hash = Argon2.hash_password("secure_password123", "strong")
# Test - Fast but unsafe, only for testing purposes
hash = Argon2.hash_password("secure_password123", "test_unsafe")
```
Preset specifications:
- OWASP: m=19456 KiB, t=2, p=1
- Strong: m=65540 KiB, t=3, p=4
- Test: m=1024 KiB, t=1, p=1 (Useful for testing)
### Benchmarking
You can benchmark the different configurations on your hardware:
```bash
mix run -e "Argon2.Benchmark.run(10)"
```
```elixir
Configuration Benchmarks (averaged over 10 runs):
OWASP:
Hash time: 25ms
Verify time: 24ms
Memory: 19MB
STRONG:
Hash time: 145ms
Verify time: 139ms
Memory: 65MB
TEST_UNSAFE:
Hash time: 1ms
Verify time: 1ms
Memory: 1MB
```
## Development
```bash
# Install dependencies
mix deps.get
# Run tests
mix test
# Run benchmarks
mix run -e "Argon2.Benchmark.run()"
# Run code quality checks
mix quality
# Generate documentation
mix docs
# Format code
mix format
```
## License
Licensed under either of
- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.
## Credits
- [RustCrypto Argon2](https://github.com/RustCrypto/password-hashes/tree/master/argon2) - The Rust implementation
- [Rustler](https://github.com/rusterlium/rustler) - Elixir NIF interface

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

128
mix.exs Normal file
View file

@ -0,0 +1,128 @@
defmodule Argon2.MixProject do
use Mix.Project
@source_url "https://code.sipstea.org/jussi/argon2id_elixir"
def project do
[
app: :argon2id_elixir,
version: "1.0.0",
elixir: "~> 1.17",
start_permanent: Mix.env() == :prod,
deps: deps(),
aliases: aliases(),
rustler_crates: rustler_crates(),
# Hex
package: package(),
description: "Argon2 password hashing using Rust",
# Docs
name: "Argon2",
docs: docs(),
# Test coverage
test_coverage: [tool: ExCoveralls],
preferred_cli_env: [
coveralls: :test,
"coveralls.detail": :test,
"coveralls.post": :test,
"coveralls.html": :test,
"coveralls.json": :test
]
]
end
def application do
[
extra_applications: [:logger]
]
end
defp deps do
[
{:rustler, "~> 0.35.0", runtime: false},
{:ex_doc, "~> 0.34.2", only: :dev, runtime: false},
{:excoveralls, "~> 0.18.3", only: :test},
{:credo, "~> 1.7.10", only: [:dev, :test], runtime: false},
{:dialyxir, "~> 1.4.4", only: [:dev, :test], runtime: false}
]
end
defp aliases do
[
fmt: [
"format",
"cmd cargo fmt --manifest-path native/argon2/Cargo.toml"
],
quality: [
"format",
"cmd cargo fmt --manifest-path native/argon2/Cargo.toml",
"credo --strict",
"dialyzer"
]
]
end
defp rustler_crates do
[
argon2: [
path: "native/argon2",
mode: rustc_mode(),
features: [],
cargomode: cargomode()
]
]
end
defp rustc_mode do
case Mix.env() do
:prod -> :release
_ -> :debug
end
end
defp cargomode do
case Mix.env() do
:prod -> "build"
_ -> "check"
end
end
defp package do
[
name: "argon2id_elixir",
files: [
"lib",
"native",
"mix.exs",
"README.md",
"LICENSE-APACHE",
"LICENSE-MIT",
".formatter.exs"
],
licenses: ["MIT", "Apache-2.0"],
links: %{
"Source repository" => @source_url
},
maintainers: ["Jussi Räsänen"]
]
end
defp docs do
[
main: "Argon2",
source_url: @source_url,
extras: ["README.md"],
groups_for_modules: [
Core: [
Argon2,
Argon2.Native
],
Utilities: [
Argon2.Benchmark
]
]
]
end
end

25
mix.lock Normal file
View file

@ -0,0 +1,25 @@
%{
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"credo": {:hex, :credo, "1.7.10", "6e64fe59be8da5e30a1b96273b247b5cf1cc9e336b5fd66302a64b25749ad44d", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "71fbc9a6b8be21d993deca85bf151df023a3097b01e09a2809d460348561d8cd"},
"dialyxir": {:hex, :dialyxir, "1.4.4", "fb3ce8741edeaea59c9ae84d5cec75da00fa89fe401c72d6e047d11a61f65f70", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "cd6111e8017ccd563e65621a4d9a4a1c5cd333df30cebc7face8029cacb4eff6"},
"earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"},
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
"ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"},
"excoveralls": {:hex, :excoveralls, "0.18.3", "bca47a24d69a3179951f51f1db6d3ed63bca9017f476fe520eb78602d45f7756", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "746f404fcd09d5029f1b211739afb8fb8575d775b21f6a3908e7ce3e640724c6"},
"file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"},
"finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
"hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
"makeup_elixir": {:hex, :makeup_elixir, "1.0.0", "74bb8348c9b3a51d5c589bf5aebb0466a84b33274150e3b6ece1da45584afc82", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "49159b7d7d999e836bedaf09dcf35ca18b312230cf901b725a64f3f42e407983"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"},
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
"mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"req": {:hex, :req, "0.5.7", "b722680e03d531a2947282adff474362a48a02aa54b131196fbf7acaff5e4cee", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "c6035374615120a8923e8089d0c21a3496cf9eda2d287b806081b8f323ceee29"},
"rustler": {:hex, :rustler, "0.35.0", "1e2e379e1150fab9982454973c74ac9899bd0377b3882166ee04127ea613b2d9", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "a176bea1bb6711474f9dfad282066f2b7392e246459bf4e29dfff6d828779fdf"},
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
"toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"},
}

364
native/argon2/Cargo.lock generated Normal file
View file

@ -0,0 +1,364 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "argon2"
version = "0.1.0"
dependencies = [
"argon2 0.5.3",
"rand",
"rustler",
]
[[package]]
name = "argon2"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
dependencies = [
"base64ct",
"blake2",
"cpufeatures",
"password-hash",
]
[[package]]
name = "base64ct"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "blake2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cpufeatures"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "inventory"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767"
[[package]]
name = "libc"
version = "0.2.162"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
[[package]]
name = "libloading"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
dependencies = [
"cfg-if",
"windows-targets",
]
[[package]]
name = "password-hash"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
"rand_core",
"subtle",
]
[[package]]
name = "ppv-lite86"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "regex-lite"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
[[package]]
name = "rustler"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b705f2c3643cc170d8888cb6bad589155d9c0248f3104ef7a04c2b7ffbaf13fc"
dependencies = [
"inventory",
"libloading",
"regex-lite",
"rustler_codegen",
]
[[package]]
name = "rustler_codegen"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ad56caff00562948bd6ac33c18dbc579e5a1bbee2d7f2f54073307e57f6b57a"
dependencies = [
"heck",
"inventory",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "typenum"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"byteorder",
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

14
native/argon2/Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "argon2"
version = "0.1.0"
edition = "2021"
[lib]
name = "argon2"
path = "src/lib.rs"
crate-type = ["cdylib"]
[dependencies]
rustler = "0.35.0"
argon2 = "0.5.3"
rand = "0.8.5"

71
native/argon2/src/lib.rs Normal file
View file

@ -0,0 +1,71 @@
use argon2::Algorithm;
use argon2::Version;
use argon2::{
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2, Params,
};
use rand::rngs::OsRng;
use rustler::Error;
const MIN_PASSWORD_LENGTH: usize = 8;
#[derive(Debug)]
enum ConfigType {
Owasp,
Strong,
TestUnsafe,
}
impl ConfigType {
fn params(&self) -> Params {
match self {
ConfigType::Owasp => Params::new(19456, 2, 1, None).expect("Invalid OWASP config"),
ConfigType::Strong => Params::new(65540, 3, 4, None).expect("Invalid strong config"),
ConfigType::TestUnsafe => Params::new(1024, 1, 1, None).expect("Invalid test config"),
}
}
}
#[rustler::nif]
fn hash_password(password: String, config_type: Option<String>) -> Result<String, Error> {
if password.len() < MIN_PASSWORD_LENGTH {
return Err(Error::Term(Box::new(format!(
"Password must be at least {} characters long",
MIN_PASSWORD_LENGTH
))));
}
let salt = SaltString::generate(&mut OsRng);
let config_type = match config_type.as_deref() {
Some("strong") => ConfigType::Strong,
Some("test_unsafe") => ConfigType::TestUnsafe,
_ => ConfigType::Owasp,
};
let argon2 = Argon2::new(Algorithm::Argon2i, Version::V0x13, config_type.params());
argon2
.hash_password(password.as_bytes(), &salt)
.map(|hash| hash.to_string())
.map_err(|e| Error::Term(Box::new(format!("Hashing error: {}", e))))
}
#[rustler::nif]
fn verify_password(password: String, hash: String) -> Result<bool, Error> {
if password.len() < MIN_PASSWORD_LENGTH {
return Err(Error::Term(Box::new(format!(
"Password must be at least {} characters long",
MIN_PASSWORD_LENGTH
))));
}
let parsed_hash = PasswordHash::new(&hash)
.map_err(|e| Error::Term(Box::new(format!("Invalid hash format: {}", e))))?;
Ok(Argon2::default()
.verify_password(password.as_bytes(), &parsed_hash)
.is_ok())
}
rustler::init!("Elixir.Argon2.Native");

90
test/argon2_test.exs Normal file
View file

@ -0,0 +1,90 @@
defmodule Argon2Test do
use ExUnit.Case
doctest Argon2
describe "hash_password/1" do
test "hashes a password with default OWASP config" do
hash = Argon2.hash_password("password123")
assert is_binary(hash)
assert String.starts_with?(hash, "$argon2i$v=19$m=19456,t=2,p=1$")
end
test "hashes a password with strong config" do
config = "strong"
hash = Argon2.hash_password("password123", config)
assert is_binary(hash)
assert String.starts_with?(hash, "$argon2i$v=19$m=65540,t=3,p=4$")
end
test "hashes a password with test/development config" do
config = "test_unsafe"
hash = Argon2.hash_password("password123", config)
assert is_binary(hash)
assert String.starts_with?(hash, "$argon2i$v=19$m=1024,t=1,p=1")
end
test "generates different hashes for the same password" do
hash1 = Argon2.hash_password("same_password123")
hash2 = Argon2.hash_password("same_password123")
refute hash1 == hash2
end
test "rejects passwords shorter than 8 characters" do
assert_raise ArgumentError, "Password must be at least 8 characters long", fn ->
Argon2.hash_password("short")
end
end
test "accepts passwords exactly 8 characters" do
hash = Argon2.hash_password("abcdefgh")
assert is_binary(hash)
end
end
describe "verify_password/2" do
test "verifies a correct password" do
password = "correct_password123"
hash = Argon2.hash_password(password)
assert Argon2.verify_password(password, hash)
end
test "rejects an incorrect password" do
password = "correct_password123"
wrong_password = "wrong_password123"
hash = Argon2.hash_password(password)
refute Argon2.verify_password(wrong_password, hash)
end
test "handles invalid hash format" do
assert_raise ArgumentError, fn ->
Argon2.verify_password("password123", "invalid_hash_format")
end
end
test "rejects verification of short passwords" do
assert_raise ArgumentError, "Password must be at least 8 characters long", fn ->
Argon2.verify_password("short", "$argon2i$v=19$m=65536,t=2,p=4$c29tZXNhbHQ$hash")
end
end
end
describe "edge cases" do
test "handles unicode passwords of sufficient length" do
password = "パスワード123456"
hash = Argon2.hash_password(password)
assert Argon2.verify_password(password, hash)
end
test "handles emoji passwords" do
password = "🏳️‍⚧️🏳️‍⚧️🏳️‍⚧️🏳️‍⚧️🏳️‍⚧️🏳️‍⚧️"
hash = Argon2.hash_password(password)
assert Argon2.verify_password(password, hash)
end
test "handles very long passwords" do
long_password = String.duplicate("a", 1000)
hash = Argon2.hash_password(long_password)
assert Argon2.verify_password(long_password, hash)
end
end
end

1
test/test_helper.exs Normal file
View file

@ -0,0 +1 @@
ExUnit.start()