# `Sigra.MFA.BackupCodes`
[🔗](https://github.com/sztheory/sigra/blob/v1.20.0/lib/sigra/mfa/backup_codes.ex#L1)

Backup code generation, hashing, and atomic consumption.

Backup codes are single-use recovery codes displayed to the user during
MFA enrollment. They are stored as SHA-256 hashes in the database and
consumed atomically via `UPDATE ... WHERE used_at IS NULL`.

## Format

Codes use XXXX-XXXX format (8 numeric digits, dash-separated).
Verification strips dashes and spaces before hashing (D-12).

## Security Properties

- Generated with `:crypto.strong_rand_bytes/1` (cryptographically secure)
- Stored as SHA-256 hex hashes -- never retrievable after initial display (D-13, D-20)
- Consumed atomically to prevent race conditions (D-16)
- Hash-to-hash comparison in DB sidesteps timing attacks (Pitfall 5)

# `append_replace_steps`
*since 0.6.0* 

```elixir
@spec append_replace_steps(
  Ecto.Multi.t(),
  module(),
  term(),
  pos_integer(),
  DateTime.t()
) ::
  {Ecto.Multi.t(), [String.t()]}
```

Appends backup-code replacement steps (`delete_all` + `insert_all`) to an
`Ecto.Multi`.

Returns `{multi, formatted_codes}` so callers can wrap the operation in
`repo.transaction/1` alongside other steps (for example audit rows).

`now` defaults to `DateTime.utc_now/0` and is used for each row's
`inserted_at`, matching `confirm_enrollment/5` bulk inserts.

# `consume`
*since 0.6.0* 

```elixir
@spec consume(module(), module(), term(), String.t()) ::
  {:ok, :consumed} | {:error, :invalid_backup_code}
```

Atomically consumes a backup code for the given user.

Normalizes the submitted code, hashes it, and performs an atomic
`UPDATE ... SET used_at = NOW() WHERE hashed_code = ? AND used_at IS NULL`.

Returns `{:ok, :consumed}` if a matching unused code was found and consumed,
or `{:error, :invalid_backup_code}` if no match.

## Parameters

- `repo` - The Ecto repo module
- `backup_code_schema` - The generated backup code Ecto schema module
- `user_id` - The user's ID
- `submitted_code` - The raw code submitted by the user

# `generate`
*since 0.6.0* 

```elixir
@spec generate(pos_integer()) :: [{String.t(), String.t()}]
```

Generates a list of `{formatted_code, sha256_hex_hash}` tuples.

Each code is an 8-digit number formatted as "XXXX-XXXX".
The hash is the SHA-256 hex digest of the normalized (digits-only) code.

## Examples

    iex> codes = Sigra.MFA.BackupCodes.generate(8)
    iex> length(codes)
    8

# `hash`
*since 0.6.0* 

```elixir
@spec hash(String.t()) :: String.t()
```

Normalizes a submitted code (strips dashes/spaces) and returns its SHA-256 hex hash.

## Examples

    iex> Sigra.MFA.BackupCodes.hash("1234-5678")
    Sigra.MFA.BackupCodes.hash("12345678")

# `regenerate`
*since 0.6.0* 

```elixir
@spec regenerate(module(), module(), term(), pos_integer()) :: {:ok, [String.t()]}
```

Regenerates backup codes for a user.

Deletes all existing codes and inserts fresh ones inside a single
`Repo.transaction/1`. Returns the raw formatted codes for display (shown
once, never retrievable again).

## Parameters

- `repo` - The Ecto repo module
- `backup_code_schema` - The generated backup code Ecto schema module
- `user_id` - The user's ID
- `count` - Number of codes to generate

# `remaining_count`
*since 0.6.0* 

```elixir
@spec remaining_count(module(), module(), term()) :: non_neg_integer()
```

Returns the count of unused backup codes for a user.

---

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