Initial commit: hotpcalc CLI
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user