# X_MFA_SECRET_INVALID

## Means

a TOTP secret that is not RFC 4648 base32, so it decodes to zero bytes

## Typical cause

`enrolTotp` is handed an imported secret outside the alphabet, or `totpCode` is called with one. `verifyTotp` does **not** throw it — an unreadable stored secret reads as a non-verdict there, the same rule `verifyAgainst` follows for a password hash Bun cannot read, so a login checking a broken row gets a credential refusal rather than a 500

## Fix

issue a fresh one and store it: `const { secret } = enrolTotp(auth, { account: user.email })`. An imported secret must be base32 (`A-Z` and `2-7`; padding, spaces and dashes are ignored) and must decode to at least one byte. Zero bytes is not a decode that failed closed: an HMAC keyed with zero bytes is a valid HMAC, so every unreadable secret in the table used to verify against one shared code stream that needed no secret to compute

[Auth and sessions](/error-codes#auth-and-sessions)
