OpenNHP/opennhp · error

keystore: generate otp

Error message

keystore: generate otp: %w

What it means

GenerateOTP calls randomDigits(6) to produce the one-time code; any failure from the randomness source is wrapped as 'keystore: generate otp'. This is rare and indicates the CSPRNG failed, not a user problem.

Solutions

  1. Check the wrapped cause; on Linux verify /dev/urandom is readable in the container.
  2. Review seccomp/apparmor profiles to allow getrandom(2).
  3. Add a retry — transient entropy starvation is rare but a single retry usually clears it.
  4. Escalate if persistent: the host's RNG setup is broken and OTP issuance must halt safely.
Defensive patterns

Strategy: retry

Try / catch

code, err := store.GenerateOTP(p)
if err != nil && strings.Contains(err.Error(), "generate otp") {
    // transient RNG failure: retry once, then fail closed
    code, err = store.GenerateOTP(p)
    if err != nil { return err }
}

Prevention

When it happens

Trigger: crypto/rand read failure — e.g. exhausted entropy, restricted /dev/urandom in a hardened container, or the randomDigits implementation failing to assemble digits.

Common situations: Containers with blocked device nodes, exotic sandboxes/seccomp profiles denying getrandom(2), or modified crypto sources in FIPS-restricted environments.

Understand the failure class

Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.

Related errors


AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07). Data as JSON: /api/errors/9b10dabd80a9d6f3. Report an issue: GitHub.

Appendix: source

Thrown at endpoints/server/keystore.go:226

		// Cap distinct deviceIds per user per cooldown window. This
		// bounds the disk-growth DoS vector: even if the attacker
		// distributes requests across deviceIds to stay under the
		// per-user OTP cap, they can only create so many distinct
		// rows before the sweep cleans them up.
		var distinctDevices int
		if err := s.db.QueryRow(
			`SELECT COUNT(DISTINCT dev_id) FROM otp_records
			 WHERE usr_id = ? AND created_at > ?`,
			p.UserId, cutoff,
		).Scan(&distinctDevices); err == nil && distinctDevices >= MaxDistinctDevicesPerUserPerWindow {
			return "", common.ErrOTPCooldown
		}
	}

	code, err := randomDigits(6)
	if err != nil {
		return "", fmt.Errorf("keystore: generate otp: %w", err)
	}

	now := time.Now().Unix()
	expires := time.Now().Add(p.TTL).Unix()

	// Invalidate previous unused OTPs for this user+device.
	_, _ = s.db.Exec(
		`UPDATE otp_records SET used = 1 WHERE usr_id = ? AND dev_id = ? AND used = 0`,
		p.UserId, p.DeviceId,
	)

	_, err = s.db.Exec(
		`INSERT INTO otp_records (usr_id, dev_id, otp_code, pub_key, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?)`,
		p.UserId, p.DeviceId, code, p.PublicKey, now, expires,
	)
	if err != nil {
		return "", fmt.Errorf("keystore: insert otp: %w", err)
	}

View on GitHub (pinned to 6e04ca5ff0)