// Package otp реализует алгоритмы одноразовых паролей HOTP (RFC 4226) // и TOTP (RFC 6238) в том виде, в каком их использует SafeNet eToken PASS. package otp import ( "crypto/hmac" "crypto/sha1" "crypto/sha256" "crypto/sha512" "crypto/subtle" "encoding/binary" "fmt" "hash" "strings" "time" ) // Algorithm — хеш, на котором строится HMAC. eToken PASS использует SHA1. type Algorithm int const ( SHA1 Algorithm = iota SHA256 SHA512 ) func (a Algorithm) String() string { switch a { case SHA256: return "SHA256" case SHA512: return "SHA512" default: return "SHA1" } } func (a Algorithm) new() func() hash.Hash { switch a { case SHA256: return sha256.New case SHA512: return sha512.New default: return sha1.New } } // ParseAlgorithm разбирает название алгоритма без учёта регистра. func ParseAlgorithm(s string) (Algorithm, error) { switch strings.ToUpper(strings.TrimSpace(s)) { case "SHA1", "SHA-1", "": return SHA1, nil case "SHA256", "SHA-256": return SHA256, nil case "SHA512", "SHA-512": return SHA512, nil } return SHA1, fmt.Errorf("неизвестный алгоритм %q (допустимо: sha1, sha256, sha512)", s) } // DefaultStep — шаг времени TOTP по умолчанию (RFC 6238). const DefaultStep = 30 * time.Second // Config описывает параметры конкретного токена. type Config struct { Secret []byte // вектор инициализации (seed) Digits int // разрядность кода: 6 или 8 Alg Algorithm // хеш для HMAC Step time.Duration // шаг времени, только для TOTP } // Validate проверяет параметры на осмысленность. func (c Config) Validate() error { if len(c.Secret) == 0 { return fmt.Errorf("пустой seed") } if c.Digits < 6 || c.Digits > 10 { return fmt.Errorf("разрядность %d вне допустимого диапазона 6..10", c.Digits) } if c.Step <= 0 { return fmt.Errorf("шаг времени должен быть положительным") } return nil } var pow10 = [...]uint32{1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000} // HOTP считает одноразовый пароль для указанного значения счётчика (RFC 4226). func (c Config) HOTP(counter uint64) string { var buf [8]byte binary.BigEndian.PutUint64(buf[:], counter) mac := hmac.New(c.Alg.new(), c.Secret) mac.Write(buf[:]) sum := mac.Sum(nil) // Динамическое усечение: младшие 4 бита последнего байта задают смещение. offset := sum[len(sum)-1] & 0x0F code := binary.BigEndian.Uint32(sum[offset:offset+4]) & 0x7FFFFFFF digits := c.Digits if digits < 1 || digits >= len(pow10) { digits = 6 } return fmt.Sprintf("%0*d", digits, code%pow10[digits]) } // TimeStep возвращает номер временного шага T для момента t (RFC 6238). func (c Config) TimeStep(t time.Time) uint64 { step := c.Step if step <= 0 { step = DefaultStep } return uint64(t.Unix() / int64(step/time.Second)) } // StepStart возвращает момент начала временного шага с номером step. func (c Config) StepStart(step uint64) time.Time { d := c.Step if d <= 0 { d = DefaultStep } return time.Unix(int64(step)*int64(d/time.Second), 0) } // TOTP считает одноразовый пароль, действующий в момент t. func (c Config) TOTP(t time.Time) string { return c.HOTP(c.TimeStep(t)) } // equalCode сравнивает коды за постоянное время. func equalCode(a, b string) bool { return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1 }