{"record":{"id":"85094bc5274cf0e2","repo":"OpenNHP/opennhp","slug":"keystore-query-otp-w","errorCode":null,"errorMessage":"keystore: query otp: %w","messagePattern":"keystore: query otp: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"endpoints/server/keystore.go","lineNumber":305,"sourceCode":"\t\t}\n\t\tif time.Now().Unix() > expiresAt {\n\t\t\treturn common.ErrOTPExpired\n\t\t}\n\t\t// Verify the registering public key matches the one bound at OTP issuance.\n\t\tif storedPubKey != \"\" && pubKey != storedPubKey {\n\t\t\treturn common.ErrOTPPublicKeyMismatch\n\t\t}\n\t\t// Mark as used — reset attempts to 0 on success.\n\t\t_, err = s.db.Exec(`UPDATE otp_records SET used = 1, attempts = 0 WHERE id = ?`, id)\n\t\tif err != nil {\n\t\t\tlog.Error(\"keystore: mark otp used: %v\", err)\n\t\t}\n\t\tlog.Info(\"keystore: otp validated for user=%s device=%s\", userId, deviceId)\n\t\treturn nil\n\t}\n\n\tif err != sql.ErrNoRows {\n\t\treturn fmt.Errorf(\"keystore: query otp: %w\", err)\n\t}\n\n\t// Code did not match — track the failed attempt on the most recent\n\t// pending (unused, unexpired) OTP for this user+device.\n\terr = s.db.QueryRow(\n\t\t`SELECT id, expires_at, used, attempts FROM otp_records\n\t\t WHERE usr_id = ? AND dev_id = ? AND used = 0\n\t\t ORDER BY created_at DESC LIMIT 1`,\n\t\tuserId, deviceId,\n\t).Scan(&id, &expiresAt, &used, &attempts)\n\tif err == sql.ErrNoRows {\n\t\treturn common.ErrOTPInvalid\n\t}\n\tif err != nil {\n\t\treturn fmt.Errorf(\"keystore: query pending otp: %w\", err)\n\t}\n\n\tif time.Now().Unix() > expiresAt {","sourceCodeStart":287,"sourceCodeEnd":323,"githubUrl":"https://github.com/OpenNHP/opennhp/blob/6e04ca5ff03222a699c24205cd4bf8fee9af7ffe/endpoints/server/keystore.go#L287-L323","documentation":"ValidateOTP runs the lookup for a matching unused, unexpired OTP row; if QueryRow/Scan fails with anything other than sql.ErrNoRows it is wrapped as 'keystore: query otp'. This is an infrastructure error, distinct from invalid/expired codes which return sentinel errors.","triggerScenarios":"SQLite error during the SELECT (corrupt db, I/O error, BUSY beyond timeout) or a Scan type mismatch between the queried columns and the destination variables.","commonSituations":"Corrupted database after a host crash, the otp_records table altered manually/externally so Scan destinations no longer match, read-only mount on the db file.","solutions":["Log the full wrapped error chain to identify the SQLite error code.","Run PRAGMA integrity_check via sqlite3 CLI if corruption is suspected.","Restore schema consistency: do not alter otp_records outside migrate().","Verify the db file and its directory are writable (WAL requires write access even for reads)."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := store.ValidateOTP(u, d, code); err != nil {\n    switch {\n    case errors.Is(err, common.ErrOTPInvalid), errors.Is(err, common.ErrOTPExpired), errors.Is(err, common.ErrOTPCooldown):\n        // user-facing sentinels — safe to expose\n    default:\n        log.Error(\"otp db failure: %v\", err) // 'query otp' wrapper: infrastructure\n        return http.StatusServiceUnavailable\n    }\n}","preventionTips":["Distinguish sentinel errors (errors.Is) from DB failures","Never hand-edit otp_records schema","Run integrity checks after host crashes"],"tags":["go","sqlite","query","otp"],"backgroundTag":"database-query-failed","analyzedSha":"6e04ca5ff03222a699c24205cd4bf8fee9af7ffe","analyzedAt":"2026-09-07T15:44:59.941Z","contentChangedAt":"2026-09-07T15:44:59.941Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}