State-of-the-art TOTP/HOTP library for .NET 10.
Full RFC 6238 (TOTP) and RFC 4226 (HOTP) compliance, Steam Guard support, multiple HMAC algorithms, configurable validation windows, NTP drift correction, otpauth:// URI support with local QR code rendering, out-of-band (SMS/email) codes, brute-force backoff protection, ASP.NET Core Identity integration, optional Redis-backed distributed storage, and a rich fluent API — built for correctness, performance, and security.
Install :
dotnet add package OtpSharper Optional add-on packages:
dotnet add package OtpSharper.AspNetCore.Identity # ASP.NET Core Identity two-factor provider
dotnet add package OtpSharper.Redis # Redis-backed distributed stores
- Features
- Installation
- Quick Start
- TOTP
- HOTP (Counter-Based)
- Steam Guard
- otpauth:// URI
- QR Code Generation
- Out-of-Band (SMS / Email) Codes
- ASP.NET Core Identity Integration
- Clock Drift Correction
- Dependency Injection
- Distributed Storage (Redis)
- Security & Hardening
- Project Structure
- Benchmarks
- Testing
- RFC Compliance
- License
| Feature | Details |
|---|---|
| TOTP | RFC 6238 compliant — 30s/60s/custom steps |
| HOTP | RFC 4226 compliant — stateful counter store |
| Steam Guard | Valve's custom 5-char TOTP variant |
| Algorithms | HMAC-SHA1, SHA256, SHA384, SHA512, SHA3-256, SHA3-512 |
| Digits | Configurable 1–10 digits |
| Windows | Asymmetric look-ahead / look-behind validation windows |
| Epoch | Custom T0 epoch (RFC 6238 §4) |
| Clock sync | NTP drift measurement + corrected time provider |
| URI | otpauth:// build, parse |
| QR codes | Local PNG / SVG / data-URI rendering — no external service |
| Out-of-band codes | Random, hashed, single-use codes for SMS/email delivery |
| ASP.NET Core Identity | Drop-in IUserTwoFactorTokenProvider (separate package) |
| DI | IServiceCollection extension methods |
| Security | Constant-time comparison, pinned + zeroed secret memory |
| Backoff | Thread-safe brute-force lockout with configurable policy |
| Secret strength | Entropy estimation and minimum-strength enforcement |
| Distributed storage | Redis-backed out-of-band code store (separate package) |
dotnet add package OtpSharperOptional add-ons:
dotnet add package OtpSharper.AspNetCore.Identity # ASP.NET Core Identity two-factor provider
dotnet add package OtpSharper.Redis # Redis-backed IOobCodeStoreRequirements: .NET 10.0+. OtpSharper itself now also pulls in QRCoder for local QR rendering. OtpSharper.AspNetCore.Identity additionally requires Microsoft.Extensions.Identity.Core; OtpSharper.Redis requires StackExchange.Redis.
usingOtpSharper;// Generate a new secret and configure TOTP for a uservarmanager=OtpManager.Create("alice@example.com",issuer:"MyApp");
# Presentto the user for scanning
Console.WriteLine(manager.GetOtpAuthUri());// otpauth://totp/...Console.WriteLine(manager.GetSetupKey());// JBSWY3DPEHPK3PXP (Base32)byte[]qrPng=manager.GetQrCodePng();// render locally, no external service// Generate the current codevarcode=manager.Generate();Console.WriteLine($"{code.Code} (expires in {code.RemainingSeconds}s)");// Validate user inputboolvalid=manager.Validate(userInput);varsecret=OtpSecret.FromBase32("JBSWY3DPEHPK3PXP");varoptions=TotpOptions.GoogleAuthenticator;// SHA1, 30s, 6 digits, ±1 windowvartotp=newTotpGenerator(secret,options);OtpCodecode=totp.Generate();varresult=totp.Validate(userInput);if(result.IsValid)Console.WriteLine($"Matched at window offset {result.WindowOffset}");varoptions=newTotpOptionsBuilder().WithAlgorithm(OtpAlgorithm.HmacSha256).WithStepSeconds(30).WithDigits(8).WithValidationWindow(1)// ±1 step (3 codes accepted).WithExtraLookBehind(1)// 2 steps behind total.Build();vartotp=newTotpGenerator(secret,options);TotpOptions.Default// SHA1, 30s, 6 digits, ±1TotpOptions.GoogleAuthenticator // Same as Default
TotpOptions.HighSecurity // SHA256, 30s, 8 digits, strict (0 window)
TotpOptions.MaxSecurity // SHA512, 30s, 8 digits, strict
TotpOptions.SixtySeconds // SHA1, 60s, 6 digits, ±1// Inspect all codes in the validation window — useful for diagnosing clock driftvarwindow=totp.GenerateWindow();foreach(var(offset,code)inwindow)Console.WriteLine($"Offset {offset,+3}: {code.Code}");usingOtpSharper.Hotp;varstore=newInMemoryHotpCounterStore();varhotp=newHotpGenerator(secret);// Generate at a specific counterOtpCodecode=hotp.GenerateAt(counter:0);// "755224" (RFC 4226 vector)// Validate + auto-advance counter in storevarresult=awaithotp.ValidateAsync("755224",keyId:"user1",store);if(result.IsValid)Console.WriteLine($"Matched counter {result.MatchedCounter}, offset {result.WindowOffset}");publicclassDbHotpCounterStore:IHotpCounterStore{privatereadonlyMyDbContext_db;publicasyncValueTask<long>GetCounterAsync(stringkeyId,CancellationTokenct)=>(await_db.OtpCounters.FindAsync([keyId],ct))?.Counter??0;publicasyncValueTaskSetCounterAsync(stringkeyId,longnewCounter,CancellationTokenct){varrow=await_db.OtpCounters.FindAsync([keyId],ct);if(rowisnull)_db.OtpCounters.Add(newOtpCounter{KeyId=keyId,Counter=newCounter});elseif(newCounter>row.Counter)row.Counter=newCounter;await_db.SaveChangesAsync(ct);}}usingOtpSharper.Steam;varsteam=newSteamGuardGenerator(secret);SteamGuardCodecode=steam.Generate();Console.WriteLine(code.Code);// e.g. "X3Y2K"boolvalid=steam.Validate(userInput).IsValid;usingOtpSharper.Uri;// Buildvaruri=OtpUri.ForTotp("alice@example.com",secret,options,issuer:"MyApp");stringstr=uri.ToUriString();// => otpauth://totp/MyApp:alice%40example.com?secret=...&issuer=MyApp// Parsevarparsed=OtpUri.Parse("otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example");TotpGeneratortotp=parsed.ToTotpGenerator();Note: OtpSharper is the only .NET OTP library in this benchmark comparison that includes a built-in
otpauth://URI parser.
usingOtpSharper.Uri;varuri=OtpUri.ForTotp("alice@example.com",secret,options,issuer:"MyApp");byte[]png=uri.ToQrCodePng(pixelsPerModule:10);// raw PNG bytesstringsvg=uri.ToQrCodeSvg(pixelsPerModule:10);// inline SVG markupstringdataUri=uri.ToQrCodeDataUri();// "data:image/png;base64,..." for <img src="...">All three render entirely locally via QRCoder — no network call, and the secret-bearing otpauth:// URI never leaves the process. OtpManager exposes matching convenience methods (GetQrCodePng, GetQrCodeSvg, GetQrCodeDataUri).
Migrating from
ToQrCodeImageUrl()/GetQrCodeUrl(): both are marked[Obsolete]. They built a URL against the Google Charts Image API, which was shut down in 2019 — the URL no longer renders an image. Switch to the methods above.
Unlike TOTP/HOTP, out-of-band codes aren't derived from a shared secret — there's nothing for the server to compute and compare. Instead the server generates a random code, remembers only its SHA-256 hash (with a TTL and attempt limit) via IOobCodeStore, and hands you the plaintext once so you can send it through SMS or email. Validation consumes the code — one-time use, unlike a TOTP code which stays valid for its whole window.
usingOtpSharper.OutOfBand;varstore=newInMemoryOobCodeStore();// or RedisOobCodeStore — see Distributed Storagevargenerator=newOobCodeGenerator(store,newOobCodeOptions{Digits=6,Ttl=TimeSpan.FromMinutes(5),MaxAttempts=5,});stringcode=awaitgenerator.GenerateAsync("phone:+15551234567");// hand `code` to your SMS provider — it is not retrievable againvarresult=awaitgenerator.ValidateAsync("phone:+15551234567",userInput);if(result.IsValid){// proceed — the code has already been consumed and cannot be reused}The OtpSharper.AspNetCore.Identity package provides an IUserTwoFactorTokenProvider<TUser> that plugs into Identity's existing enrollment flow (GenerateNewAuthenticatorKey / ResetAuthenticatorKeyAsync) — only the token provider registration changes. It adds configurable HMAC algorithm/digits and optional replay protection on top of what Identity's built-in AuthenticatorTokenProvider offers.
dotnet add package OtpSharper.AspNetCore.IdentityusingOtpSharper.AspNetCore.Identity;services.AddIdentity<ApplicationUser,IdentityRole>().AddEntityFrameworkStores<AppDbContext>().AddOtpSharperTwoFactorTokenProvider<ApplicationUser>(options =>{options.TotpOptions=TotpOptions.GoogleAuthenticator;// default; matches existing authenticator-app enrollmentsoptions.ReplayTracker=newUsedCodeTracker();// optional: reject an intercepted code replayed within the window});Call UserManager.GenerateTwoFactorTokenAsync(user, "OtpSharper") / VerifyTwoFactorTokenAsync(user, "OtpSharper", code) as usual, or pass tokenProviderName: "Authenticator" to AddOtpSharperTwoFactorTokenProvider to swap it in as a transparent replacement for Identity's built-in provider without changing any calling code.
usingOtpSharper.Sync;// Measure drift vs NTPClockDriftResultdrift=awaitClockSync.MeasureDriftAsync("pool.ntp.org");Console.WriteLine(drift);// "Drift: +234.5ms vs pool.ntp.org at 2025-01-01T..."if(drift.IsProblematic){ITimeProvidercorrected=drift.CreateCorrectedTimeProvider();varoptions=newTotpOptions{TimeProvider=corrected};vartotp=newTotpGenerator(secret,options);}// Or auto-create in one callITimeProviderprovider=awaitClockSync.CreateCorrectedTimeProviderAsync();// Program.cs / Startup.csservices.AddTotp("JBSWY3DPEHPK3PXP", options =>options.WithAlgorithm(OtpAlgorithm.HmacSha256).WithDigits(8));// Or from a full otpauth:// URIservices.AddOtpManager("otpauth://totp/App:user@example.com?secret=...");// Out-of-band (SMS/email) codes — in-memory store by defaultservices.AddOobCodeGenerator(newOobCodeOptions{Digits=6,Ttl=TimeSpan.FromMinutes(5)});// InjectpublicclassAuthService(TotpGeneratortotp){publicboolVerify(stringcode)=>totp.Validate(code).IsValid;}The default InMemoryOobCodeStore, InMemoryHotpCounterStore, and UsedCodeTracker are all per-process — fine for a single instance, not shared across a multi-instance deployment. The OtpSharper.Redis package provides Redis-backed implementations of all three: IOobCodeStore, IHotpCounterStore, and IUsedCodeStore (a new interface UsedCodeTracker now implements, so it's swappable without touching calling code).
dotnet add package OtpSharper.RedisusingOtpSharper.Redis;usingStackExchange.Redis;services.AddSingleton<IConnectionMultiplexer>(_ =>ConnectionMultiplexer.Connect("localhost:6379"));services.AddRedisOobCodeStore();// registers IOobCodeStore backed by Redisservices.AddOobCodeGenerator();// picks up the Redis store instead of the in-memory defaultservices.AddRedisHotpCounterStore();// registers IHotpCounterStore backed by Redisservices.AddRedisUsedCodeStore();// registers IUsedCodeStore backed by Redis, for replay// protection in OtpSharperTotpOptions.ReplayTracker// (OtpSharper.AspNetCore.Identity) or your own codeRedisOobCodeStore— each pending code is a Redis hash with a nativeEXPIREmatching its TTL, so Redis evicts stale entries on its own even ifRemoveAsyncis never called.RedisHotpCounterStore—SetCounterAsync's "only advance, never regress" contract is enforced with a small Lua script evaluated server-side, so the read-compare-write is atomic even with multiple instances racing to advance the same counter.RedisUsedCodeStore— marks a (keyId, counter) pair used viaSET ... NX EX, an atomic "first use" check with a built-in expiry, in one round trip.
All three are covered by integration tests in tests/OtpSharper.Redis.Tests — they run against a real Redis instance (docker run --rm -p 6379:6379 redis, or set OTPSHARPER_TEST_REDIS) and no-op if none is reachable, rather than mocking StackExchange.Redis's surface.
Enabled by default to prevent timing oracle attacks. The validator always compares the full code string before returning, regardless of the position of the first differing character.
varoptions=newTotpOptions{UseConstantTimeComparison=true// default — do not disable in production};OtpSecret allocates a GC-pinned array and zeroes it on Dispose(). Always use using:
usingvarsecret=OtpSecret.FromBase32("JBSWY3DPEHPK3PXP");// secret bytes are zeroed when the using block exits// Estimate entropydoublebits=OtpSecretGenerator.EstimateEntropyBits(base32Secret);// ClassifySecretStrengthstrength=OtpSecretGenerator.AssessStrength(base32Secret);// Weak (≤80 bits) | Adequate (81–127) | Strong (128–255) | VeryStrong (256+)// Enforce a minimum — throws CryptographicException if not metOtpSecretGenerator.EnsureMinimumStrength(base32Secret,minimumBits:128);// Generate a correctly-sized secret for an algorithmOtpSecretsecret=OtpSecretGenerator.GenerateForAlgorithm(OtpAlgorithm.HmacSha256);Thread-safe in-memory lockout policy. OtpSharper.Redis doesn't cover OtpBackoffPolicy yet (it does cover HOTP counters and replay tracking — see Distributed Storage); for distributed lockout today, replicate the same logic against a shared cache yourself.
varpolicy=newOtpBackoffPolicy(newOtpBackoffOptions{MaxFailedAttempts=5,LockoutDuration=TimeSpan.FromMinutes(15),AttemptWindow=TimeSpan.FromMinutes(10),ResetOnSuccess=true,});// Check before validatingBackoffResultcheck=policy.CheckAllowed(userId);if(!check.IsAllowed){Console.WriteLine($"Locked out until {check.LockoutExpiry}");return;}boolvalid=totp.Validate(userCode).IsValid;if(valid)policy.RecordSuccess(userId);else{varresult=policy.RecordFailure(userId);Console.WriteLine($"{result.RemainingAttempts} attempts remaining");}| Use Case | Recommended Algorithm | Reason |
|---|---|---|
| Google Authenticator compatibility | SHA1 | RFC-mandated; universal support |
| New systems / higher security | SHA256 | Stronger, still widely supported |
| Maximum security | SHA512 or SHA3-512 | Future-proof; OtpSharper exclusive for SHA3 |
OtpSharper/
├── src/
│ ├── OtpSharper/
│ │ ├── Abstractions/
│ │ │ ├── OtpBackoffPolicy.cs # Brute-force lockout
│ │ │ ├── TotpValidationService.cs # High-level validation service
│ │ │ └── UsedCodeTracker.cs # Replay prevention tracker
│ │ ├── Algorithms/
│ │ │ ├── HmacProvider.cs # HMAC-SHA1/256/384/512/SHA3 computation
│ │ │ └── OtpAlgorithm.cs # Algorithm enum
│ │ ├── Core/
│ │ │ ├── Base32.cs # RFC 4648 Base32 codec
│ │ │ ├── DynamicTruncation.cs # RFC 4226 §5.3 truncation + constant-time compare
│ │ │ ├── OtpResults.cs # OtpCode / OtpValidationResult types
│ │ │ ├── OtpSecret.cs # Pinned, zeroed secret container
│ │ │ ├── OtpSecretGenerator.cs # Key generation + entropy assessment
│ │ │ └── TimeProvider.cs # Abstracted time source
│ │ ├── Extensions/
│ │ │ └── ServiceCollectionExtensions.cs # DI registration helpers
│ │ ├── Hotp/
│ │ │ ├── HotpCounterStore.cs # IHotpCounterStore + in-memory impl
│ │ │ ├── HotpGenerator.cs # RFC 4226 HOTP generator + validator
│ │ │ └── HotpOptions.cs # HOTP configuration
│ │ ├── OutOfBand/
│ │ │ ├── IOobCodeStore.cs # IOobCodeStore + in-memory impl
│ │ │ ├── OobCodeGenerator.cs # Random hashed single-use SMS/email codes
│ │ │ └── OobCodeOptions.cs # Digits / TTL / attempt-limit configuration
│ │ ├── Steam/
│ │ │ └── SteamGuardGenerator.cs # Steam Guard 5-char TOTP variant
│ │ ├── Sync/
│ │ │ └── ClockSync.cs # NTP drift measurement + correction
│ │ ├── Totp/
│ │ │ ├── TotpGenerator.cs # RFC 6238 TOTP generator + validator
│ │ │ ├── TotpOptions.cs # TOTP configuration + presets
│ │ │ └── TotpOptionsBuilder.cs # Fluent options builder
│ │ ├── Uri/
│ │ │ ├── OtpUri.cs # otpauth:// builder, parser
│ │ │ └── OtpQrCode.cs # Local PNG / SVG / data-URI QR rendering
│ │ ├── GlobalUsings.cs
│ │ ├── OtpManager.cs # High-level enrollment + validation facade
│ │ └── OtpSharper.csproj
│ ├── OtpSharper.AspNetCore.Identity/
│ │ ├── IdentityBuilderExtensions.cs # AddOtpSharperTwoFactorTokenProvider<TUser>
│ │ ├── OtpSharperTotpOptions.cs # Algorithm/digits/replay-tracker configuration
│ │ ├── OtpSharperTotpTokenProvider.cs # IUserTwoFactorTokenProvider<TUser> implementation
│ │ └── OtpSharper.AspNetCore.Identity.csproj
│ └── OtpSharper.Redis/
│ ├── RedisOobCodeStore.cs # IOobCodeStore backed by Redis hashes + native TTL
│ ├── ServiceCollectionExtensions.cs # AddRedisOobCodeStore
│ └── OtpSharper.Redis.csproj
├── tests/
│ ├── OtpSharper.Tests/
│ │ ├── AbstractionTests.cs # Backoff policy, used-code store tests
│ │ ├── Base32AndUriTests.cs # Base32 codec + URI round-trip tests
│ │ ├── HotpTests.cs # RFC 4226 test vectors
│ │ ├── OtpQrCodeTests.cs # Local PNG/SVG/data-URI QR rendering tests
│ │ ├── OutOfBandTests.cs # OOB code generator + in-memory store tests
│ │ ├── SteamGuardTests.cs # Steam Guard output tests
│ │ ├── TotpTests.cs # RFC 6238 test vectors + window tests
│ │ └── OtpSharper.Tests.csproj
│ └── OtpSharper.Redis.Tests/
│ ├── RedisFixture.cs # Shared connection + availability check
│ ├── RedisOobCodeStoreTests.cs # Integration tests against a real Redis instance
│ ├── RedisHotpCounterStoreTests.cs # Including a concurrent-advance race test
│ ├── RedisUsedCodeStoreTests.cs # Including a concurrent-first-use race test
│ └── OtpSharper.Redis.Tests.csproj
├── OtpSharper.Benchmark/
│ ├── AlgorithmBenchmarks.cs # Per-algorithm TOTP throughput
│ ├── Base32Benchmarks.cs # Base32 encode/decode at various key sizes
│ ├── HotpGenerationBenchmarks.cs # HOTP generation at various counters
│ ├── OtpUriBenchmarks.cs # URI parse + round-trip
│ ├── SecretKeySetupBenchmarks.cs # Full cold-path setup + generate
│ ├── TotpGenerationBenchmarks.cs # Steady-state TOTP generation
│ ├── TotpValidationBenchmarks.cs # Server-side validation hot path
│ ├── Program.cs
│ └── OtpSharper.Benchmark.csproj
├── OtpSharper.sln
└── LICENSE
Benchmarks compare OtpSharper against Otp.NET (v1.4.1), the most widely-used .NET OTP library. All tests run on .NET 10.0 using BenchmarkDotNet 0.15.8 in Release mode with JIT optimizations enforced.
cd OtpSharper.Benchmark
dotnet run -c ReleaseThese benchmarks measure the cold path — relevant for stateless APIs that construct the TOTP object on every request rather than caching it.
| Method | Categories | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|---|
| Otp.NET | Setup_And_Generate | 2,933.23 ns | 40.393 ns | 35.808 ns | baseline | 960 B | ||
| OtpSharper | Setup_And_Generate | 1,510.06 ns | 14.997 ns | 12.523 ns | 1.94x faster | 0.03x | 736 B | 1.30x less |
| Otp.NET | Setup_FromBase32_Generate | 3,233.58 ns | 27.413 ns | 22.891 ns | baseline | 1008 B | ||
| OtpSharper | Setup_FromBase32_Generate | 1,687.82 ns | 22.657 ns | 20.085 ns | 1.92x faster | 0.03x | 784 B | 1.29x less |
| Otp.NET | Setup_ObjectCreation | 61.45 ns | 1.529 ns | 1.277 ns | baseline | 168 B | ||
| OtpSharper | Setup_ObjectCreation | 155.67 ns | 6.084 ns | 5.080 ns | 2.53x slower | 0.09x | 192 B | 1.14x more |
This scenario constructs the TOTP object and computes a code in one shot, simulating a stateless server that doesn't cache generator instances (a common pattern in microservices and serverless functions). OtpSharper is nearly twice as fast here. In a system processing thousands of 2FA verifications per second, this directly translates to throughput.
Same as above but starting from a Base32-encoded secret string — the realistic path when a stored secret is read from a database and decoded before use. OtpSharper's combined Base32 decode + HMAC compute pipeline is more efficient than Otp.NET's equivalent. Memory savings (~220 bytes per call) also reduce GC pressure in high-throughput scenarios.
When creating a generator object alone (with no code generation), OtpSharper is slower. This is an intentional trade-off: OtpSecret performs GC pinning at construction time to protect key material from being moved or scanned in memory. This extra work during object creation pays a small upfront cost that enables the secure zeroing-on-dispose behaviour. For any scenario that actually generates or validates a code — which is every real-world use — the pinning overhead is amortised and OtpSharper wins overall (see the two rows above).
In short: OtpSharper is faster where it matters (end-to-end operations) and slower only in the micro-benchmark that isolates pure object allocation — a scenario that never occurs in isolation in production.
Tests use xUnit and FluentAssertions.
cd tests/OtpSharper.Tests
dotnet testTest coverage includes:
- RFC 4226 test vectors — all 10 HOTP reference values from Appendix D
- RFC 6238 test vectors — all 18 TOTP reference values from Appendix B (SHA1, SHA256, SHA512)
- Validation window — look-ahead, look-behind, and asymmetric window tests
- Base32 codec — encode/decode round-trips, padding, error cases
- otpauth:// URI — parse, build, and round-trip for TOTP and HOTP URIs
- Steam Guard — expected output for known inputs
- Backoff policy — lockout triggering, expiry, reset on success, concurrent access
- Used-code / replay tracking — first-use vs. replay, via the
IUsedCodeStoreinterface - Out-of-band codes — generation, validation, expiry, attempt-limit lockout, one-time use
- Local QR rendering — PNG signature validity, SVG markup, data-URI round-trip
Redis-backed stores (OtpSharper.Redis) have their own integration test project, tests/OtpSharper.Redis.Tests, run separately since they need a real Redis instance:
docker run --rm -p 6379:6379 redis # or set OTPSHARPER_TEST_REDIS to point elsewherecd tests/OtpSharper.Redis.Tests
dotnet testTests in that project no-op (rather than fail) if no Redis is reachable, so dotnet test on the whole solution still passes without one — see RedisFixture for the connectivity check. They cover round-tripping each store plus the concurrency guarantees that motivate using Redis in the first place: RedisHotpCounterStoreTests.ConcurrentAdvances_NeverRegress and RedisUsedCodeStoreTests.ConcurrentFirstUse_OnlyOneWinner both fire many simultaneous calls at the same key and assert the atomic Lua-script / SET NX EX behaviour holds up.
Not yet covered:
OtpSharper.AspNetCore.Identity'sOtpSharperTotpTokenProviderhas no tests yet — it needs aUserManager<TUser>test harness (a fakeIUserStore/IUserAuthenticatorKeyStore, orMicrosoft.AspNetCore.Identity.EntityFrameworkCorewith an in-memory provider) that's more setup than the rest of this session's scope covered. Treat that package as the one part of this PR still worth reviewing carefully before merging.
| RFC | Section | Status |
|---|---|---|
| RFC 4226 | §4 Algorithm | ✅ Full |
| RFC 4226 | §5 Dynamic Truncation | ✅ Full |
| RFC 4226 | Appendix D test vectors | ✅ All 10 pass |
| RFC 6238 | §4 TOTP Algorithm | ✅ Full |
| RFC 6238 | §5 Security | ✅ (constant-time, window) |
| RFC 6238 | Appendix B test vectors | ✅ All 18 pass |
| RFC 4648 | Base32 encoding | ✅ Full |
| Google Auth Key URI | otpauth:// format | ✅ Full |
MIT — see LICENSE for details.
Copyright © 2026 neo-vortex