Packages
ash_authentication
5.0.0-rc.5
5.0.0-rc.12
5.0.0-rc.11
5.0.0-rc.10
5.0.0-rc.9
5.0.0-rc.8
5.0.0-rc.7
5.0.0-rc.6
5.0.0-rc.5
5.0.0-rc.4
5.0.0-rc.3
5.0.0-rc.2
5.0.0-rc.1
5.0.0-rc.0
4.14.1
4.14.0
4.13.7
4.13.6
4.13.5
4.13.4
4.13.3
4.13.2
retired
4.13.1
retired
4.13.0
4.12.0
4.11.0
4.10.0
4.9.9
4.9.8
4.9.7
4.9.6
4.9.5
4.9.4
4.9.3
4.9.2
4.9.1
4.9.0
4.8.7
4.8.6
4.8.5
4.8.3
4.8.2
4.8.1
4.8.0
4.7.6
4.7.5
4.7.4
4.7.3
4.7.2
4.7.1
4.7.0
4.6.4
4.6.3
4.6.2
4.6.1
4.6.0
4.5.6
4.5.5
4.5.4
4.5.3
4.5.2
4.5.1
4.5.0
4.4.9
4.4.8
4.4.7
4.4.6
4.4.5
4.4.4
4.4.3
4.4.2
4.4.1
4.4.0
4.3.12
4.3.11
4.3.10
4.3.9
4.3.8
4.3.7
4.3.6
4.3.5
4.3.4
4.3.3
4.3.2
4.3.1
4.3.0
4.2.7
4.2.6
4.2.5
4.2.4
4.2.3
4.2.2
4.2.1
4.2.0
4.1.0
4.0.4
4.0.3
4.0.2
4.0.1
4.0.0
4.0.0-rc.6
4.0.0-rc.5
4.0.0-rc.3
4.0.0-rc.2
4.0.0-rc.1
4.0.0-rc.0
3.12.4
3.12.3
3.12.2
3.12.1
3.12.0
3.11.16
3.11.15
3.11.14
3.11.13
3.11.12
3.11.11
3.11.10
3.11.9
3.11.8
3.11.7
3.11.6
3.11.5
3.11.4
3.11.3
3.11.2
3.11.1
3.11.0
3.10.8
3.10.7
3.10.6
3.10.5
3.10.4
3.10.3
3.10.2
3.10.1
3.10.0
3.9.6
3.9.5
3.9.4
3.9.3
3.9.2
3.9.1
3.9.0
3.8.0
3.7.9
3.7.8
3.7.6
3.7.5
3.7.4
3.7.3
3.7.2
3.7.1
3.7.0
3.6.1
3.6.0
3.5.3
3.5.2
3.5.1
3.5.0
3.3.1
3.3.0
3.2.2
3.2.1
3.2.0
3.1.0
3.0.3
Authentication extension for the Ash Framework.
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
documentation/topics/recovery-code-security.md
<!--
SPDX-FileCopyrightText: 2026 Alembic Pty Ltd
SPDX-License-Identifier: MIT
-->
# Recovery Code Security
This document explains the security model behind the recovery code strategy,
including hashing trade-offs, entropy requirements, and verification mechanics.
## How Recovery Codes Work
Recovery codes are generated by the system, displayed once to the user, and
stored as hashed values. When a user authenticates with a recovery code:
1. The submitted code is hashed
2. The hash is matched against stored values
3. On match, the code record is deleted (single-use enforcement)
4. The user is authenticated
Plaintext codes are never stored. They exist only in memory during generation
and are returned to the caller via action metadata.
## Hash Provider Selection
The recovery code strategy supports pluggable hash providers. The choice of
provider involves a trade-off between code length and offline attack resistance.
### SHA-256 (Default)
SHA-256 is a fast, deterministic hash. Given the same input, it always produces
the same output. This enables:
- **Atomic verification** — hash the input once, then use a single database
query to find and delete the matching record. No race conditions.
- **Near-instant verification** — SHA-256 takes microseconds, regardless of how
many codes are stored.
The cost: because SHA-256 is fast, an attacker with database access can
brute-force short codes quickly. The defence is to use codes with sufficient
entropy (the strategy enforces a minimum of 60 bits).
### Bcrypt / Argon2
These are slow, salted hashes designed for passwords. Each hash uses a random
salt, so the same input produces different output each time. This means:
- **No atomic verification** — verification requires loading all stored hashes
and checking each one individually.
- **Slower verification** — up to ~100ms per hash comparison, so verifying
against 10 stored codes may take ~1 second.
- **Strong offline resistance** — even short codes take years to brute-force.
The benefit: codes can be shorter (e.g. 8 characters) while remaining secure
against offline attacks.
## Entropy and Code Length
The strategy calculates the entropy of the configured code format and validates
it against the hash provider's declared `minimum_entropy()`.
**Entropy formula:** `code_length * log2(alphabet_size)`
| Alphabet | Size | Code Length | Entropy (bits) | SHA-256 Safe? |
|----------|------|-------------|----------------|---------------|
| A-Z, 0-9 | 36 | 8 | ~41 | No |
| A-Z, 0-9 | 36 | 10 | ~52 | No |
| A-Z, 0-9 | 36 | 12 | ~62 | Yes |
| A-Z, 0-9 | 36 | 16 | ~83 | Yes |
| A-Z (no 0-9) | 26 | 12 | ~56 | No |
| A-Z, a-z, 0-9 | 62 | 10 | ~60 | Borderline |
The default configuration (36-character alphabet, 12-character codes) provides
~62 bits of entropy. At SHA-256 speeds (~10 billion hashes/sec on a modern GPU),
this would take approximately 15 years to brute-force.
Bcrypt and Argon2 declare `minimum_entropy()` of 0, meaning any code length is
accepted. Their computational cost (~100ms per attempt) provides the protection
instead.
## Atomic Verification
The strategy uses different verification approaches depending on the hash
provider's `deterministic?()` callback:
### Deterministic Hash (SHA-256)
```
1. Hash the submitted code: SHA256("AB3KMN7QR2XY") → "c95b6120..."
2. DELETE FROM recovery_codes WHERE user_id = ? AND code = "c95b6120..."
3. If a row was deleted → code was valid
4. If no rows deleted → code was invalid
```
This is a single atomic database operation. Two concurrent requests with the
same code cannot both succeed — only one `DELETE` will find the row.
### Non-Deterministic Hash (Bcrypt)
```
1. SELECT * FROM recovery_codes WHERE user_id = ? FOR UPDATE
2. For each stored hash: bcrypt_verify(submitted_code, stored_hash)
3. If match found: DELETE FROM recovery_codes WHERE id = ?
4. Pad remaining iterations with simulate() for constant time
```
The `FOR UPDATE` lock serialises concurrent requests at the database level,
preventing two requests from matching the same code. The iteration count is
padded to `recovery_code_count` to prevent timing-based information leakage
about how many codes remain.
## Brute Force Protection
Brute force protection is mandatory. Without it, an attacker who obtains a
session could attempt to guess recovery codes. The three options mirror those
available for the TOTP strategy:
- **Custom preparation** — implement your own rate limiting or lockout logic
- **Rate limiting** — use the `AshRateLimiter` extension to limit attempts
- **Audit log** — use an audit log add-on to track and limit failed attempts