ellite/Wallos · error · InvalidArgumentException

Counter must be at least 0.

Error message

Counter must be at least 0.

What it means

HOTP::getParameterMap registers a normalizer for the 'counter' parameter that casts to int and throws InvalidArgumentException if the result is negative. This runs whenever a parameter is set or a provisioning URI is loaded, enforcing counter >= 0 at write time.

Solutions

  1. Fix the source value: ensure counter is >= 0 before setParameter() or in the provisioning URI.
  2. Clamp on import: max(0, (int) $value) when hydrating counters from external data.
  3. Sanitize URIs before loadFromProvisioningUri(): regex-check counter=\d+.
  4. Reject at your own input-validation layer with a clear user-facing message.

Example fix

// before
$hotp->setParameter('counter', $userInput); // could be -3
// after
$hotp->setParameter('counter', max(0, (int) $userInput));
Defensive patterns

Strategy: validation

Validate before calling

if ((int) $value < 0) {
    throw new DomainException('Counter must be >= 0');
}
$hotp->setParameter('counter', (int) $value);

Type guard

function sanitizeCounter(mixed $value): int {
    return max(0, (int) $value);
}

Try / catch

try {
    $otp = $factory->loadFromProvisioningUri($uri);
} catch (InvalidArgumentException $e) {
    log_error('Malformed counter in provisioning URI', ['uri' => $uri]);
    throw new InvalidProvisioningUriException($uri, $e);
}

Prevention

When it happens

Trigger: Setting the counter parameter with a negative value ($hotp->setParameter('counter', -5)) or loading a provisioning URI containing counter=-1, so the map callback throws during parameter hydration.

Common situations: Parsing malicious/malformed otpauth://hotp URIs; importing OTP state from external systems where counters were stored as signed or corrupted values.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of ellite/Wallos@52820e87ca (2026-09-13). Data as JSON: /api/errors/beff630aa985f531. Report an issue: GitHub.

Appendix: source

Thrown at libs/OTPHP/HOTP.php:98

        }

        return $this->verifyOtpWithWindow($otp, $counter, $window);
    }

    public function setCounter(int $counter): void
    {
        $this->setParameter('counter', $counter);
    }

    /**
     * @return array<non-empty-string, callable>
     */
    protected function getParameterMap(): array
    {
        return [...parent::getParameterMap(), ...[
            'counter' => static function (mixed $value): int {
                $value = (int) $value;
                $value >= 0 || throw new InvalidArgumentException('Counter must be at least 0.');

                return $value;
            },
        ]];
    }

    private function updateCounter(int $counter): void
    {
        $this->setCounter($counter);
    }

    /**
     * @param null|0|positive-int $window
     */
    private function getWindow(null|int $window): int
    {
        return abs($window ?? self::DEFAULT_WINDOW);
    }

View on GitHub (pinned to 52820e87ca)