ellite/Wallos · error · InvalidArgumentException

Invalid data.

Error message

Invalid data.

What it means

OTP::generateOTP calls unpack('C*', $hash) to convert the HMAC bytes to a byte array and throws InvalidArgumentException if unpack returns false — which for a valid hash string essentially cannot happen, so this is a defensive internal invariant check. It fires during at()/code generation.

Solutions

  1. Verify the digest is one of sha1, sha256, sha512: $otp->setDigest('sha256').
  2. Ensure the secret is a valid non-empty base32 string so getDecodedSecret() yields bytes.
  3. Check the PHP hash extension is enabled (php -m | grep hash).
  4. If it persists, report as a library bug — unpack on a valid hash_hmac result should never fail.

Example fix

// before
$totp->setDigest('sha512-256'); // unsupported
// after
$totp->setDigest('sha512');
Defensive patterns

Strategy: validation

Validate before calling

if (!in_array($otp->getDigest(), ['sha1', 'sha256', 'sha512'], true)) {
    throw new DomainException('Unsupported digest for OTP generation');
}

Try / catch

try {
    $code = $totp->now();
} catch (InvalidArgumentException | RuntimeException $e) {
    log_critical('OTP generation failed', ['exception' => $e->getMessage()]);
    throw new OtpGenerationException($e);
}

Prevention

When it happens

Trigger: unpack() failing on the HMAC hash — only realistically possible if hash_hmac returned an empty/invalid string, e.g. an unknown digest algorithm name or an empty decoded secret slipping through.

Common situations: Setting an unsupported digest via setDigest (typo like 'sha257'), or a PHP environment where hash_hmac is disabled/behaving unexpectedly.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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

Appendix: source

Thrown at libs/OTPHP/OTP.php:65

     * @return non-empty-string
     */
    final protected static function generateSecret(): string
    {
        return Base32::encodeUpper(random_bytes(self::DEFAULT_SECRET_SIZE));
    }

    /**
     * The OTP at the specified input.
     *
     * @param 0|positive-int $input
     *
     * @return non-empty-string
     */
    protected function generateOTP(int $input): string
    {
        $hash = hash_hmac($this->getDigest(), $this->intToByteString($input), $this->getDecodedSecret(), true);
        $unpacked = unpack('C*', $hash);
        $unpacked !== false || throw new InvalidArgumentException('Invalid data.');
        $hmac = array_values($unpacked);

        $offset = ($hmac[count($hmac) - 1] & 0xF);
        $code = ($hmac[$offset] & 0x7F) << 24 | ($hmac[$offset + 1] & 0xFF) << 16 | ($hmac[$offset + 2] & 0xFF) << 8 | ($hmac[$offset + 3] & 0xFF);
        $otp = $code % (10 ** $this->getDigits());

        return str_pad((string) $otp, $this->getDigits(), '0', STR_PAD_LEFT);
    }

    /**
     * @param array<non-empty-string, mixed> $options
     */
    protected function filterOptions(array &$options): void
    {
        foreach ([
            'algorithm' => 'sha1',
            'period' => 30,
            'digits' => 6,

View on GitHub (pinned to 52820e87ca)