genotp

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 1, 2026 License: MIT Imports: 17 Imported by: 0

README

genotp-go

Security-focused OTP library in Go. Full implementation of HOTP (RFC 4226) and TOTP (RFC 6238) plus advanced features: context binding, per-context replay isolation, and clock skew detection.

Highlights

  • ✅ Passes all RFC 4226 & RFC 6238 test vectors (SHA1/256/512)
  • ✅ Replay protection + rate limiting with bounded memory
  • ✅ Constant-time comparison to prevent timing attacks
  • Context binding — OTP codes bound to (IP, device, session, origin)
  • Per-context replay isolation — code collisions between users don't block each other
  • Anti-phishing origin binding — origin URL automatically normalized
  • Clock skew detector with opt-in auto-adjust
  • ✅ Compatible with Google Authenticator / Authy / Microsoft Authenticator (default mode)
  • ✅ Comprehensive test coverage

Installation

go get github.com/robby031/genotp-go

Basic Usage

Standard TOTP (Google Authenticator compatible)
package main

import (
    "fmt"
    "github.com/robby031/genotp-go"
)

func main() {
    secret, _ := genotp.CreateSecret()
    code, _ := genotp.GenTotpDefault(secret)
    valid, _ := genotp.VerifyTotpDefault(secret, code)
    
    fmt.Printf("Code: %s, Valid: %v\n", code, valid)
}
Builder pattern (more ergonomic)
secret, _ := genotp.CreateSecret()

totp, _ := genotp.NewTotpBuilder().
    Secret(secret).
    Algorithm(genotp.SHA1).
    Digits(6).
    Period(30).
    Build()

code, _ := totp.Generate(nil)
valid, _ := totp.Verify(code, nil, 1)
QR code for authenticator app
uri := genotp.NewOtpAuthUri(genotp.TotpType, "ACME:alice@example.com", genotp.EncodeBase32(secret)).
    Issuer("ACME").
    Algorithm(genotp.SHA1).
    Digits(6).
    Period(30).
    Build()

// Render `uri` to QR code (e.g., with a QR code library)
Context binding — anti channel OTP intercept (flagship feature)
hotp, _ := genotp.NewHOTP(secret, genotp.SHA1, 6)

// Server binds code to (session + IP hash) of user at issue time:
ctx := genotp.NewOtpContextBuilder().
    Session("login-abc123").
    IP("hash_of_user_ip").
    Build()

code, _ := hotp.GenBound(counter, ctx)
// Send `code` via any channel (SMS, email, WhatsApp, Telegram, push notif, ...).

// When user submits:
if hotp.VerifyBound(form.Code, counter, ctx) {
    // ✓ code correct AND context matches
}
// Attacker who intercepts code from different IP/session -> automatically rejected.

Features

HOTP (RFC 4226)
  • Generate and verify HMAC-based One-Time Passwords
  • Look-ahead resynchronization for counter drift
  • Context binding for enhanced security
TOTP (RFC 6238)
  • Time-based One-Time Passwords with configurable period
  • Window-based verification for clock skew tolerance
  • Support for SHA1, SHA256, and SHA512 algorithms
  • Context binding and clock skew tracking
Context Binding

Bind OTP codes to specific contexts:

  • IP address (or hash thereof)
  • Device identifier
  • Session ID
  • Origin URL (anti-phishing)
  • Custom fields

Two modes:

  1. HMAC binding: Different contexts produce different OTP codes
  2. Verifier-stored: Standard OTP codes, but server validates context
Clock Skew Detection

Track and compensate for clock drift between client and server:

  • Passive mode: only reports statistics
  • Active mode: automatically adjusts verification window
  • Recommends for window sizing or NTP sync
Replay Protection

Prevent OTP code reuse with:

  • Per-context replay isolation
  • Configurable rate limiting
  • Bounded memory usage

API Reference

