/** * RFC 4226 HOTP — HMAC-based one-time password. * * @param {string | Buffer | Uint8Array} secret * @param {number} counter * @param {HotpOptions} [options] * @returns {string} Zero-padded N-digit code. */ declare function hotp(secret: string | Buffer | Uint8Array, counter: number, options?: HotpOptions): string; /** * Verify a counter-based OTP, returning the *matched counter* on * success (so the caller can advance their stored value) or `null` * when nothing in the drift window matched. * * The compare is timing-safe. Every candidate counter in the window * is checked even after a match — the constant-time property does * not extend across the loop, but the input is derived from a * pre-computed hash, not the user's guess, so this is safe. * * @param {unknown} code User-supplied candidate (string of digits). * @param {string | Buffer | Uint8Array} secret * @param {number} counter Current stored counter. * @param {HotpVerifyOptions} [options] * @returns {number | null} Matched counter (advance to `matched + 1`) * or `null` on no match. */ declare function verifyHotp(code: unknown, secret: string | Buffer | Uint8Array, counter: number, options?: HotpVerifyOptions): number | null; /** * Shared forward-scan core for {@link verifyHotp} and {@link verifyTotp}. * * Validates the code shape, then timing-safely scans the counters * `[start, start + span]`. Deliberately has **no `span` upper bound** — * `verifyHotp` applies its own `[0, 10]` window guard before calling in, * while `verifyTotp` legitimately needs a span of up to `2 × window` * (its symmetric skew window mapped onto a forward-only HOTP scan). The * two concepts were previously coupled, which made any TOTP `window > 5` * throw `HOTP window must be an integer in [0, 10]`. * * @private * @unstable This export exists so `verifyTotp` can share the timing-safe * forward scan without re-implementing it. It is **not** part of the * public API — the underscore prefix, the `@private` tag, and this * note are all signals that the signature can move to `internal/`, * change shape, or disappear entirely in any future release with no * major-version bump. Do not import it from `@exortek/otp/hotp`. * * @param {string} code * @param {string | Buffer | Uint8Array} secret * @param {number} start First counter to try (non-negative). * @param {number} span How many counters past `start` to also try. * @param {6 | 7 | 8 | 9 | 10} digits * @param {OtpAlgorithm} algorithm * @returns {number | null} Matched counter, or `null`. */ declare function _verifyHotpForward(code: string, secret: string | Buffer | Uint8Array, start: number, span: number, digits: 6 | 7 | 8 | 9 | 10, algorithm: OtpAlgorithm): number | null; /** * @typedef {object} ResyncOptions * @property {number} [startCounter=0] * Where to start scanning. Almost always the last known-good counter * from your database. * @property {number} [maxLookAhead=500] * How far ahead of `startCounter` we scan. RFC 4226 §7.4 does not * specify a bound; production deployments use 100–1000 depending on * how often tokens might drift. * @property {6 | 7 | 8 | 9 | 10} [digits=6] * @property {OtpAlgorithm} [algorithm='SHA1'] */ /** * RFC 4226 §7.4 counter resynchronisation. Given two consecutive OTPs * the user typed off a hardware token that drifted, find the counter * value that makes both codes match — code #1 at some counter `N` * and code #2 at exactly `N+1`. Returns the *next* counter to store * (`N + 2`) on success, or `null` when the pair is not consistent. * * The scan is bounded by `maxLookAhead`; requests further off than * that fail rather than hanging. * * const nextCounter = resynchronize(secret, ['847362', '128394'], { * startCounter: userRow.hotpCounter, * }) * if (nextCounter === null) return res.status(400).end('resync failed') * await db.users.update(userId, { hotpCounter: nextCounter }) * * @param {string | Buffer | Uint8Array} secret * @param {[string, string]} codes Two consecutive user-entered codes. * @param {ResyncOptions} [options] * @returns {number | null} */ declare function resynchronize(secret: string | Buffer | Uint8Array, codes: [string, string], options?: ResyncOptions): number | null; type OtpAlgorithm = "SHA1" | "SHA224" | "SHA256" | "SHA384" | "SHA512"; type HotpOptions = { /** * Length of the emitted code. 6 is the universal default — Google * Authenticator, Microsoft Authenticator, Yubico, and every other * mainstream app agree on 6. Twilio Authy accepts 7. Aegis / 2FAS / * FreeOTP / 1Password / Bitwarden accept 6-10. Values above 10 * would emit non-uniform digits and are refused. */ digits?: 6 | 7 | 8 | 9 | 10 | undefined; /** * HMAC algorithm. **`SHA1` is the only value that works everywhere** — * Google Authenticator and Microsoft Authenticator only accept SHA-1. * `SHA256` and `SHA512` are supported by Twilio Authy (SHA-256 only), * Aegis, 2FAS, FreeOTP, 1Password, Bitwarden, and Yubico * Authenticator. Stick with SHA-1 for public-facing enrollment; * SHA-256/512 only when you control the client too. */ algorithm?: OtpAlgorithm | undefined; }; type HotpVerifyOptions = { digits?: 6 | 7 | 8 | 9 | 10 | undefined; algorithm?: OtpAlgorithm | undefined; /** * Counter drift tolerance — accept codes in the range * `[counter, counter + window]`. HOTP always looks *ahead* (never * behind) because used counters can never be replayed. Set to 0 for * strict single-counter verify. */ window?: number | undefined; }; type ResyncOptions = { /** * Where to start scanning. Almost always the last known-good counter * from your database. */ startCounter?: number | undefined; /** * How far ahead of `startCounter` we scan. RFC 4226 §7.4 does not * specify a bound; production deployments use 100–1000 depending on * how often tokens might drift. */ maxLookAhead?: number | undefined; digits?: 6 | 7 | 8 | 9 | 10 | undefined; algorithm?: OtpAlgorithm | undefined; }; export { _verifyHotpForward, hotp, resynchronize, verifyHotp }; export type { HotpOptions, HotpVerifyOptions, OtpAlgorithm, ResyncOptions };