diff --git a/backend/src/auth/handlers.rs b/backend/src/auth/handlers.rs index b3679f5..2c3c378 100644 --- a/backend/src/auth/handlers.rs +++ b/backend/src/auth/handlers.rs @@ -175,20 +175,18 @@ pub async fn recover( } for user in &users { - // Check PIN lockout. If the lockout has expired, also reset the failed-attempt - // counter so the user gets a fresh 3-strike window — otherwise the counter - // stays at 3+ and every subsequent wrong PIN immediately re-locks them, even - // after waiting out the cooldown. Without this reset, a once-locked account - // is effectively permanently fragile. + // Check PIN lockout. The failed-attempt counter is deliberately NOT reset + // when the window expires — it drives the *escalating* backoff below, so a + // determined guesser faces an exponentially growing wait. A successful + // recovery (and a host PIN reset) is what clears it. The counter only ever + // grows on a *wrong* PIN, so a legitimate user is unaffected. if let Some(locked_until) = user.pin_locked_until { if Utc::now() < locked_until { return Err(AppError::TooManyRequests( - "Zu viele Versuche. Bitte warte 15 Minuten.".into(), + "Zu viele Versuche. Bitte warte und versuche es später erneut.".into(), None, )); } - // Lockout window expired — wipe the counter and the timestamp. - User::reset_pin_attempts(&state.pool, user.id).await?; } let pin_matches = @@ -227,13 +225,19 @@ pub async fn recover( "recover: wrong PIN" ); if attempts >= 3 { - let lockout = Utc::now() + chrono::Duration::minutes(15); + // Escalating backoff: 15min, 30, 60, … doubling per failure past the + // threshold, capped at 24h. Makes sustained guessing against the + // 6-digit space (1M combinations) astronomically slow. + let exp = (attempts as u32).saturating_sub(3).min(10); + let minutes = 15i64.saturating_mul(1i64 << exp).min(24 * 60); + let lockout = Utc::now() + chrono::Duration::minutes(minutes); User::lock_pin(&state.pool, user.id, lockout).await?; tracing::warn!( user_id = %user.id, event_id = %event.id, ip = %ip, - "recover: account locked for 15 minutes" + minutes, + "recover: account locked (escalating backoff)" ); } }