Core Types
  • Algorithm: SHA1, SHA256, SHA512
  • HOTP: HMAC-based OTP implementation
  • TOTP: Time-based OTP implementation
  • OtpContext: Context binding data
  • ClockSkewDetector: Clock drift tracking
  • Verifier: Replay protection and rate limiting (per-instance)
  • ReplayStore: pluggable backend untuk replay-set (default = in-memory bounded + TTL; untuk multi-replica deployment implement dengan Redis / etcd / sql — lihat docs/distributed_replay_protection.md)
Helper Functions
  • CreateSecret(): Generate a random 160-bit secret
  • GenHotpDefault(): Generate HOTP with default parameters
  • GenTotpDefault(): Generate TOTP with default parameters
  • VerifyHotpDefault(): Verify HOTP with default parameters
  • VerifyTotpDefault(): Verify TOTP with default parameters
  • EncodeBase32(data []byte) string: Encode bytes to Base32 (RFC 4648, no padding)
  • DecodeBase32(dst []byte, src string) (int, error): Decode Base32 ke buffer caller. Strip ASCII whitespace, -, dan = otomatis. Mengembalikan jumlah byte yang ditulis. Returns ErrDstTooSmall jika dst kekecilan, ErrInvalidSecret jika ada karakter invalid.

Testing

go test ./tests

All RFC test vectors are included and verified:

  • RFC 4226 HOTP test vectors
  • RFC 6238 TOTP test vectors (SHA1, SHA256, SHA512)

License

MIT — see LICENSE

Documentation

Index

Constants

View Source
const (
	MinSecretBytes     = 16
	DefaultSecretBytes = 20
)

Variables

View Source
var (
	ErrInvalidSecret      = errors.New("invalid secret key")
	ErrInvalidCode        = errors.New("invalid OTP code")
	ErrInvalidDigits      = errors.New("invalid number of digits")
	ErrInvalidAlgorithm   = errors.New("invalid algorithm")
	ErrInvalidCounter     = errors.New("invalid counter value")
	ErrInvalidTime        = errors.New("invalid time value")
	ErrVerificationFailed = errors.New("OTP verification failed")
	ErrRateLimited        = errors.New("rate limited")
	ErrReplayAttack       = errors.New("replay attack detected")
	ErrDstTooSmall        = errors.New("destination buffer too small")
)

Functions

func CreateSecret

func CreateSecret() ([]byte, error)

func DecodeBase32

func DecodeBase32(dst []byte, src string) (int, error)

func EncodeBase32

func EncodeBase32(data []byte) string

func GenHotpDefault

func GenHotpDefault(secret []byte, counter uint64) (string, error)

func GenTotpDefault

func GenTotpDefault(secret []byte) (string, error)

func VerifyHotpDefault

func VerifyHotpDefault(secret []byte, code string, counter uint64) (bool, error)

func VerifyTotpDefault

func VerifyTotpDefault(secret []byte, code string) (bool, error)

Types

type Algorithm

type Algorithm int
const (
	SHA1 Algorithm = iota
	SHA256
	SHA512
)

func (Algorithm) String

func (a Algorithm) String() string

type ClockSkewDetector

type ClockSkewDetector struct {
	// contains filtered or unexported fields
}

func NewClockSkewDetector

func NewClockSkewDetector(capacity int) *ClockSkewDetector

func (*ClockSkewDetector) CurrentOffset

func (d *ClockSkewDetector) CurrentOffset() int64

func (*ClockSkewDetector) DisableAutoAdjust

func (d *ClockSkewDetector) DisableAutoAdjust()

func (*ClockSkewDetector) EnableAutoAdjust

func (d *ClockSkewDetector) EnableAutoAdjust()

func (*ClockSkewDetector) IsAutoAdjust

func (d *ClockSkewDetector) IsAutoAdjust() bool

func (*ClockSkewDetector) Record

func (d *ClockSkewDetector) Record(matchedOffset int64, windowUsed uint64)

func (*ClockSkewDetector) Report

func (d *ClockSkewDetector) Report() SkewReport

func (*ClockSkewDetector) Reset

func (d *ClockSkewDetector) Reset()

type GenOtpError

type GenOtpError struct {
	Message string
}

func NewGenOtpError

