Skip to main content

max / makenotwork

security: regression test for TOTP-decrypt fallthrough to backup codes Locks in the M-Sec1 fix: an undecryptable stored TOTP secret (simulated legacy plaintext row) must fall through to the backup-code branch rather than 500 the verify handler. Closes the negative-test gap the audit flagged on two_factor.rs. ultra-fuzz Run 5 M-Sec1 (deep). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-06-24 02:15 UTC
Commit: b84e6e9dde9687f28742db22ee8ec5ba7ef20cef
Parent: 5a31ce3
1 file changed, +34 insertions, -0 deletions
@@ -181,6 +181,40 @@ async fn totp_backup_code_login() {
181 181 }
182 182
183 183 #[tokio::test]
184 + async fn totp_decrypt_failure_falls_through_to_backup_code() {
185 + // M-Sec1 (deep): a stored TOTP secret that fails to decrypt — e.g. a legacy
186 + // pre-encryption plaintext row lacking the `enc:v1:` prefix — must not 500
187 + // the verify handler. A 500 there would also skip the backup-code branch and
188 + // lock the user out. The handler logs the decrypt failure and falls through.
189 + let mut h = TestHarness::new().await;
190 + let user_id = h.signup("totpdec", "totpdec@test.com", "Password1!").await;
191 + let (_, codes) = setup_and_enable_totp(&mut h, "totpdec@test.com").await;
192 + h.client.post_form("/logout", "").await;
193 +
194 + // Corrupt the stored secret to a bare base32 value (no `enc:v1:` prefix), so
195 + // `decrypt_totp_secret` returns Err on the next verify.
196 + sqlx::query("UPDATE users SET totp_secret = $1 WHERE id = $2")
197 + .bind("JBSWY3DPEHPK3PXP")
198 + .bind(user_id)
199 + .execute(&h.db)
200 + .await
201 + .expect("corrupt totp secret");
202 +
203 + // A backup-code login still succeeds: the verify handler tries TOTP first
204 + // (decrypt fails -> logged -> falls through), then consumes the backup code.
205 + // If the decrypt error propagated, this single verify POST would 500 before
206 + // reaching the backup branch and `login_with_2fa`'s success assert would fail.
207 + login_with_2fa(&mut h, "totpdec", "Password1!", &codes[0]).await;
208 +
209 + let resp = h.client.get("/dashboard").await;
210 + assert_eq!(
211 + resp.status.as_u16(),
212 + 200,
213 + "backup-code login must succeed despite an undecryptable TOTP secret"
214 + );
215 + }
216 +
217 + #[tokio::test]
184 218 async fn totp_backup_code_single_use() {
185 219 let mut h = TestHarness::new().await;
186 220 h.signup("totp4", "totp4@test.com", "Password1!").await;