Go implementation of RFC 6238 (TOTP) and RFC 4226 (HOTP) with backup recovery codes.
- Strictly follows RFC 6238 and RFC 4226, verified against official RFC test vectors.
- Standard library only (
crypto/hmac,crypto/rand,crypto/subtle,encoding/base32). - Constant-time code comparison with
crypto/subtle. ValidateWithStepreturns the counter step to prevent replay attacks within the 30-second window.- Generates
xxxx-xxxxbackup codes using 32 unambiguous characters (23456789abcdefghjkmnpqrstuvwxyz, skipping0,1,i,l,o). - Zero modulo bias because 256 divides evenly by 32.
- Generates secrets in ~145 ns and recovery codes in ~560 ns.
go get github.com/fumbledlol/totp-gopackage main
import (
"fmt"
"log"
"github.com/fumbledlol/totp-go"
)
func main() {
secret, err := totp.GenerateSecret()
if err != nil {
log.Fatal(err)
}
authURL := totp.BuildURL("Acme", "alice@example.com", secret)
fmt.Println("Secret:", secret)
fmt.Println("URL:", authURL)
}valid := totp.Validate("123456", secret)
if valid {
fmt.Println("Code accepted")
}To stop users or attackers from reusing the same passcode during its 30-second window, record the consumed counter step in Redis or SQL:
valid, step := totp.ValidateWithStep(passcode, userSecret)
if !valid {
return errors.New("invalid or expired passcode")
}
if step <= user.LastUsedTOTPStep {
return errors.New("passcode already used")
}
user.LastUsedTOTPStep = stepGenerate, normalize, and hash backup codes:
codes, err := totp.GenerateRecoveryCodes(10)
if err != nil {
log.Fatal(err)
}
// Show these to the user once:
for _, code := range codes {
fmt.Println(code) // e.g. 7m4k-9y2p
}
// Store hashes with your server pepper
serverPepper := []byte("your-secret-server-pepper")
for _, code := range codes {
hash := totp.HashRecoveryCode(code, serverPepper)
// save hash to database...
}
// When validating user input:
inputCode := "7M4K 9Y2P" // handles spaces, dashes, and casing
inputHash := totp.HashRecoveryCode(inputCode, serverPepper)BenchmarkGenerateSecret-12 8068794 144.5 ns/op 32 B/op 1 allocs/op
BenchmarkValidate-12 716224 1457.0 ns/op 1024 B/op 16 allocs/op
BenchmarkGenerateRecoveryCodes-12 2140929 562.6 ns/op 400 B/op 12 allocs/op
BenchmarkHashRecoveryCode-12 1605768 758.0 ns/op 664 B/op 10 allocs/op