# Two-step verification

Convoro supports a second factor at sign-in: a six-digit code from an
authenticator app, on top of the password.

The reasoning behind each decision is in the
[design note](../design/2026-08-05-two-factor.md). This page is how to run it.

---

## Before it will work: an application key

The TOTP secret is a password equivalent — anybody holding it can generate
valid codes forever — so it is encrypted at rest with the application key.

**On a fresh install `APP_KEY` is empty**, and encrypting with an empty key is
theatre. Rather than quietly providing less protection than it claims,
enrolment refuses and says so.

Generate one:

```bash
php tools/convoro key:generate
```

It writes `APP_KEY` to `.env`. Do this once, per site.

> **Keep it.** Losing the key means every enrolled account's secret becomes
> unreadable, and everybody who has it on falls back to recovery codes. Include
> `.env` in whatever you back up.

---

## For a member

**Settings → Security → Set it up.**

The page shows an `otpauth://` link — tapping it on a phone opens the
authenticator app directly — and the same secret written out in blocks of four
for typing in by hand. There is no QR code; the design note explains why.

Entering a code the app produces finishes enrolment. **Nothing changes about
signing in until that code is accepted**, so mistyping the secret into the app
cannot lock anybody out of their own account.

Ten recovery codes are shown once, immediately after. They are hashed the same
way passwords are, so there is no way to show them again — replacing the set is
the only option if they are lost.

### Signing in afterwards

The password goes in as usual, then a second page asks for the code. A recovery
code works there too, for anybody who cannot reach their phone. Until the code
is accepted **nothing treats the sign-in as having happened** — there is no
session, no user, no partial access.

A half-finished sign-in expires after five minutes on its own.

### Recovery codes

- Ten, each usable once.
- The account is told when it is down to three, because nobody counts their own.
- Replacing the set revokes every old one, and asks for the password first.

### Turning it off

Settings → Security. It asks for both the password and a current code —
turning protection off is exactly the moment worth asking who is doing the
asking. Recovery codes are deleted with it, so re-enrolling later starts clean
rather than reviving a set of ten passwords nobody remembers having.

---

## Requiring it of administrators

**Admin → Settings → Security → "Require two-step verification of
administrators."**

Anybody who can reach the admin panel must then have it on. An administrator
who has not enrolled is sent to their security settings and cannot open the
admin panel until they finish.

They are **not** locked out of the forum. Locking staff out of the site to make
them do a security task is how the setting gets turned off again.

The gate is skipped entirely when no application key is configured, because a
gate nobody can pass is a site with no administrators.

### If you turn it on and then cannot get in

Enrolling is the way through — `/settings/security` is not gated, so it is
reachable while the admin panel is not. If that is genuinely impossible, clear
the setting directly:

```sql
UPDATE cv_settings SET value = NULL WHERE `key` = 'require_admin_two_factor';
```

---

## What this does not do

- **No SMS.** SIM swapping makes it weaker than the password it protects.
- **No email codes.** The email account is usually what an attacker holding the
  password already has.
- **No passkeys or WebAuthn** yet. The right long-term answer and a much bigger
  feature; it deserves its own design note.
- **No trusted devices.** A cookie that skips the second factor needs its own
  thinking about theft and revocation.
- **No requiring it of ordinary members.** A forum that demands a second factor
  to read a thread loses the member.
