Sigra.Crypto (Sigra v1.20.0)

Copy Markdown View Source

Password hashing, verification, and hash upgrade operations.

This module wraps the configured Sigra.Hasher implementation (default: Sigra.Hashers.Argon2) to provide a stable API for password operations. Application code should always use this module rather than calling hashing libraries directly.

Three-Way Verification

verify_with_upgrade/2,3 returns one of three results:

  • {:ok, :valid} -- password correct, hash is current
  • {:ok, :valid, new_hash} -- password correct, hash needs upgrade
  • {:error, :invalid} -- password incorrect

This enables transparent migration from bcrypt to Argon2id and automatic rehashing when Argon2 parameters are strengthened.

Enumeration Prevention

The no_user_verify/1 function runs a dummy hash operation when a user is not found, preventing timing-based user enumeration attacks.

Summary

Functions

Returns true if the hash string has an Argon2 prefix ($argon2).

Returns true if the hash string has a bcrypt prefix ($2b$ or $2a$).

Hashes a plaintext password using the configured hasher.

Checks whether an Argon2id hash needs rehashing due to parameter changes.

Runs a dummy hash to prevent timing-based user enumeration.

Verifies a plaintext password against a hashed password.

Verifies a password and detects whether the hash needs upgrading.

Functions

argon2_hash?(hash)

(since 0.2.0)
@spec argon2_hash?(String.t()) :: boolean()

Returns true if the hash string has an Argon2 prefix ($argon2).

bcrypt_hash?(hash)

(since 0.2.0)
@spec bcrypt_hash?(String.t()) :: boolean()

Returns true if the hash string has a bcrypt prefix ($2b$ or $2a$).

hash_password(password, opts \\ [])

(since 0.1.0)
@spec hash_password(
  String.t(),
  keyword()
) :: String.t()

Hashes a plaintext password using the configured hasher.

Returns the hashed password string (e.g., "$argon2id$...").

Options

Examples

iex> hashed = Sigra.Crypto.hash_password("supersecret123")
iex> String.starts_with?(hashed, "$argon2id$")
true

needs_rehash?(hashed_password, opts \\ [])

(since 0.2.0)
@spec needs_rehash?(
  String.t(),
  keyword()
) :: boolean()

Checks whether an Argon2id hash needs rehashing due to parameter changes.

Parses the hash string to extract m, t, and p parameters and compares them against the current configuration. Returns true if any parameter differs or if the hash cannot be parsed.

Non-Argon2 hashes always return true.

Options

  • :m_cost - Expected memory cost as power of 2. Default: from :argon2_elixir config or 16.
  • :t_cost - Expected time cost (iterations). Default: from :argon2_elixir config or 3.
  • :parallelism - Expected parallelism. Default: from :argon2_elixir config or 4.

Examples

iex> hashed = Sigra.Crypto.hash_password("test")
iex> Sigra.Crypto.needs_rehash?(hashed)
false

iex> Sigra.Crypto.needs_rehash?("$2b$12$...")
true

no_user_verify(opts \\ [])

(since 0.1.0)
@spec no_user_verify(keyword()) :: false

Runs a dummy hash to prevent timing-based user enumeration.

When a login attempt references a non-existent user, call this function to ensure the response time is similar to a real password verification. Always returns false.

Options

Examples

iex> Sigra.Crypto.no_user_verify()
false

verify_password(password, hashed_password, opts \\ [])

(since 0.1.0)
@spec verify_password(String.t(), String.t(), keyword()) :: boolean()

Verifies a plaintext password against a hashed password.

Returns true if the password matches, false otherwise. Uses constant-time comparison internally (provided by the hasher).

Options

Examples

iex> hashed = Sigra.Crypto.hash_password("supersecret123")
iex> Sigra.Crypto.verify_password("supersecret123", hashed)
true

iex> Sigra.Crypto.verify_password("wrong", hashed)
false

verify_with_upgrade(password, hashed_password, opts \\ [])

(since 0.2.0)
@spec verify_with_upgrade(String.t(), String.t() | nil, keyword()) ::
  {:ok, :valid} | {:ok, :valid, String.t()} | {:error, :invalid}

Verifies a password and detects whether the hash needs upgrading.

Returns one of three results:

  • {:ok, :valid} -- password matches, hash is current
  • {:ok, :valid, new_hash} -- password matches, caller should persist new_hash
  • {:error, :invalid} -- password does not match

Hash upgrade is triggered when:

  • The hash is bcrypt ($2b$ or $2a$ prefix) -- migrates to Argon2id
  • The hash is Argon2id with stale parameters -- rehashes with current params

When hashed_password is nil, runs timing protection and returns {:error, :invalid}.

Options

  • :hasher - Module implementing Sigra.Hasher. Default: Sigra.Hashers.Argon2
  • :m_cost - Argon2 memory cost parameter for rehash detection
  • :t_cost - Argon2 time cost parameter for rehash detection
  • :parallelism - Argon2 parallelism parameter for rehash detection

Examples

iex> hashed = Sigra.Crypto.hash_password("secret")
iex> Sigra.Crypto.verify_with_upgrade("secret", hashed)
{:ok, :valid}

iex> Sigra.Crypto.verify_with_upgrade("wrong", hashed)
{:error, :invalid}