func NewGenOtpError(msg string) *GenOtpError

func (*GenOtpError) Error

func (e *GenOtpError) Error() string

type HOTP

type HOTP struct {
	// contains filtered or unexported fields
}

func NewHOTP

func NewHOTP(secret []byte, algorithm Algorithm, digits uint32) (*HOTP, error)

func NewHotpFromConfig

func NewHotpFromConfig(secret []byte, c HotpConfig) (*HOTP, error)

func (*HOTP) ClearSecret

func (h *HOTP) ClearSecret()

func (*HOTP) GenBound

func (h *HOTP) GenBound(counter uint64, context *OtpContext) (string, error)

func (*HOTP) Generate

func (h *HOTP) Generate(counter uint64) (string, error)

func (*HOTP) Verify

func (h *HOTP) Verify(code string, counter uint64) (bool, error)

func (*HOTP) VerifyBound

func (h *HOTP) VerifyBound(code string, counter uint64, context *OtpContext) (bool, error)

func (*HOTP) VerifyWithResync

func (h *HOTP) VerifyWithResync(code string, counter uint64, lookAhead uint64) (uint64, bool, error)

type HotpBuilder

type HotpBuilder struct {
	// contains filtered or unexported fields
}

func NewHotpBuilder

func NewHotpBuilder() *HotpBuilder

func (*HotpBuilder) Algorithm

func (b *HotpBuilder) Algorithm(algorithm Algorithm) *HotpBuilder

func (*HotpBuilder) Build

func (b *HotpBuilder) Build() (*HOTP, error)

func (*HotpBuilder) Digits

func (b *HotpBuilder) Digits(digits uint32) *HotpBuilder

func (*HotpBuilder) Secret

func (b *HotpBuilder) Secret(secret []byte) *HotpBuilder

type HotpConfig

type HotpConfig struct {
	Algorithm Algorithm
	Digits    uint32
}

func NewHotpConfig

func NewHotpConfig() HotpConfig

func (HotpConfig) WithAlgorithm

func (c HotpConfig) WithAlgorithm(algorithm Algorithm) HotpConfig

func (HotpConfig) WithDigits

func (c HotpConfig) WithDigits(digits uint32) HotpConfig

type InMemoryReplayStore

type InMemoryReplayStore struct {
	// contains filtered or unexported fields
}

InMemoryReplayStore adalah default impl — bounded map dengan TTL.

**Anti-amnesia:** versi lama Verifier melakukan `map = make(...)` saat penuh, yang menyebabkan SEMUA kode lama tiba-tiba kembali valid. Itu kelemahan yang bisa dieksploitasi attacker yang tahu cap-nya: flood dengan kode acak supaya cap terlampaui -> semua kode bekas yang dia pernah intercept jadi bisa di-replay lagi.

Implementasi ini ganti pola itu dengan:

  1. Sweep periodik (~tiap 30s) untuk reclaim entries yang sudah expired.
  2. Saat cap-hit, evict 10% entries acak (bukan clear-all). Worst case attacker bisa men-displace 10% entries terlama, bukan 100%.

Pilihan batch 10% acak (vs O(n) eviction per entry tertua):

  • Map iteration di Go acak per range -> setara random eviction tanpa overhead LRU list.
  • Amortized O(1) per CheckAndRecord call: setelah evict 10%, ada ruang 10% sebelum trigger eviction lagi -> cost terbagi merata.
  • 90% entries bertahan dari setiap eviction -> defense-in-depth terhadap amnesia tetap kuat.

Kompleksitas:

  • CheckAndRecord: O(1) amortized, O(0.1n) worst case saat cap-hit.
  • Memory: bounded di maxSize entries x ~100 byte/key worst case.

func NewInMemoryReplayStore

func NewInMemoryReplayStore(maxSize int) *InMemoryReplayStore

NewInMemoryReplayStore membuat store dengan kapasitas maxSize entries. Sweep periodik dilakukan ~tiap 30 detik. Untuk OTP workload normal, maxSize 10.000 menampung beberapa menit traffic, cukup untuk TTL OTP (~90 detik).

