= TOTP_DAILY_FAILURES; } function totp_note_failure(int $userId): void { $k = totp_fail_key($userId); q('INSERT INTO {rate_limits} (k, hits, window_start) VALUES (?, 1, ?) ON DUPLICATE KEY UPDATE hits = hits + 1', [$k, time() + 86400]); if ((int)qv('SELECT hits FROM {rate_limits} WHERE k = ?', [$k]) === TOTP_DAILY_FAILURES) { audit('2fa.locked', 'user', $userId); gatecraft_notify_security_change($userId, 'Many wrong authenticator codes were entered for your account today, so codes are not accepted until tomorrow. If this was not you, change your password'); } } /** Verify a user's code against their stored secret and record the step to stop replays. */ function totp_verify_user(int $userId, string $code): bool { if (totp_locked($userId)) return false; $u = q1('SELECT totp_secret_enc, totp_last_step FROM {accounts} WHERE user_id = ?', [$userId]); $secret = decrypt_secret($u['totp_secret_enc'] ?? null); if (!$secret) { // The secret is there but cannot be read: the encryption key changed. Say so in the log, as no code can ever match. if (!empty($u['totp_secret_enc'])) log_debug('the authenticator secret of user ' . $userId . ' cannot be decrypted. The app key changed; reset this person\'s authenticator.'); return false; } $step = totp_match($secret, $code, $u['totp_last_step'] !== null ? (int)$u['totp_last_step'] : null); if ($step === null) { totp_note_failure($userId); return false; } $upd = q('UPDATE {accounts} SET totp_last_step = ? WHERE user_id = ? AND (totp_last_step IS NULL OR totp_last_step < ?)', [$step, $userId, $step]); return $upd->rowCount() === 1; }