{"record":{"id":"9b10dabd80a9d6f3","repo":"OpenNHP/opennhp","slug":"keystore-generate-otp-w","errorCode":null,"errorMessage":"keystore: generate otp: %w","messagePattern":"keystore: generate otp: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"endpoints/server/keystore.go","lineNumber":226,"sourceCode":"\n\t\t// Cap distinct deviceIds per user per cooldown window. This\n\t\t// bounds the disk-growth DoS vector: even if the attacker\n\t\t// distributes requests across deviceIds to stay under the\n\t\t// per-user OTP cap, they can only create so many distinct\n\t\t// rows before the sweep cleans them up.\n\t\tvar distinctDevices int\n\t\tif err := s.db.QueryRow(\n\t\t\t`SELECT COUNT(DISTINCT dev_id) FROM otp_records\n\t\t\t WHERE usr_id = ? AND created_at > ?`,\n\t\t\tp.UserId, cutoff,\n\t\t).Scan(&distinctDevices); err == nil && distinctDevices >= MaxDistinctDevicesPerUserPerWindow {\n\t\t\treturn \"\", common.ErrOTPCooldown\n\t\t}\n\t}\n\n\tcode, err := randomDigits(6)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"keystore: generate otp: %w\", err)\n\t}\n\n\tnow := time.Now().Unix()\n\texpires := time.Now().Add(p.TTL).Unix()\n\n\t// Invalidate previous unused OTPs for this user+device.\n\t_, _ = s.db.Exec(\n\t\t`UPDATE otp_records SET used = 1 WHERE usr_id = ? AND dev_id = ? AND used = 0`,\n\t\tp.UserId, p.DeviceId,\n\t)\n\n\t_, err = s.db.Exec(\n\t\t`INSERT INTO otp_records (usr_id, dev_id, otp_code, pub_key, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?)`,\n\t\tp.UserId, p.DeviceId, code, p.PublicKey, now, expires,\n\t)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"keystore: insert otp: %w\", err)\n\t}","sourceCodeStart":208,"sourceCodeEnd":244,"githubUrl":"https://github.com/OpenNHP/opennhp/blob/6e04ca5ff03222a699c24205cd4bf8fee9af7ffe/endpoints/server/keystore.go#L208-L244","documentation":"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.","triggerScenarios":"crypto/rand read failure — e.g. exhausted entropy, restricted /dev/urandom in a hardened container, or the randomDigits implementation failing to assemble digits.","commonSituations":"Containers with blocked device nodes, exotic sandboxes/seccomp profiles denying getrandom(2), or modified crypto sources in FIPS-restricted environments.","solutions":["Check the wrapped cause; on Linux verify /dev/urandom is readable in the container.","Review seccomp/apparmor profiles to allow getrandom(2).","Add a retry — transient entropy starvation is rare but a single retry usually clears it.","Escalate if persistent: the host's RNG setup is broken and OTP issuance must halt safely."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"code, err := store.GenerateOTP(p)\nif err != nil && strings.Contains(err.Error(), \"generate otp\") {\n    // transient RNG failure: retry once, then fail closed\n    code, err = store.GenerateOTP(p)\n    if err != nil { return err }\n}","preventionTips":["Verify /dev/urandom or getrandom(2) availability in containers","Review seccomp profiles for crypto syscalls","Fail closed: never issue OTPs with a weak fallback RNG"],"tags":["go","crypto","random","otp"],"backgroundTag":"unexpected-api-response-shape","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"}