func (*InMemoryReplayStore) CheckAndRecord

func (s *InMemoryReplayStore) CheckAndRecord(key []byte, ttl time.Duration) (bool, error)

func (*InMemoryReplayStore) Reset

func (s *InMemoryReplayStore) Reset() error

func (*InMemoryReplayStore) Size

func (s *InMemoryReplayStore) Size() int

Size mengembalikan jumlah entries aktif (termasuk yang sudah expired tapi belum disweep). Buat introspeksi / metrics.

type KeyGen

type KeyGen struct{}

func (*KeyGen) FillSecret

func (k *KeyGen) FillSecret(buf []byte) error

func (*KeyGen) GenerateDefaultSecret

func (k *KeyGen) GenerateDefaultSecret() ([]byte, error)

func (*KeyGen) GenerateSecret

func (k *KeyGen) GenerateSecret(bitLength int) ([]byte, error)

type Metrics

type Metrics struct {
	HotpGenerations   atomic.Uint64
	HotpVerifications atomic.Uint64
	TotpGenerations   atomic.Uint64
	TotpVerifications atomic.Uint64
	Errors            atomic.Uint64
}

func NewMetrics

func NewMetrics() *Metrics

func (*Metrics) GetErrors

func (m *Metrics) GetErrors() uint64

func (*Metrics) GetHotpGenerations

func (m *Metrics) GetHotpGenerations() uint64

func (*Metrics) GetHotpVerifications

func (m *Metrics) GetHotpVerifications() uint64

func (*Metrics) GetTotpGenerations

func (m *Metrics) GetTotpGenerations() uint64

func (*Metrics) GetTotpVerifications

func (m *Metrics) GetTotpVerifications() uint64

func (*Metrics) IncrementError

func (m *Metrics) IncrementError()

func (*Metrics) IncrementHotpGeneration

func (m *Metrics) IncrementHotpGeneration()

func (*Metrics) IncrementHotpVerification

func (m *Metrics) IncrementHotpVerification()

func (*Metrics) IncrementTotpGeneration

func (m *Metrics) IncrementTotpGeneration()

func (*Metrics) IncrementTotpVerification

func (m *Metrics) IncrementTotpVerification()

func (*Metrics) Reset

func (m *Metrics) Reset()

type OtpAuthUri

type OtpAuthUri struct {
	// contains filtered or unexported fields
}

func NewOtpAuthUri

func NewOtpAuthUri(typ OtpType, label, secret string) *OtpAuthUri

func (*OtpAuthUri) Algorithm

func (u *OtpAuthUri) Algorithm(algorithm Algorithm) *OtpAuthUri

func (*OtpAuthUri) Build

func (u *OtpAuthUri) Build() string

func (*OtpAuthUri) Counter

func (u *OtpAuthUri) Counter(counter uint64) *OtpAuthUri

func (*OtpAuthUri) Digits

func (u *OtpAuthUri) Digits(digits uint32) *OtpAuthUri

func (*OtpAuthUri) Issuer

func (u *OtpAuthUri) Issuer(issuer string) *OtpAuthUri

func (*OtpAuthUri) Period

func (u *OtpAuthUri) Period(period uint64) *OtpAuthUri

func (*OtpAuthUri) String

func (u *OtpAuthUri) String() string

type OtpContext

type OtpContext struct {
	// contains filtered or unexported fields
}

func NewOtpContext

func NewOtpContext() *OtpContext

func OtpContextFromBytes

func OtpContextFromBytes(b []byte) *OtpContext

func (*OtpContext) Bytes

func (c *OtpContext) Bytes() []byte

func (*OtpContext) IsEmpty

func (c *OtpContext) IsEmpty() bool

type OtpContextBuilder

type OtpContextBuilder struct {
	// contains filtered or unexported fields
}

func NewOtpContextBuilder

func NewOtpContextBuilder() *OtpContextBuilder

func (*OtpContextBuilder) Build

func (b *OtpContextBuilder) Build() *OtpContext

func (*OtpContextBuilder) Custom

