OpenNHP/opennhp · error

keystore: insert otp

Error message

keystore: insert otp: %w

What it means

GenerateOTP inserts a row into otp_records; an Exec failure is wrapped as 'keystore: insert otp'. Typical causes are SQLITE_BUSY under write contention, a UNIQUE/constraint violation, or database I/O errors.

Solutions

  1. Read the wrapped cause: SQLITE_BUSY means contention — serialize writes or move to a client-server DB at scale.
  2. Never place the SQLite file on NFS/network storage; SQLite locking is local-disk only.
  3. Check disk quota/space in the container or volume.
  4. Ensure only one process writes; SetMaxOpenConns(1) already serializes within the process.
Defensive patterns

Strategy: retry

Validate before calling

// keep the database on local writable disk
if fi, err := os.Stat(dbPath); err != nil || !fi.Mode().Perm().IsRegular() { /* abort */ }

Try / catch

if err != nil && strings.Contains(err.Error(), "insert otp") {
    if strings.Contains(err.Error(), "SQLITE_BUSY") {
        time.Sleep(100 * time.Millisecond) // bounded retry
    }
}

Prevention

When it happens

Trigger: High OTP issuance rate exceeding the single-writer connection's throughput (busy_timeout 5000ms exhausted), disk full, or a corrupted/truncated otp_records table.

Common situations: Load tests hammering OTP generation against one SQLite file, deployments with the keystore on a network filesystem that breaks SQLite locking, container disk quota exhausted.

Related errors


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

Appendix: source

Thrown at endpoints/server/keystore.go:243

	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)
	}

	log.Info("keystore: otp generated for user=%s device=%s", p.UserId, p.DeviceId)
	return code, nil
}

// MaxOTPAttempts is the number of consecutive incorrect OTP guesses allowed
// before the OTP is invalidated.
const MaxOTPAttempts = 5

// ValidateOTP checks the OTP for the given user+device. Returns nil on
// success, or a specific error:
//
//	ErrOTPInvalid     — no matching OTP found (or wrong code)
//	ErrOTPExpired     — OTP has expired
//	ErrOTPAlreadyUsed — OTP was already used
//	ErrOTPRateLimited — too many failed attempts; OTP has been invalidated
//	ErrOTPPublicKeyMismatch — OTP was issued for a different public key

View on GitHub (pinned to 6e04ca5ff0)