@ferrow/totp-otp
v1.0.0
Published
RFC 6238 TOTP + RFC 4226 HOTP over Node's built-in crypto: base32 codec, SHA1/SHA256/SHA512, verify with a ± window, otpauth:// provisioning URIs, constant-time compare. Validated against the RFC 6238 Appendix B test vectors.
Maintainers
Readme
totp-otp
RFC 6238 TOTP + RFC 4226 HOTP for TypeScript/JavaScript, built on Node's
built-in crypto (zero external runtime dependencies). Includes RFC 4648
base32 encode/decode, SHA1/SHA256/SHA512, verification with a ± drift
window, an otpauth:// provisioning URI builder, and a constant-time
comparison. Validated against the RFC 6238 Appendix B test vectors —
see examples/demo.js.
Not audited. This is a straightforward, spec-conformant implementation that passes the RFC's own published test vectors — it has not been through an independent security audit. Treat accordingly for anything high-stakes; review the source yourself before using it to protect real accounts.
Install
npm install totp-otpQuickstart
import { totp, verifyTotp, base32Decode, buildProvisioningUri } from "totp-otp";
const secret = base32Decode("JBSWY3DPEHPK3PXP");
const code = totp(secret); // current 6-digit code, 30s period
verifyTotp(code, secret); // true, ± 1 period window by default
buildProvisioningUri("Ferrow:[email protected]", "JBSWY3DPEHPK3PXP", { issuer: "Ferrow" });
// "otpauth://totp/Ferrow:chris%40example.com?secret=...&issuer=Ferrow&algorithm=SHA1&digits=6&period=30"API
base32Encode(bytes: Uint8Array): string / base32Decode(input: string): Buffer
RFC 4648 base32 codec. Decode strips whitespace, upper-cases, and tolerates missing padding.
hotp(secret: Uint8Array, counter: number | bigint, options?: HotpOptions): string
RFC 4226 HOTP. secret is raw key bytes (decode from base32 first if
that's how you store it).
totp(secret: Uint8Array, options?: TotpOptions): string
RFC 6238 TOTP — HOTP with counter = floor(timestamp / period).
verifyTotp(token: string, secret: Uint8Array, options?: VerifyOptions): boolean
Accepts codes from ±options.window periods around the current (or
options.timestamp) time, using a constant-time comparison per candidate.
constantTimeEqual(a: string, b: string): boolean
Constant-time string compare (via Node's crypto.timingSafeEqual); returns
false immediately on a length mismatch.
buildProvisioningUri(label: string, secretBase32: string, options?: ProvisioningOptions): string
Builds an otpauth://totp/... or otpauth://hotp/... URI (what
authenticator apps read, typically via a QR code).
type HashAlgorithm = "sha1" | "sha256" | "sha512";
interface HotpOptions { digits?: number; algorithm?: HashAlgorithm; } // digits default 6, algorithm default "sha1"
interface TotpOptions extends HotpOptions { period?: number; timestamp?: number; } // period default 30
interface VerifyOptions extends TotpOptions { window?: number; } // window default 1
interface ProvisioningOptions { type?: "totp" | "hotp"; issuer?: string; algorithm?: HashAlgorithm; digits?: number; period?: number; counter?: number; }Limits
- Not audited (see above).
- No built-in secret generation/storage helper — you supply the secret
bytes (e.g. from
crypto.randomBytes+base32Encodefor provisioning). verifyTotp's window checks2*window + 1candidates linearly; large windows increase both acceptance tolerance and brute-force surface — keep it small (1-2) for real accounts.- No replay-protection/used-code tracking — that's an application-level concern (reject a code that already succeeded once).
Part of the ferrow-toolkit collection · Sponsored by Ferrow
