Skip to main content

max / makenotwork

totp: gate confirm on step monotonicity, unifying it with the login replay rule (ultra-fuzz NOTE tail) The confirm (enable) flow validated with check_current and recorded the matched step separately, diverging from the login path which validates via find_matching_step and rejects a code whose step <= the last accepted step. Confirm now uses the same matched-step + monotonicity gate, so both paths share one replay rule. setup clears totp_last_used_step (defaults to 0) so a genuine first code (step ~= now/30) always passes. totp suite (6) green; clippy clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-06-23 20:55 UTC
Commit: 6ef2042b19fce767bbcddb8b242d262837ddc872
Parent: 7af8fb8
1 file changed, +14 insertions, -9 deletions
@@ -85,21 +85,26 @@ pub(super) async fn confirm(
85 85
86 86 let totp = build_totp(&secret, &user.email)?;
87 87
88 - if !totp.check_current(&form.code).map_err(|e| {
89 - AppError::Internal(anyhow::anyhow!("TOTP check failed: {}", e))
90 - })? {
88 + // Validate via the matched time step (not `check_current`) and apply the same
89 + // step-monotonicity gate the login path uses, so confirm and login share one
90 + // replay rule: a code is accepted only if its step is newer than the last
91 + // accepted step, and that step is then recorded. `setup` cleared the step to
92 + // NULL (defaults to 0), so a genuine first code (step ~= now/30) always wins.
93 + let now = chrono::Utc::now().timestamp() as u64;
94 + let matched_step = find_matching_step(&totp, &form.code, now);
95 + let last_step = db::totp::get_totp_last_used_step(&state.db, user.id)
96 + .await?
97 + .unwrap_or(0);
98 + let Some(step) = matched_step.filter(|&step| step > last_step) else {
91 99 return Ok((
92 100 [("HX-Retarget", "#totp-confirm-status"), ("HX-Reswap", "innerHTML")],
93 101 Html("<span class=\"save-error\">Invalid code. Please try again.</span>"),
94 102 )
95 103 .into_response());
96 - }
104 + };
97 105
98 - // Record the matched step to prevent replay on the very first login
99 - let now = chrono::Utc::now().timestamp() as u64;
100 - if let Some(step) = find_matching_step(&totp, &form.code, now) {
101 - db::totp::set_totp_last_used_step(&state.db, user.id, step).await?;
102 - }
106 + // Record the matched step to prevent replay on the very first login.
107 + db::totp::set_totp_last_used_step(&state.db, user.id, step).await?;
103 108
104 109 db::totp::enable_totp(&state.db, user.id).await?;
105 110