136 lines
3.9 KiB
Go
136 lines
3.9 KiB
Go
// 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
|
|
}
|