RFC Implementations
otplib is fully compliant with the following RFCs:
RFC 4226 - HOTP: An HMAC-Based One-Time Password Algorithm
Key Requirements
| Requirement | Status | Notes |
|---|---|---|
| HMAC-SHA-1 | Yes | Default hash algorithm |
| Counter as 8-byte big-endian | Yes | counterToBytes() in core |
| Dynamic truncation | Yes | dynamicTruncate() in core |
| Digit extraction (6-8 digits) | Yes | Configurable digits parameter |
| Look-ahead synchronization | Yes | Configurable window parameter |
| Secret minimum 128 bits | Yes | validateSecret() enforces this |
| Secret recommended 160 bits | Yes | generateSecret() uses 20 bytes |
Test Vectors
otplib passes all RFC 4226 Appendix D test vectors:
| Counter | Expected | Actual |
|---|---|---|
| 0 | 755224 | 755224 |
| 1 | 287082 | 287082 |
| 2 | 359152 | 359152 |
| 3 | 969429 | 969429 |
| 4 | 338314 | 338314 |
| 5 | 254676 | 254676 |
| 6 | 287922 | 287922 |
| 7 | 162583 | 162583 |
| 8 | 399871 | 399871 |
| 9 | 520489 | 520489 |
Intermediate HMAC-SHA-1 Values
otplib also verifies the intermediate HMAC-SHA-1 computation against RFC 4226 Appendix D. This ensures the crypto plugin is functioning correctly before truncation.
| Counter | HMAC-SHA-1 (hex) |
|---|---|
| 0 | cc93cf18508d94934c64b65d8ba7667fb7cde4b0 |
| 1 | 75a48a19d4cbe100644e8ac1397eea747a2d33ab |
| 2 | 0bacb7fa082fef30782211938bc1c5e70416ff44 |
| 3 | 66c28227d03a2d5529262ff016a1e6ef76557ece |
| 4 | a904c900a64b35909874b33e61c5938a8e15ed1c |
| 5 | a37e783d7b7233c083d4f62926c7a25f238d0316 |
| 6 | bc9cd28561042c83f219324d3c607256c03272ae |
| 7 | a4fb960c0bc06e1eabb804e5b397cdc4b45596fa |
| 8 | 1b3c89f65e6c9e883012052823443f048b4332db |
| 9 | 1637409809a679dc698207310c8c7fc07290d9e5 |
Test File: packages/hotp/src/rfc4226.test.ts
RFC 6238 - TOTP: Time-Based One-Time Password Algorithm
Key Requirements
| Requirement | Status | Notes |
|---|---|---|
| Based on HOTP | Yes | Extends HOTP implementation |
| Unix time (T0) = 0 | Yes | Default epoch start |
| Time step (X) = 30 | Yes | Default, configurable |
| SHA-1 support | Yes | Default algorithm |
| SHA-256 support | Yes | Optional algorithm |
| SHA-512 support | Yes | Optional algorithm |
| Time drift tolerance | Yes | Configurable window |
Test Vectors
otplib passes all RFC 6238 Appendix B test vectors:
| Time (sec) | Mode | SHA | Expected | Actual |
|---|---|---|---|---|
| 59 | TOTP | SHA1 | 94287082 | 94287082 |
| 59 | TOTP | SHA256 | 46119246 | 46119246 |
| 59 | TOTP | SHA512 | 90693936 | 90693936 |
| 1111111109 | TOTP | SHA1 | 07081804 | 07081804 |
| 1111111109 | TOTP | SHA256 | 68084774 | 68084774 |
| 1111111109 | TOTP | SHA512 | 25091201 | 25091201 |
| 1111111111 | TOTP | SHA1 | 14050471 | 14050471 |
| 1234567890 | TOTP | SHA1 | 89005924 | 89005924 |
| 2000000000 | TOTP | SHA1 | 69279037 | 69279037 |
| 20000000000 | TOTP | SHA1 | 65353130 | 65353130 |
Intermediate Time Step (T) Values
otplib verifies the time step calculation against the "Value of T (hex)" from RFC 6238 Appendix B. This ensures that time-to-counter conversion is accurate.
| Time (sec) | Value of T (hex) |
|---|---|
| 59 | 0000000000000001 |
| 1111111109 | 00000000023523ec |
| 1111111111 | 00000000023523ed |
| 1234567890 | 000000000273ef07 |
| 2000000000 | 0000000003f940aa |
| 20000000000 | 0000000027bc86aa |
RFC 4648 - The Base16, Base32, and Base64 Data Encodings
Key Requirements
| Requirement | Status | Notes |
|---|---|---|
| RFC 4648 Section 6 (Base32) | Implemented | Standard alphabet |
| Padding character (=) | Implemented | Optional padding |
| Case-insensitive decoding | Implemented | Uppercase conversion |
| No padding for GA | Implemented | Configurable |
| Test vectors | Pass | All Section 10 vectors |
Test Vectors
otplib passes all RFC 4648 Section 10 test vectors:
| Input | Expected (with padding) | Expected (no padding) | Actual (with padding) | Actual (no padding) | Status |
|---|---|---|---|---|---|
| "" (empty) | "" | "" | "" | "" | Pass |
| "f" | "MY======" | "MY" | "MY======" | "MY" | Pass |
| "fo" | "MZXQ====" | "MZXQ" | "MZXQ====" | "MZXQ" | Pass |
| "foo" | "MZXW6===" | "MZXW6" | "MZXW6===" | "MZXW6" | Pass |
| "foob" | "MZXW6YQ=" | "MZXW6YQ" | "MZXW6YQ=" | "MZXW6YQ" | Pass |
| "fooba" | "MZXW6YTB" | "MZXW6YTB" | "MZXW6YTB" | "MZXW6YTB" | Pass |
| "foobar" | "MZXW6YTBOI======" | "MZXW6YTBOI" | "MZXW6YTBOI======" | "MZXW6YTBOI" | Pass |
Google Authenticator Compatibility
Google Authenticator expects no padding and uses the standard RFC 4648 alphabet:
// GA-compatible encoding
const secret = base32.encode(randomBytes(20), { padding: false });
// Example: "GEZDGNBVGY3TQOJQGEZDGNBVGY"otpauth:// URI Format
otplib generates interoperable otpauth:// URIs and permissively parses the format used by Google Authenticator:
otpauth://TYPE/LABEL?PARAMETERSSupported Parameters:
| Parameter | Required | Type | Description | Default |
|---|---|---|---|---|
| secret | Yes | string | Base32-encoded secret | - |
| issuer | Recommended | string | Provider name | - |
| algorithm | No | string | Hash algorithm | SHA1 |
| digits | No | number | OTP length | 6 |
| counter | Yes (HOTP only) | number | Initial counter | - |
| period | No (TOTP only) | number | Time step in seconds | 30 |
The Key URI format requires counter for HOTP. Otplib's HOTP generation helpers default it to 0 and always emit it; parsing also accepts an absent counter for backward compatibility.
The OTPAuth URI format does not define how duplicate query parameters are resolved. Otplib consistently uses the first occurrence of every decoded key, matching URLSearchParams.get(): the first value is parsed and validated, while later values are ignored without being decoded. A bare parameter counts as an empty first value. Avoid duplicate parameters when interoperability matters.
Parsing preserves optionality: an absent, bare, or empty algorithm parameter leaves the parsed algorithm undefined. OTP generation and verification apply the SHA-1 default when consuming that result.
URI Generation
import { generateURI } from "otplib";
const uri = generateURI({
issuer: "MyService",
label: "user@example.com",
secret: "GEZDGNBVGY3TQOJQGEZDGNBVGY",
algorithm: "sha1",
digits: 6,
period: 30,
});
// otpauth://totp/MyService:user%40example.com?secret=...&issuer=MyServiceTest Integration
- Generate secret and URI in your app
- Display QR code from URI
- Scan with Google Authenticator
- Verify tokens generated by app
Security Considerations
RFC 4226 Section 7.3 - Throttling
Recommendation: Implement exponential backoff after failed attempts.
// Example rate limiting strategy
const MAX_ATTEMPTS = 3;
const LOCKOUT_DURATION = 30 * 60; // 30 minutes
function checkRateLimit(userId: string): boolean {
const attempts = getFailedAttempts(userId);
if (attempts >= MAX_ATTEMPTS) {
const lockoutEnd = getLockoutEndTime(userId);
if (Date.now() < lockoutEnd) {
return false; // Locked out
}
}
return true;
}RFC 6238 Section 5.2 - Tolerance
Recommendation: Use smallest tolerance that provides good UX.
// RFC-compliant (past only, for transmission delay)
epochTolerance: [5, 0];
// Standard (allow ±30 seconds)
epochTolerance: 30;
// Strict (current period only)
epochTolerance: 0;Timing Attack Prevention
otplib uses constant-time comparison for all token verifications:
function constantTimeEqual(a: string | Uint8Array, b: string | Uint8Array): boolean {
const bufA = typeof a === "string" ? new TextEncoder().encode(a) : a;
const bufB = typeof b === "string" ? new TextEncoder().encode(b) : b;
if (bufA.length !== bufB.length) {
return false;
}
let result = 0;
for (let i = 0; i < bufA.length; i++) {
result |= bufA[i] ^ bufB[i];
}
return result === 0;
}Compliance Testing
All RFC compliance is verified through automated tests, see the rfc-test-vectors.ts file for more details.