func (b *OtpContextBuilder) Custom(key, value string) *OtpContextBuilder

func (*OtpContextBuilder) Device

func (b *OtpContextBuilder) Device(deviceID string) *OtpContextBuilder

func (*OtpContextBuilder) IP

func (*OtpContextBuilder) Origin

func (b *OtpContextBuilder) Origin(origin string) *OtpContextBuilder

func (*OtpContextBuilder) Session

func (b *OtpContextBuilder) Session(session string) *OtpContextBuilder

type OtpType

type OtpType int
const (
	HotpType OtpType = iota
	TotpType
)

type ReplayStore

type ReplayStore interface {
	// CheckAndRecord mengembalikan true KALAU `key` belum pernah dicatat
	// dalam window TTL-nya (firstSeen). Atomic: kalau dua caller paralel
	// (atau dua replica) memanggil dengan key yang sama bersamaan, tepat
	// satu yang mendapat firstSeen=true.
	//
	// `ttl` = berapa lama key tetap menolak resubmission. Untuk TOTP
	// pilih period x (1 + 2xwindow). Mis. period=30, window=1 -> ttl=90s
	// supaya kode yang valid di counter T-1 ditolak sampai jendela
	// validitas alaminya lewat.
	//
	// Error backend (Redis down, network timeout) di-propagate via err;
	// caller (Verifier) memperlakukan error sebagai "fail closed" — kode
	// ditolak. Lebih baik false negative daripada bypass.
	CheckAndRecord(key []byte, ttl time.Duration) (firstSeen bool, err error)

	// Reset menghapus semua entries. Dipakai oleh testing dan admin
	// reset. Backend distributed bisa FLUSHDB / namespaced delete.
	Reset() error
}

ReplayStore abstrak penyimpanan kode OTP yang sudah pernah diterima.

**Kenapa interface dan bukan map biasa di Verifier:** library ini dirancang bisa dipakai di mode single-process (cukup InMemoryReplayStore) dan distributed (multi-replica di belakang load balancer). Tanpa abstraksi backend, replay protection pecah secara halus di lingkungan distributed:

Server A | Server B | Server C  <- 3 replika di belakang LB
  RAM     |   RAM    |   RAM    <- state TERPISAH per proses

Skenario serangan nyata:

  1. Attacker dapat 1 OTP valid, kirim ke LB -> di-route ke Server A.
  2. Server A.usedCodes catat -> accept.
  3. Attacker kirim ULANG kode yang sama -> kali ini di-route ke Server B.
  4. Server B.usedCodes kosong (state tidak ter-replikasi) -> accept lagi.
  5. Effective replay bypass = N kali, dengan N = jumlah replica.

Solusi: implementasikan ReplayStore dengan storage shared (Redis SET NX EX, etcd lease, sql + unique constraint, dll) supaya semua replica melihat state yang sama. Library kasih default in-memory yang aman untuk single-process; production deploy multi-replica WAJIB pakai distributed backend. Lihat docs/redis_example untuk contoh.

type SkewRecommend

type SkewRecommend int
const (
	InsufficientData SkewRecommend = iota
	NoActionNeeded
	ConsistentDrift
	WidenWindowOrCheckNtp
)

func (SkewRecommend) String

func (r SkewRecommend) String() string

type SkewReport

type SkewReport struct {
	SampleCount  int
	MeanOffset   float64
	NonZeroCount int
	EdgeHitRatio float64
	Recommend    SkewRecommend
}

type TOTP

type TOTP struct {
	// contains filtered or unexported fields
}

func NewTOTP

func NewTOTP(secret []byte, algorithm Algorithm, digits uint32, period uint64) (*TOTP, error)

func NewTotpFromConfig

func NewTotpFromConfig(secret []byte, c TotpConfig) (*TOTP, error)

func (*TOTP) ClearSecret

func (t *TOTP) ClearSecret()

func (*TOTP) GenBound

func (t *TOTP) GenBound(context *OtpContext, timeVal *uint64) (string, error)

func (*TOTP) Generate

func (t *TOTP) Generate(timeVal *uint64) (string, error)

