This document outlines security considerations and best practices when using the genotp-go library.
- Secret Key Management
- Memory Safety
- Timing Attacks
- Replay Protection
- Rate Limiting
- Clock Skew Detection
- Context Binding
- Algorithm Selection
- URI Security
- Best Practices
- Reporting Vulnerabilities
The library uses crypto/rand from the Go standard library to generate secrets. crypto/rand calls the OS-provided CSPRNG: getrandom(2) on Linux, arc4random on macOS/BSD, and BCryptGenRandom on Windows.
import "github.com/robby031/genotp-go"
// Generate a default 160-bit (20-byte) secret
secret, err := genotp.CreateSecret()
// Or with a custom length (minimum 128 bits)
kg := &genotp.KeyGen{}
secret, err := kg.GenerateSecret(256)- Minimum Key Length: Use at least 128-bit (16-byte) secrets for HOTP/TOTP
- Recommended Key Length: Use 256-bit (32-byte) secrets for better security
- Never Reuse Secrets: Each user/service should have a unique secret
- Encrypt at Rest: Store secrets encrypted in your database
- Use a KMS: Consider using a Key Management Service for managing encryption keys
- Access Control: Restrict access to secrets to authorized personnel only
- Audit Logs: Log all access to secret keys
- Use HTTPS: Always transmit secrets over encrypted connections
- Avoid Email: Never send secrets via email or other insecure channels
- Secure Channels: Use secure channels for initial secret provisioning
HOTP and TOTP provide a ClearSecret() method that explicitly overwrites the secret with zeros. Unlike Rust, Go does not have automatic destructors, so the caller must invoke ClearSecret() manually.
hotp, _ := genotp.NewHOTP(secret, genotp.SHA1, 6)
// ... use hotp ...
hotp.ClearSecret() // Secret is overwritten with zerosImportant note on the Go GC: The Go garbage collector does not guarantee timely deallocation. Secrets may remain in memory until the next GC cycle or until the page is reused. For high-security requirements, consider managing secrets with sync.Pool or pinned memory.
- Manual Zeroize:
ClearSecret()performs a byte-by-byte overwrite loop - Cleared Flag: The struct carries an atomic
clearedflag; operations after clearing will fail - No Persistence: Secrets are never written to disk or logs by the library
The library implements constant-time comparison internally. All Verify and VerifyWithResync methods on HOTP, as well as Verify, VerifyBound, and VerifyTracking on TOTP, automatically use constant-time comparison.
hotp, _ := genotp.NewHOTP(secret, genotp.SHA1, 6)
ok, _ := hotp.Verify(code, counter) // internal constant-time comparison- Timing Side Channels: Code comparison time is independent of the input
- No Early Returns: Comparison completes even after a mismatch is detected
- Fixed-Time Operations: All operations take the same amount of time regardless of input
The library provides replay protection through the Verifier struct:
verifier := genotp.NewVerifier(5) // max 5 attempts
ok := verifier.VerifyWithReplayProtection(code, expected)For stronger protection, use context binding:
ctx := genotp.NewOtpContextBuilder().IP(clientIP).Device(deviceID).Build()
ok := verifier.VerifyWithContext(code, expected, issuedCtx, requestCtx)- Code Tracking:
ReplayStorerecords codes that have already been accepted - TTL: Entries automatically expire after the TTL (default 90 seconds)
- Fail Closed: Store errors (e.g., Redis down) are treated as reject, not accept
Warning: NewVerifier uses an InMemoryReplayStore that is only safe for single-process deployments. In a multi-replica environment (e.g., Kubernetes), replay state is isolated per process, and an attacker can bypass replay protection by routing the same code to a different replica.
Solution: Use NewVerifierWithStore with a shared backend:
// Implement ReplayStore with Redis SET NX EX
distributedStore := NewRedisReplayStore(redisClient)
verifier := genotp.NewVerifierWithStore(5, distributedStore, 90*time.Second)The Verifier provides rate limiting based on an attempt counter:
verifier := genotp.NewVerifier(5)
if verifier.IsRateLimited() {
// reject the request, return 429 or lock out the user
}
ok := verifier.VerifyWithReplayProtection(code, expected)verifier.ResetAttempts() // Reset the counter (e.g., after secondary authentication)
verifier.ClearUsedCodes() // Clear the replay set (admin/testing)The attempt-based rate limiting is per-instance, not distributed. For multi-replica deployments, implement distributed rate limiting at the gateway layer (e.g., Redis INCR + EXPIRE).
The library supports clock-skew detection and compensation for TOTP:
detector := genotp.NewClockSkewDetector(64)
detector.EnableAutoAdjust()
ok, _ := totp.VerifyTracking(code, nil, 1, detector)
report := detector.Report()
// report.Recommend provides guidance such as ConsistentDrift, WidenWindowOrCheckNtp, etc.- Clock Skew: Compensates for time drift between the client and the server
- Auto Adjust: Automatically adjusts the offset based on verification history
- Edge Hit Detection: Detects whether the user frequently lands at the edge of the window
Context binding ties an OTP to a specific context (IP, device, origin) to prevent phishing and replay in a different environment:
ctx := genotp.NewOtpContextBuilder().
IP(clientIP).
Device(deviceID).
Origin("https://example.com").
Build()
code, _ := totp.GenBound(ctx, nil)
ok, _ := totp.VerifyBound(userCode, ctx, nil, 1)- Phishing Resistance: A code is only valid for the matching context
- Reusable with Verifier:
VerifyWithContextcompares the context before the replay check
- SHA1: The default, widely supported by clients, but consider it deprecated for new systems
- SHA256: Recommended for new implementations
- SHA512: The highest security level, but may not be supported by all clients
- Use SHA256+: Prefer SHA256 or SHA512 for new implementations
- Check Client Support: Verify that the client supports the chosen algorithm
- Plan Migration: Have a migration plan if you are currently using SHA1
- SHA1: The fastest, but the least secure
- SHA256: A good balance of speed and security
- SHA512: Slower, but the most secure
The library generates otpauth:// URIs for provisioning:
uri := genotp.NewOtpAuthUri(genotp.TotpType, "Service:user@example.com", secretB32).
Issuer("Example Inc").
Algorithm(genotp.SHA256).
Digits(6).
Period(30).
Build()- Secret in URI: The secret is included in the URI (required by the standard)
- Transmission: Use QR codes or secure channels for URI transmission
- Short Lifetime: Generate URIs with a short expiration when possible
- Access Control: Restrict who can generate and view URIs
- HTTPS Only: Never transmit URIs over unencrypted connections
- QR Code Security: Display QR codes securely, not in public areas
- One-Time Use: Generate a new URI for each provisioning event
- Audit Logging: Log URI generation events
- Use the Latest Version: Always use the latest version of the library
- Keep Dependencies Updated: Update dependencies regularly
- Security Audits: Perform regular security audits
- Penetration Testing: Conduct penetration testing
- Code Review: Have code reviewed by security experts
- Validate Input: Always validate user input
- Error Handling: Implement proper error handling
- Logging: Log security-relevant events
- Monitoring: Monitor for suspicious activity
- Incident Response: Have an incident response plan
- Environment Separation: Separate development, staging, and production
- Access Control: Implement proper access controls
- Network Security: Use firewalls and network segmentation
- Regular Backups: Maintain regular, secure backups
- Disaster Recovery: Have a disaster recovery plan
If you discover a security vulnerability, please report it responsibly via the GitHub issue tracker or by contacting the maintainer.
- Description: A detailed description of the vulnerability
- Impact: The potential impact of the vulnerability
- Reproduction: Steps to reproduce the vulnerability
- Proof of Concept: A proof of concept (if applicable)
- Suggested Fix: A suggested fix or mitigation (if known)
- Private Disclosure: Vulnerabilities are disclosed privately first
- Patch Timeline: Patches are released within a reasonable timeframe
- Public Disclosure: Public disclosure after a patch is available
- Credit: Reporters are credited in security advisories