max / makenotwork
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; |