func (*TOTP) Verify

func (t *TOTP) Verify(code string, timeVal *uint64, window uint64) (bool, error)

func (*TOTP) VerifyBound

func (t *TOTP) VerifyBound(code string, context *OtpContext, timeVal *uint64, window uint64) (bool, error)

func (*TOTP) VerifyTracking

func (t *TOTP) VerifyTracking(code string, timeVal *uint64, window uint64, detector *ClockSkewDetector) (bool, error)

type TotpBuilder

type TotpBuilder struct {
	// contains filtered or unexported fields
}

func NewTotpBuilder

func NewTotpBuilder() *TotpBuilder

func (*TotpBuilder) Algorithm

func (b *TotpBuilder) Algorithm(algorithm Algorithm) *TotpBuilder

func (*TotpBuilder) Build

func (b *TotpBuilder) Build() (*TOTP, error)

func (*TotpBuilder) Digits

func (b *TotpBuilder) Digits(digits uint32) *TotpBuilder

func (*TotpBuilder) Period

func (b *TotpBuilder) Period(period uint64) *TotpBuilder

func (*TotpBuilder) Secret

func (b *TotpBuilder) Secret(secret []byte) *TotpBuilder

type TotpConfig

type TotpConfig struct {
	Algorithm Algorithm
	Digits    uint32
	Period    uint64
}

func NewTotpConfig

func NewTotpConfig() TotpConfig

func (TotpConfig) WithAlgorithm

func (c TotpConfig) WithAlgorithm(algorithm Algorithm) TotpConfig

func (TotpConfig) WithDigits

func (c TotpConfig) WithDigits(digits uint32) TotpConfig

func (TotpConfig) WithPeriod

func (c TotpConfig) WithPeriod(period uint64) TotpConfig

type Verifier

type Verifier struct {
	// contains filtered or unexported fields
}

func NewVerifier

func NewVerifier(maxAttempts uint32) *Verifier

NewVerifier membuat Verifier dengan InMemoryReplayStore default (10.000 entries, TTL 90 detik). Cocok untuk single-process / single-replica.

**Untuk deployment multi-replica (Kubernetes, dll):** in-memory store TIDAK memberikan replay protection lintas replica — state pisah per-process. Pakai NewVerifierWithStore dengan implementasi ReplayStore yang shared (Redis SET NX EX, dll). Lihat docs/redis_replay_store.go.example.

func NewVerifierWithCapacity

func NewVerifierWithCapacity(maxAttempts uint32, maxUsedCodes int) *Verifier

NewVerifierWithCapacity sama dengan NewVerifier tapi memungkinkan override kapasitas in-memory store.

func NewVerifierWithStore

func NewVerifierWithStore(maxAttempts uint32, store ReplayStore, ttl time.Duration) *Verifier

NewVerifierWithStore membuat Verifier dengan ReplayStore custom dan TTL eksplisit. Dipakai untuk inject Redis / etcd / sql backend untuk distributed replay protection.

**Catatan rate-limit (attempts counter):** masih per-instance, BUKAN distributed. Untuk distributed rate-limit, caller bertanggung jawab implement di layer di atas (mis. Redis INCR + EXPIRE di middleware gateway, atau pakai package rate-limit khusus). Library tidak mengabstraksi ini supaya scope tetap sempit.

func (*Verifier) ClearUsedCodes

func (v *Verifier) ClearUsedCodes()

ClearUsedCodes membersihkan replay-set. Untuk InMemoryReplayStore, drop semua entries. Untuk Redis backend, panggil pattern delete / FLUSHDB di implementor.

func (*Verifier) IsRateLimited

func (v *Verifier) IsRateLimited() bool

func (*Verifier) ResetAttempts

func (v *Verifier) ResetAttempts()

func (*Verifier) VerifyWithContext

func (v *Verifier) VerifyWithContext(code, expected string, issuedContext, requestContext *OtpContext) bool

func (*Verifier) VerifyWithReplayProtection

func (v *Verifier) VerifyWithReplayProtection(code, expected string) bool

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL