# `Sigra.Crypto`
[🔗](https://github.com/sztheory/sigra/blob/v1.20.0/lib/sigra/crypto.ex#L1)

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.

# `argon2_hash?`
*since 0.2.0* 

```elixir
@spec argon2_hash?(String.t()) :: boolean()
```

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

# `bcrypt_hash?`
*since 0.2.0* 

```elixir
@spec bcrypt_hash?(String.t()) :: boolean()
```

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

# `hash_password`
*since 0.1.0* 

```elixir
@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

- `:hasher` - Module implementing `Sigra.Hasher`. Default: `Sigra.Hashers.Argon2`

## Examples

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

# `needs_rehash?`
*since 0.2.0* 

```elixir
@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`
*since 0.1.0* 

```elixir
@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

- `:hasher` - Module implementing `Sigra.Hasher`. Default: `Sigra.Hashers.Argon2`

## Examples

    iex> Sigra.Crypto.no_user_verify()
    false

# `verify_password`
*since 0.1.0* 

```elixir
@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

- `:hasher` - Module implementing `Sigra.Hasher`. Default: `Sigra.Hashers.Argon2`

## 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`
*since 0.2.0* 

```elixir
@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}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
