package authenticator import ( "fmt" "strconv" "strings" "git.wxccs.org/iceking2nd/winauth-go/internal/global" ) // HOTPAuthenticator implements RFC 4226 counter-based HOTP. type HOTPAuthenticator struct { Base Counter uint64 } // NewHOTPAuthenticator returns a fresh HOTP authenticator with the project // defaults (6 digits, SHA1). func NewHOTPAuthenticator() *HOTPAuthenticator { return &HOTPAuthenticator{Base: NewBase()} } // Name returns the short logger tag for this type. func (h *HOTPAuthenticator) Name() string { return "hotp" } // Enroll loads the secret from a base32 string and optionally seeds the // counter. func (h *HOTPAuthenticator) Enroll(b32 string, counter uint64) error { const fn = "internal.authenticator.HOTPAuthenticator.Enroll" raw, err := Base32Decode(b32) if err != nil { return err } h.SecretKey = raw h.Counter = counter global.Log.WithField("func", fn).WithField("counter", counter).Debug("enrolled HOTP") return nil } // CurrentCode increments the internal counter and returns the resulting code. func (h *HOTPAuthenticator) CurrentCode() (string, error) { if h.SecretKey == nil { return "", fmt.Errorf("authenticator: no secret loaded") } h.Counter++ digits := h.CodeDigits if digits == 0 { digits = DefaultCodeDigits } return hotpCode(h.SecretKey, h.Counter, digits, h.HMACType), nil } // Sync is a no-op for HOTP — there is no server clock to align against. func (h *HOTPAuthenticator) Sync() error { return nil } // SecretData appends "|" to the base secret data string, matching // the C# HOTPAuthenticator.SecretData getter. func (h *HOTPAuthenticator) SecretData() string { return h.EncodeSecretData() + "|" + strconv.FormatUint(h.Counter, 10) } // SetSecretData parses the "|" form. func (h *HOTPAuthenticator) SetSecretData(value string) error { if err := h.DecodeSecretData(value); err != nil { return err } if idx := strings.Index(value, "|"); idx >= 0 { c, err := strconv.ParseUint(strings.TrimSpace(value[idx+1:]), 10, 64) if err == nil { h.Counter = c } } return nil }